diff --git a/QMUI/.github/ISSUE_TEMPLATE.md b/QMUI/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index ec59617c..00000000 --- a/QMUI/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,10 +0,0 @@ -### 运行环境 ### - -- [x] iOS 设备:`iPhone` / `iPad` / `模拟器` -- [x] 系统版本:`iOS 10.x` -- [x] Xcode 版本:`8.x` -- [x] QMUI iOS 版本:`1.x.x` - -### 具体问题描述 ### - -#### 问题截图 #### diff --git a/QMUI/.gitignore b/QMUI/.gitignore index 9741d4f2..1d74e0bf 100644 --- a/QMUI/.gitignore +++ b/QMUI/.gitignore @@ -1,26 +1,4 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -qmui.xcodeproj/project.xcworkspace/xcuserdata/ .DS_Store +.xcuserdatad +xcuserdata -## Build generated -build/ -DerivedData/ - -## Various settings -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata/ - -## Other -*.moved-aside -*.xccheckout -*.xcscmblueprint diff --git a/QMUI/CONTRIBUTING.md b/QMUI/CONTRIBUTING.md new file mode 100644 index 00000000..d02683b6 --- /dev/null +++ b/QMUI/CONTRIBUTING.md @@ -0,0 +1,19 @@ +[腾讯开源激励计划](https://opensource.tencent.com/contribution) 鼓励开发者的参与和贡献,期待你的加入。我们欢迎 [report Issues](https://github.com/QMUI/QMUI_iOS/issues) 或者 [pull requests](https://github.com/QMUI/QMUI_iOS/pulls)。 在贡献代码之前请阅读以下指引。 + +## 问题管理 +我们用 Github Issues 去跟踪 public bugs 和 feature requests。 + +### 使用 Issues + +1. 新建 issues 前,请查找已存在或者相类似的 issue,从而保证不存在冗余。 +2. 新建 issues 时,请根据我们提供的 issue 模板,尽可能提供详细的描述、截屏或者短视频来辅助我们定位问题。 + +### Pull Requests + +我们欢迎大家为 QMUI_iOS 贡献代码,在完成一个 pull request 之前请确认: + +1. 从 `master` fork 你自己的分支。 +2. 在修改了代码之后请修改对应的文档和注释。 +3. 在新建的文件中请加入 licence 和 copy right 声明。 +4. 确保一致的代码风格。 +5. 做充分的测试。 diff --git a/QMUI/LICENSE b/QMUI/LICENSE deleted file mode 100644 index 3ab3636d..00000000 --- a/QMUI/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -(The MIT License) - - Copyright (c) 2016 QMUI Team (http://qmuiteam.com) - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. diff --git a/QMUI/LICENSE.TXT b/QMUI/LICENSE.TXT new file mode 100644 index 00000000..485d8bd0 --- /dev/null +++ b/QMUI/LICENSE.TXT @@ -0,0 +1,14 @@ +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. +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. + + +Terms of the MIT License: +--------------------------------------------------- +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h index e019d335..543a96f4 100644 --- a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h +++ b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.h @@ -1,22 +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. + */ + // // QMUIConfigurationTemplate.h // -// Created by QQMail on 15/3/29. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/3/29. // #import +#import /** - * QMUIConfigurationTemplate 是一份配置表,用于配合 QMUIKit 来管理整个 App 的全局样式,使用方式如下: - * 1. 在 QMUI 项目代码的文件夹里找到 QMUIConfigurationTemplate 目录,把里面所有文件复制到自己项目里。 - * 2. 在自己项目的 AppDelegate 里 #import "QMUIConfigurationTemplate.h",然后在 application:didFinishLaunchingWithOptions: 里调用 [QMUIConfigurationTemplate setupConfigurationTemplate],即可让配置表生效。 - * 3. 更新 QMUIKit 的版本时,请留意 Release Log 里是否有提醒更新配置表,请尽量保持自己项目里的配置表与 QMUIKit 里的配置表一致,避免遗漏新的属性。 + * QMUIConfigurationTemplate 是一份配置表,用于配合 QMUIConfiguration 来管理整个 App 的全局样式,使用方式: + * 在 QMUI 项目代码的文件夹里找到 QMUIConfigurationTemplate 目录,把里面所有文件复制到自己项目里,保证能被编译到即可,不需要在某些地方 import,也不需要手动运行。 * - * @warning 请不要在 + load 方法里调用 QMUIConfigurationTemplate 或 QMUIConfigurationMacros 提供的宏,那个时机太早,可能导致 crash + * @warning 更新 QMUIKit 的版本时,请留意 Release Log 里是否有提醒更新配置表,请尽量保持自己项目里的配置表与 QMUIKit 里的配置表一致,避免遗漏新的属性。 + * @warning 配置表的 class 名必须以 QMUIConfigurationTemplate 开头,并且实现 ,因为这两者是 QMUI 识别该 NSObject 是否为一份配置表的条件。 + * @warning QMUI 2.3.0 之后,配置表改为自动运行,不需要再在某个地方手动运行了。 */ -@interface QMUIConfigurationTemplate : NSObject - -+ (void)setupConfigurationTemplate; +@interface QMUIConfigurationTemplate : NSObject @end diff --git a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m index a651b4ca..74e36837 100644 --- a/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m +++ b/QMUI/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m @@ -1,9 +1,16 @@ +/** + * 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. + */ + // // QMUIConfigurationTemplate.m // qmui // -// Created by QQMail on 15/3/29. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/3/29. // #import "QMUIConfigurationTemplate.h" @@ -11,15 +18,17 @@ @implementation QMUIConfigurationTemplate -+ (void)setupConfigurationTemplate { +#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.clearColor = UIColorMakeWithRGBA(255, 255, 255, 0); // UIColorClear : 透明色 + QMUICMI.whiteColor = UIColorMake(255, 255, 255); // UIColorWhite : 白色(不用 [UIColor whiteColor] 是希望保持颜色空间为 RGB) + QMUICMI.blackColor = UIColorMake(0, 0, 0); // UIColorBlack : 黑色(不用 [UIColor blackColor] 是希望保持颜色空间为 RGB) QMUICMI.grayColor = UIColorMake(179, 179, 179); // UIColorGray : 最常用的灰色 QMUICMI.grayDarkenColor = UIColorMake(163, 163, 163); // UIColorGrayDarken : 深一点的灰色 QMUICMI.grayLightenColor = UIColorMake(198, 198, 198); // UIColorGrayLighten : 浅一点的灰色 @@ -33,7 +42,7 @@ + (void)setupConfigurationTemplate { 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 的输入框 @@ -42,6 +51,11 @@ + (void)setupConfigurationTemplate { 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 @@ -53,93 +67,117 @@ + (void)setupConfigurationTemplate { 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 : QMUINavigationButton 的字体 - 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 : UINavigationBar 的 tintColor,也即导航栏上面的按钮颜色 - QMUICMI.navBarTitleColor = UIColorBlack; // NavBarTitleColor : UINavigationBar 的标题颜色,以及 QMUINavigationTitleView 的默认文字颜色 + 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 在大标题模式下的标题颜色 + QMUICMI.navBarLargeTitleFont = nil; // NavBarLargeTitleFont : UINavigationBar 在大标题模式下的标题字体 QMUICMI.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; // NavBarBarBackButtonTitlePositionAdjustment : 导航栏返回按钮的文字偏移 - QMUICMI.navBarBackIndicatorImage = nil; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片 + QMUICMI.sizeNavBarBackIndicatorImageAutomatically = YES; // SizeNavBarBackIndicatorImageAutomatically : 是否要自动调整 NavBarBackIndicatorImage 的 size 为 (13, 21) + QMUICMI.navBarBackIndicatorImage = nil; // 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:UIColorWhite]; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 + QMUICMI.navBarAccessoryViewTypeDisclosureIndicatorImage = [[UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:nil] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 #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 里的文本框的圆角大小 - #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 : 列表右边的字母索引条在选中时的背景色 QMUICMI.tableViewSeparatorColor = UIColorSeparator; // TableViewSeparatorColor : 列表的分隔线颜色 - QMUICMI.tableViewCellNormalHeight = 44; // TableViewCellNormalHeight : 列表默认的 cell 高度 + 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 时的箭头的图片 @@ -153,32 +191,99 @@ + (void)setupConfigurationTemplate { QMUICMI.tableViewSectionFooterFont = UIFontBoldMake(12); // TableViewSectionFooterFont : Plain 类型的 QMUITableView sectionFooter 里的文字字体 QMUICMI.tableViewSectionHeaderTextColor = UIColorGrayDarken; // TableViewSectionHeaderTextColor : Plain 类型的 QMUITableView sectionHeader 里的文字颜色 QMUICMI.tableViewSectionFooterTextColor = UIColorGray; // TableViewSectionFooterTextColor : Plain 类型的 QMUITableView sectionFooter 里的文字颜色 - QMUICMI.tableViewSectionHeaderHeight = 20; // TableViewSectionHeaderHeight : Plain 类型的 QMUITableView sectionHeader 的默认高度 - QMUICMI.tableViewSectionFooterHeight = 0; // TableViewSectionFooterHeight : 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 + 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 里的文字颜色 QMUICMI.tableViewGroupedSectionFooterTextColor = UIColorGray; // TableViewGroupedSectionFooterTextColor : Grouped 类型的 QMUITableView sectionFooter 里的文字颜色 - QMUICMI.tableViewGroupedSectionHeaderHeight = 15; // TableViewGroupedSectionHeaderHeight : Grouped 类型的 QMUITableView sectionHeader 的默认高度 - QMUICMI.tableViewGroupedSectionFooterHeight = 1; // TableViewGroupedSectionFooterHeight : 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, 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 - QMUICMI.windowLevelQMUIImagePreviewView = UIWindowLevelStatusBar + 1.0; // UIWindowLevelQMUIImagePreviewView : QMUIImagePreviewViewController 里使用的 UIWindow 的 windowLevel + QMUICMI.windowLevelQMUIConsole = 1; // UIWindowLevelQMUIConsole : QMUIConsole 内部的 UIWindow 的 windowLevel + + #pragma mark - QMUIBadge + + 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 : 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.supportedOrientationMask = UIInterfaceOrientationMaskPortrait; // SupportedOrientationMask : 默认支持的横竖屏方向 - QMUICMI.automaticallyRotateDeviceOrientation = NO; // AutomaticallyRotateDeviceOrientation : 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕 - QMUICMI.statusbarStyleLightInitially = NO; // StatusbarStyleLightInitially : 默认的状态栏内容是否使用白色,默认为 NO,也即黑色 - QMUICMI.needsBackBarButtonItemTitle = NO; // NeedsBackBarButtonItemTitle : 全局是否需要返回按钮的 title,不需要则只显示一个返回image + QMUICMI.automaticCustomNavigationBarTransitionStyle = NO; // AutomaticCustomNavigationBarTransitionStyle : 界面 push/pop 时是否要自动根据两个界面的 barTintColor/backgroundImage/shadowImage 的样式差异来决定是否使用自定义的导航栏效果 + QMUICMI.supportedOrientationMask = UIInterfaceOrientationMaskAll; // SupportedOrientationMask : 默认支持的横竖屏方向 + 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.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.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。 +- (BOOL)shouldApplyTemplateAutomatically { + return YES; } @end diff --git a/QMUI/QMUIKit.podspec b/QMUI/QMUIKit.podspec index 54e9ca9e..aef548fd 100644 --- a/QMUI/QMUIKit.podspec +++ b/QMUI/QMUIKit.podspec @@ -1,24 +1,420 @@ Pod::Spec.new do |s| s.name = "QMUIKit" - s.version = "1.7.4" + s.version = "4.8.0" s.summary = "致力于提高项目 UI 开发效率的解决方案" s.description = <<-DESC QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设计目的是用于辅助快速搭建一个具备基本设计还原效果的 iOS 项目,同时利用自身提供的丰富控件及兼容处理, 让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。 DESC - s.homepage = "http://qmuiteam.com/ios" + s.homepage = "https://github.com/Tencent/QMUI_iOS" s.license = 'MIT' - s.author = {"qmuiteam" => "qmuiteam@qq.com"} - s.source = {:git => "https://github.com/QMUI/QMUI_iOS.git", :tag => s.version.to_s} - s.social_media_url = 'https://github.com/QMUI/QMUI_iOS' + 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 = 'http://qmuiteam.com/ios/page/document.html' - s.screenshot = 'https://cloud.githubusercontent.com/assets/1190261/26751376/63f96538-486a-11e7-81cf-5bc83a945207.png' + 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, '7.0' - s.source_files = 'QMUIKit/**/*.{h,m}' + s.platform = :ios, '13.0' + s.frameworks = 'Foundation', 'UIKit', 'CoreGraphics' s.preserve_paths = 'QMUIConfigurationTemplate/*' - s.resource = 'QMUIKit/**/*.bundle' + s.source_files = 'QMUIKit/QMUIKit.h' + s.resource_bundles = {'QMUIKit' => ['QMUIKit/PrivacyInfo.xcprivacy']} - s.frameworks = 'Foundation', 'UIKit', 'CoreGraphics', 'Photos' + s.subspec 'QMUICore' do |ss| + 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 'QMUIMainFrame' do |ss| + ss.source_files = 'QMUIKit/QMUIMainFrame' + ss.dependency 'QMUIKit/QMUICore' + ss.dependency 'QMUIKit/QMUIComponents/QMUINavigationTitleView' + ss.dependency 'QMUIKit/QMUIComponents/QMUITableView' + ss.dependency 'QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView' + ss.dependency 'QMUIKit/QMUIComponents/QMUIEmptyView' + ss.dependency 'QMUIKit/QMUIComponents/QMUIKeyboardManager' + ss.dependency 'QMUIKit/QMUILog' + 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 + + s.subspec 'QMUILog' do |ss| + ss.source_files = 'QMUIKit/QMUIComponents/QMUILog/*.{h,m}' + end + + s.subspec 'QMUIComponents' do |ss| + + ss.dependency 'QMUIKit/QMUICore' + + ss.subspec 'QMUICAAnimationExtension' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/CAAnimation+QMUI.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' + end + + ss.subspec 'QMUICALayerExtension' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' + end + + ss.subspec 'QMUIAnimation' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIAnimation' + end + + 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 'QMUIButton' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUIButton.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUILayouter' + end + + ss.subspec 'QMUINavigationButton' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.{h,m}' + sss.dependency 'QMUIKit/QMUIMainFrame' + end + + ss.subspec 'QMUIToolbarButton' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.{h,m}' + end + + ss.subspec 'QMUITableView' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUITableView.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUITableViewProtocols' + end + + ss.subspec 'QMUITableViewProtocols' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUITableViewProtocols.{h,m}' + end + + 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 + + # 从这里开始就是非必须的组件 + + ss.subspec 'QMUIMultipleDelegates' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIMultipleDelegates/*.{h,m}' + end + + ss.subspec 'QMUIAlertController' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIAlertController.{h,m}' + 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| + sss.source_files = 'QMUIKit/QMUIComponents/QMUICellHeightCache.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUITableViewProtocols' + end + + ss.subspec 'QMUICellHeightKeyCache' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUICellHeightKeyCache/*.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUITableViewProtocols' + sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' + end + + ss.subspec 'QMUICellSizeKeyCache' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUICellSizeKeyCache/*.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' + end + + 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 + + ss.subspec 'QMUICollectionViewPagingLayout' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.{h,m}' + end + + ss.subspec 'QMUIDialogViewController' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIDialogViewController.{h,m}' + sss.dependency 'QMUIKit/QMUIMainFrame' + sss.dependency 'QMUIKit/QMUIComponents/QMUILabel' + sss.dependency 'QMUIKit/QMUIComponents/QMUIModalPresentationViewController' + sss.dependency 'QMUIKit/QMUIComponents/QMUITableView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + 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/QMUIResources' + sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + end + + ss.subspec 'QMUIFloatLayoutView' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIFloatLayoutView.{h,m}' + end + + ss.subspec 'QMUIGridView' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIGridView.{h,m}' + end + + ss.subspec 'QMUIImagePreviewView' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIImagePreviewView/*.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIZoomImageView' + sss.dependency 'QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout' + 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 + + ss.subspec 'QMUIMarqueeLabel' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIMarqueeLabel.{h,m}' + end + + 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| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIOrderedDictionary.{h,m}' + end + + ss.subspec 'QMUIPieProgressView' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIPieProgressView.{h,m}' + end + + 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| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIScrollAnimator/*.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' + end + + ss.subspec 'QMUIEmotionInputManager' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIEmotionInputManager.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIEmotionView' + end + + ss.subspec 'QMUISearchBar' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUISearchBar.{h,m}' + end + + ss.subspec 'QMUISearchController' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUISearchController.{h,m}' + sss.dependency 'QMUIKit/QMUIMainFrame' + sss.dependency 'QMUIKit/QMUIComponents/QMUISearchBar' + sss.dependency 'QMUIKit/QMUIComponents/QMUIEmptyView' + end + + ss.subspec 'QMUISegmentedControl' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUISegmentedControl.{h,m}' + end + + ss.subspec 'QMUITableViewCell' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUITableViewCell.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + end + + ss.subspec 'QMUITableViewHeaderFooterView' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.{h,m}' + end + + ss.subspec 'QMUITestView' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUITestView.{h,m}' + end + + ss.subspec 'QMUITextField' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUITextField.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates' + end + + ss.subspec 'QMUITextView' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUITextView.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUILabel' + 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/QMUIResources' + sss.dependency 'QMUIKit/QMUIComponents/QMUIToastView' + end + + 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/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| + sss.source_files = 'QMUIKit/QMUIComponents/ImagePickerLibrary/*.{h,m}' + sss.dependency 'QMUIKit/QMUIMainFrame' + sss.dependency 'QMUIKit/QMUIResources' + sss.dependency 'QMUIKit/QMUIComponents/QMUIImagePreviewView' + sss.dependency 'QMUIKit/QMUIComponents/QMUITableViewCell' + sss.dependency 'QMUIKit/QMUIComponents/QMUIButton' + sss.dependency 'QMUIKit/QMUIComponents/QMUINavigationButton' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAssetLibrary' + sss.dependency 'QMUIKit/QMUIComponents/QMUIZoomImageView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAlertController' + sss.dependency 'QMUIKit/QMUIComponents/QMUIEmptyView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIAppearance' + end + + + ss.subspec 'QMUILogManagerViewController' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUILogManagerViewController.{h,m}' + sss.dependency 'QMUIKit/QMUIMainFrame' + sss.dependency 'QMUIKit/QMUIComponents/QMUIStaticTableView' + sss.dependency 'QMUIKit/QMUIComponents/QMUITableView' + sss.dependency 'QMUIKit/QMUIComponents/QMUIPopupMenuView' + sss.dependency 'QMUIKit/QMUIComponents/QMUISearchController' + end + + ss.subspec 'QMUILogWithConfigurationSupported' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.{h,m}' + end + + ss.subspec 'NavigationBarTransition' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/NavigationBarTransition/*.{h,m}' + sss.dependency 'QMUIKit/QMUIMainFrame' + sss.dependency 'QMUIKit/QMUIComponents/QMUINavigationTitleView' + end + + ss.subspec 'QMUIBadge' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/QMUIBadge/*.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUILabel' + end + + ss.subspec 'QMUIToastView' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/ToastView/*.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUIKeyboardManager' + end + + ss.subspec 'QMUIStaticTableView' do |sss| + sss.source_files = 'QMUIKit/QMUIComponents/StaticTableView/*.{h,m}' + sss.dependency 'QMUIKit/QMUIComponents/QMUITableViewCell' + 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 fbe1e6b3..ec0cc7b0 100644 --- a/QMUI/QMUIKit/Info.plist +++ b/QMUI/QMUIKit/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.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 new file mode 100644 index 00000000..9277b810 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.h @@ -0,0 +1,158 @@ +/** + * 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. + */ + +// +// QMUIAsset.h +// qmui +// +// Created by QMUI Team on 15/6/30. +// + +#import +#import + +typedef NS_ENUM(NSUInteger, QMUIAssetType) { + QMUIAssetTypeUnknow, + QMUIAssetTypeImage, + QMUIAssetTypeVideo, + QMUIAssetTypeAudio +}; + +typedef NS_ENUM(NSUInteger, QMUIAssetSubType) { + QMUIAssetSubTypeUnknow, + QMUIAssetSubTypeImage, + QMUIAssetSubTypeLivePhoto NS_ENUM_AVAILABLE_IOS(9_1), + QMUIAssetSubTypeGIF +}; + +/// Status when download asset from iCloud +typedef NS_ENUM(NSUInteger, QMUIAssetDownloadStatus) { + QMUIAssetDownloadStatusSucceed, + QMUIAssetDownloadStatusDownloading, + QMUIAssetDownloadStatusCanceled, + QMUIAssetDownloadStatusFailed +}; + + +@class PHAsset; + +/** + * 相册里某一个资源的包装对象,该资源可能是图片、视频等。 + * @note QMUIAsset 重写了 isEqual: 方法,只要两个 QMUIAsset 的 identifier 相同,则认为是同一个对象,以方便在数组、字典等容器中对大量 QMUIAsset 进行遍历查找等操作。 + */ +@interface QMUIAsset : NSObject + +@property(nonatomic, assign, readonly) QMUIAssetType assetType; +@property(nonatomic, assign, readonly) QMUIAssetSubType assetSubType; + +- (instancetype)initWithPHAsset:(PHAsset *)phAsset; + +@property(nonatomic, strong, readonly) PHAsset *phAsset; +@property(nonatomic, assign, readonly) QMUIAssetDownloadStatus downloadStatus; // 从 iCloud 下载资源大图的状态 +@property(nonatomic, assign) double downloadProgress; // 从 iCloud 下载资源大图的进度 +@property(nonatomic, assign) NSInteger requestID; // 从 iCloud 请求获得资源的大图的请求 ID +@property (nonatomic, copy, readonly) NSString *identifier;// Asset 的标识,每个 QMUIAsset 的 identifier 都不同。只要两个 QMUIAsset 的 identifier 相同则认为它们是同一个 asset + +/// Asset 的原图(包含系统相册“编辑”功能处理后的效果) +- (UIImage *)originImage; + +/** + * Asset 的缩略图 + * + * @param size 指定返回的缩略图的大小,pt 为单位 + * + * @return Asset 的缩略图 + */ +- (UIImage *)thumbnailWithSize:(CGSize)size; + +/** + * Asset 的预览图 + * + * @warning 输出与当前设备屏幕大小相同尺寸的图片,如果图片原图小于当前设备屏幕的尺寸,则只输出原图大小的图片 + * @return Asset 的全屏图 + */ +- (UIImage *)previewImage; + +/** + * 异步请求 Asset 的原图,包含了系统照片“编辑”功能处理后的效果(剪裁,旋转和滤镜等),可能会有网络请求 + * + * @param completion 完成请求后调用的 block,参数中包含了请求的原图以及图片信息,这个 block 会被多次调用, + * 其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直到获取到高清图。 + * @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。 + * + * @return 返回请求图片的请求 id + */ +- (NSInteger)requestOriginImageWithCompletion:(void (^)(UIImage *result, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler; + +/** + * 异步请求 Asset 的缩略图,不会产生网络请求 + * + * @param size 指定返回的缩略图的大小 + * @param completion 完成请求后调用的 block,参数中包含了请求的缩略图以及图片信息,这个 block 会被多次调用, + * 其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直到获取到高清图,这时 block 中的第二个参数(图片信息)返回的为 nil。 + * + * @return 返回请求图片的请求 id + */ +- (NSInteger)requestThumbnailImageWithSize:(CGSize)size completion:(void (^)(UIImage *result, NSDictionary *info))completion; + +/** + * 异步请求 Asset 的预览图,可能会有网络请求 + * + * @param completion 完成请求后调用的 block,参数中包含了请求的预览图以及图片信息,这个 block 会被多次调用, + * 其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直到获取到高清图。 + * @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。 + * + * @return 返回请求图片的请求 id + */ +- (NSInteger)requestPreviewImageWithCompletion:(void (^)(UIImage *result, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler; + +/** + * 异步请求 Live Photo,可能会有网络请求 + * + * @param completion 完成请求后调用的 block,参数中包含了请求的 Live Photo 以及相关信息,若 assetType 不是 QMUIAssetTypeLivePhoto 则为 nil + * @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。 + * + * @warning iOS 9.1 以下中并没有 Live Photo,因此无法获取有效结果。 + * + * @return 返回请求图片的请求 id + */ +- (NSInteger)requestLivePhotoWithCompletion:(void (^)(PHLivePhoto *livePhoto, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler; + +/** + * 异步请求 AVPlayerItem,可能会有网络请求 + * + * @param completion 完成请求后调用的 block,参数中包含了请求的 AVPlayerItem 以及相关信息,若 assetType 不是 QMUIAssetTypeVideo 则为 nil + * @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。 + * + * @return 返回请求 AVPlayerItem 的请求 id + */ +- (NSInteger)requestPlayerItemWithCompletion:(void (^)(AVPlayerItem *playerItem, NSDictionary *info))completion withProgressHandler:(PHAssetVideoProgressHandler)phProgressHandler; + +/** + * 异步请求图片的 Data + * + * @param completion 完成请求后调用的 block,参数中包含了请求的图片 Data(若 assetType 不是 QMUIAssetTypeImage 或 QMUIAssetTypeLivePhoto 则为 nil),该图片是否为 GIF 的判断值,以及该图片的文件格式是否为 HEIC + */ +- (void)requestImageData:(void (^)(NSData *imageData, NSDictionary *info, BOOL isGIF, BOOL isHEIC))completion; + +/** + * 获取图片的 UIImageOrientation 值,仅 assetType 为 QMUIAssetTypeImage 或 QMUIAssetTypeLivePhoto 时有效 + */ +- (UIImageOrientation)imageOrientation; + +/// 更新下载资源的结果 +- (void)updateDownloadStatusWithDownloadResult:(BOOL)succeed; + +/** + * 获取 Asset 的体积(数据大小) + */ +- (void)assetSize:(void (^)(long long size))completion; + +- (NSTimeInterval)duration; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m new file mode 100644 index 00000000..0f6b6b7e --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m @@ -0,0 +1,345 @@ +/** + * 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. + */ + +// +// QMUIAsset.m +// qmui +// +// Created by QMUI Team on 15/6/30. +// + +#import "QMUIAsset.h" +#import +#import +#import "QMUICore.h" +#import "QMUIAssetsManager.h" +#import "NSString+QMUI.h" + +static NSString * const kAssetInfoImageData = @"imageData"; +static NSString * const kAssetInfoOriginInfo = @"originInfo"; +static NSString * const kAssetInfoDataUTI = @"dataUTI"; +static NSString * const kAssetInfoOrientation = @"orientation"; +static NSString * const kAssetInfoSize = @"size"; + +@interface QMUIAsset () + +@property(nonatomic, copy) NSDictionary *phAssetInfo; +@end + +@implementation QMUIAsset { + PHAsset *_phAsset; + float imageSize; +} + +- (instancetype)initWithPHAsset:(PHAsset *)phAsset { + if (self = [super init]) { + _phAsset = phAsset; + switch (phAsset.mediaType) { + case PHAssetMediaTypeImage: + _assetType = QMUIAssetTypeImage; + if ([[phAsset qmui_valueForKey:@"uniformTypeIdentifier"] isEqualToString:(__bridge NSString *)kUTTypeGIF]) { + _assetSubType = QMUIAssetSubTypeGIF; + } else { + if (phAsset.mediaSubtypes & PHAssetMediaSubtypePhotoLive) { + _assetSubType = QMUIAssetSubTypeLivePhoto; + } else { + _assetSubType = QMUIAssetSubTypeImage; + } + } + break; + case PHAssetMediaTypeVideo: + _assetType = QMUIAssetTypeVideo; + break; + case PHAssetMediaTypeAudio: + _assetType = QMUIAssetTypeAudio; + break; + default: + _assetType = QMUIAssetTypeUnknow; + break; + } + } + return self; +} + +- (PHAsset *)phAsset { + return _phAsset; +} + +- (UIImage *)originImage { + __block UIImage *resultImage = nil; + PHImageRequestOptions *phImageRequestOptions = [[PHImageRequestOptions alloc] init]; + phImageRequestOptions.deliveryMode = PHImageRequestOptionsDeliveryModeHighQualityFormat; + phImageRequestOptions.networkAccessAllowed = YES; + phImageRequestOptions.synchronous = YES; + [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageDataForAsset:_phAsset options:phImageRequestOptions resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) { + resultImage = [UIImage imageWithData:imageData]; + }]; + return resultImage; +} + +- (UIImage *)thumbnailWithSize:(CGSize)size { + __block UIImage *resultImage; + PHImageRequestOptions *phImageRequestOptions = [[PHImageRequestOptions alloc] init]; + phImageRequestOptions.networkAccessAllowed = YES; + phImageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeFast; + // 在 PHImageManager 中,targetSize 等 size 都是使用 px 作为单位,因此需要对targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片 + [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset + targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale) + contentMode:PHImageContentModeAspectFill options:phImageRequestOptions + resultHandler:^(UIImage *result, NSDictionary *info) { + resultImage = result; + }]; + + return resultImage; +} + +- (UIImage *)previewImage { + __block UIImage *resultImage = nil; + PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init]; + imageRequestOptions.networkAccessAllowed = YES; + imageRequestOptions.synchronous = YES; + [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset + targetSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT) + contentMode:PHImageContentModeAspectFill + options:imageRequestOptions + resultHandler:^(UIImage *result, NSDictionary *info) { + resultImage = result; + }]; + return resultImage; +} + +- (NSInteger)requestOriginImageWithCompletion:(void (^)(UIImage *result, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler { + PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init]; + imageRequestOptions.networkAccessAllowed = YES; // 允许访问网络 + imageRequestOptions.progressHandler = phProgressHandler; + return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageDataForAsset:_phAsset options:imageRequestOptions resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) { + if (completion) { + completion([UIImage imageWithData:imageData], info); + } + }]; +} + +- (NSInteger)requestThumbnailImageWithSize:(CGSize)size completion:(void (^)(UIImage *result, NSDictionary *info))completion { + PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init]; + imageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeFast; + imageRequestOptions.networkAccessAllowed = YES; + // 在 PHImageManager 中,targetSize 等 size 都是使用 px 作为单位,因此需要对targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片 + return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale) contentMode:PHImageContentModeAspectFill options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) { + if (completion) { + completion(result, info); + } + }]; +} + +- (NSInteger)requestPreviewImageWithCompletion:(void (^)(UIImage *result, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler { + PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init]; + imageRequestOptions.networkAccessAllowed = YES; // 允许访问网络 + imageRequestOptions.progressHandler = phProgressHandler; + return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeAspectFill options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) { + if (completion) { + completion(result, info); + } + }]; +} + +- (NSInteger)requestLivePhotoWithCompletion:(void (^)(PHLivePhoto *livePhoto, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler { + if ([[PHCachingImageManager class] instancesRespondToSelector:@selector(requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler:)]) { + PHLivePhotoRequestOptions *livePhotoRequestOptions = [[PHLivePhotoRequestOptions alloc] init]; + livePhotoRequestOptions.networkAccessAllowed = YES; // 允许访问网络 + livePhotoRequestOptions.progressHandler = phProgressHandler; + return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestLivePhotoForAsset:_phAsset targetSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT) contentMode:PHImageContentModeDefault options:livePhotoRequestOptions resultHandler:^(PHLivePhoto * _Nullable livePhoto, NSDictionary * _Nullable info) { + if (completion) { + completion(livePhoto, info); + } + }]; + } else { + if (completion) { + completion(nil, nil); + } + return 0; + } +} + +- (NSInteger)requestPlayerItemWithCompletion:(void (^)(AVPlayerItem *playerItem, NSDictionary *info))completion withProgressHandler:(PHAssetVideoProgressHandler)phProgressHandler { + if ([[PHCachingImageManager class] instancesRespondToSelector:@selector(requestPlayerItemForVideo:options:resultHandler:)]) { + PHVideoRequestOptions *videoRequestOptions = [[PHVideoRequestOptions alloc] init]; + videoRequestOptions.networkAccessAllowed = YES; // 允许访问网络 + videoRequestOptions.progressHandler = phProgressHandler; + return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestPlayerItemForVideo:_phAsset options:videoRequestOptions resultHandler:^(AVPlayerItem * _Nullable playerItem, NSDictionary * _Nullable info) { + if (completion) { + completion(playerItem, info); + } + }]; + } else { + if (completion) { + completion(nil, nil); + } + return 0; + } +} + +- (void)requestImageData:(void (^)(NSData *imageData, NSDictionary *info, BOOL isGIF, BOOL isHEIC))completion { + if (self.assetType != QMUIAssetTypeImage) { + if (completion) { + completion(nil, nil, NO, NO); + } + return; + } + __weak __typeof(self)weakSelf = self; + if (!self.phAssetInfo) { + // PHAsset 的 UIImageOrientation 需要调用过 requestImageDataForAsset 才能获取 + [self requestPhAssetInfo:^(NSDictionary *phAssetInfo) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf.phAssetInfo = phAssetInfo; + if (completion) { + NSString *dataUTI = phAssetInfo[kAssetInfoDataUTI]; + BOOL isGIF = self.assetSubType == QMUIAssetSubTypeGIF; + BOOL isHEIC = [dataUTI isEqualToString:@"public.heic"]; + NSDictionary *originInfo = phAssetInfo[kAssetInfoOriginInfo]; + completion(phAssetInfo[kAssetInfoImageData], originInfo, isGIF, isHEIC); + } + }]; + } else { + if (completion) { + NSString *dataUTI = self.phAssetInfo[kAssetInfoDataUTI]; + BOOL isGIF = self.assetSubType == QMUIAssetSubTypeGIF; + BOOL isHEIC = [@"public.heic" isEqualToString:dataUTI]; + NSDictionary *originInfo = self.phAssetInfo[kAssetInfoOriginInfo]; + completion(self.phAssetInfo[kAssetInfoImageData], originInfo, isGIF, isHEIC); + } + } +} + +- (UIImageOrientation)imageOrientation { + UIImageOrientation orientation; + if (self.assetType == QMUIAssetTypeImage) { + if (!self.phAssetInfo) { + // PHAsset 的 UIImageOrientation 需要调用过 requestImageDataForAsset 才能获取 + __weak __typeof(self)weakSelf = self; + [self requestImagePhAssetInfo:^(NSDictionary *phAssetInfo) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf.phAssetInfo = phAssetInfo; + } synchronous:YES]; + } + // 从 PhAssetInfo 中获取 UIImageOrientation 对应的字段 + orientation = (UIImageOrientation)[self.phAssetInfo[kAssetInfoOrientation] integerValue]; + } else { + orientation = UIImageOrientationUp; + } + return orientation; +} + +- (NSString *)identifier { + return _phAsset.localIdentifier; +} + +- (void)requestPhAssetInfo:(void (^)(NSDictionary *))completion { + if (!_phAsset) { + if (completion) { + completion(nil); + } + return; + } + if (self.assetType == QMUIAssetTypeVideo) { + PHVideoRequestOptions *videoRequestOptions = [[PHVideoRequestOptions alloc] init]; + videoRequestOptions.networkAccessAllowed = YES; + [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestAVAssetForVideo:_phAsset options:videoRequestOptions resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) { + if ([asset isKindOfClass:[AVURLAsset class]]) { + NSMutableDictionary *tempInfo = [[NSMutableDictionary alloc] init]; + if (info) { + [tempInfo setObject:info forKey:kAssetInfoOriginInfo]; + } + AVURLAsset *urlAsset = (AVURLAsset*)asset; + NSNumber *size; + [urlAsset.URL getResourceValue:&size forKey:NSURLFileSizeKey error:nil]; + [tempInfo setObject:size forKey:kAssetInfoSize]; + if (completion) { + completion(tempInfo); + } + } + }]; + } else { + [self requestImagePhAssetInfo:^(NSDictionary *phAssetInfo) { + if (completion) { + completion(phAssetInfo); + } + } synchronous:NO]; + } +} + +- (void)requestImagePhAssetInfo:(void (^)(NSDictionary *))completion synchronous:(BOOL)synchronous { + PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init]; + imageRequestOptions.synchronous = synchronous; + imageRequestOptions.networkAccessAllowed = YES; + [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageDataForAsset:_phAsset options:imageRequestOptions resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { + if (info) { + NSMutableDictionary *tempInfo = [[NSMutableDictionary alloc] init]; + if (imageData) { + [tempInfo setObject:imageData forKey:kAssetInfoImageData]; + [tempInfo setObject:@(imageData.length) forKey:kAssetInfoSize]; + } + [tempInfo setObject:info forKey:kAssetInfoOriginInfo]; + if (dataUTI) { + [tempInfo setObject:dataUTI forKey:kAssetInfoDataUTI]; + } + [tempInfo setObject:@(orientation) forKey:kAssetInfoOrientation]; + if (completion) { + completion(tempInfo); + } + } + }]; +} + +- (void)setDownloadProgress:(double)downloadProgress { + _downloadProgress = downloadProgress; + _downloadStatus = QMUIAssetDownloadStatusDownloading; +} + +- (void)updateDownloadStatusWithDownloadResult:(BOOL)succeed { + _downloadStatus = succeed ? QMUIAssetDownloadStatusSucceed : QMUIAssetDownloadStatusFailed; +} + +- (void)assetSize:(void (^)(long long size))completion { + if (!self.phAssetInfo) { + // PHAsset 的 UIImageOrientation 需要调用过 requestImageDataForAsset 才能获取 + __weak __typeof(self)weakSelf = self; + [self requestPhAssetInfo:^(NSDictionary *phAssetInfo) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + strongSelf.phAssetInfo = phAssetInfo; + if (completion) { + /** + * 这里不在主线程执行,若用户在该 block 中操作 UI 时会产生一些问题, + * 为了避免这种情况,这里该 block 主动放到主线程执行。 + */ + dispatch_async(dispatch_get_main_queue(), ^{ + completion([phAssetInfo[kAssetInfoSize] longLongValue]); + }); + } + }]; + } else { + if (completion) { + completion([self.phAssetInfo[kAssetInfoSize] longLongValue]); + } + } +} + +- (NSTimeInterval)duration { + if (self.assetType != QMUIAssetTypeVideo) { + return 0; + } + return _phAsset.duration; +} + +- (BOOL)isEqual:(id)object { + if (!object) return NO; + if (self == object) return YES; + if (![object isKindOfClass:[self class]]) return NO; + return [self.identifier isEqualToString:((QMUIAsset *)object).identifier]; +} + +@end diff --git a/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsGroup.h b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h similarity index 78% rename from QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsGroup.h rename to QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h index cde88f02..d4e236b6 100644 --- a/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsGroup.h +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h @@ -1,12 +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. + */ + // // QMUIAssetsGroup.h // qmui // -// Created by Kayo Lee on 15/6/30. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/6/30. // -#import +#import #import #import #import @@ -17,10 +24,10 @@ /// 相册展示内容的类型 typedef NS_ENUM(NSUInteger, QMUIAlbumContentType) { - QMUIAlbumContentTypeAll, // 展示所有资源(照片和视频) + QMUIAlbumContentTypeAll, // 展示所有资源 QMUIAlbumContentTypeOnlyPhoto, // 只展示照片 QMUIAlbumContentTypeOnlyVideo, // 只展示视频 - QMUIAlbumContentTypeOnlyAudio NS_ENUM_AVAILABLE_IOS(8_0) // 只展示音频 + QMUIAlbumContentTypeOnlyAudio // 只展示音频 }; /// 相册展示内容按日期排序的方式 @@ -32,15 +39,10 @@ typedef NS_ENUM(NSUInteger, QMUIAlbumSortType) { @interface QMUIAssetsGroup : NSObject -- (instancetype)initWithALAssetsGroup:(ALAssetsGroup *)alAssetsGroup; - - (instancetype)initWithPHCollection:(PHAssetCollection *)phAssetCollection; - (instancetype)initWithPHCollection:(PHAssetCollection *)phAssetCollection fetchAssetsOptions:(PHFetchOptions *)pHFetchOptions; -/// 仅能通过 initWithALAssetsGroup 方法修改 alAssetsGroup 的值 -@property(nonatomic, strong, readonly) ALAssetsGroup *alAssetsGroup; - /// 仅能通过 initWithPHCollection 和 initWithPHCollection:fetchAssetsOption 方法修改 phAssetCollection 的值 @property(nonatomic, strong, readonly) PHAssetCollection *phAssetCollection; @@ -56,8 +58,6 @@ typedef NS_ENUM(NSUInteger, QMUIAlbumSortType) { /** * 相册的缩略图,即系统接口中的相册海报(Poster Image) * - * @param size 缩略图的 size,仅在 iOS 8.0 及以上的版本有效,其他版本则调用 ALAsset 的接口由系统返回一个固定大小的缩略图 - * * @return 相册的缩略图 */ - (UIImage *)posterImageWithSize:(CGSize)size; diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m new file mode 100644 index 00000000..528916b4 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m @@ -0,0 +1,104 @@ +/** + * 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. + */ + +// +// QMUIAssetsGroup.m +// qmui +// +// Created by QMUI Team on 15/6/30. +// + +#import "QMUIAssetsGroup.h" +#import "QMUICore.h" +#import "QMUIAsset.h" +#import "QMUIAssetsManager.h" + +@interface QMUIAssetsGroup() + +@property(nonatomic, strong, readwrite) PHAssetCollection *phAssetCollection; +@property(nonatomic, strong, readwrite) PHFetchResult *phFetchResult; + +@end + +@implementation QMUIAssetsGroup + +- (instancetype)initWithPHCollection:(PHAssetCollection *)phAssetCollection fetchAssetsOptions:(PHFetchOptions *)pHFetchOptions { + self = [super init]; + if (self) { + self.phFetchResult = [PHAsset fetchAssetsInAssetCollection:phAssetCollection options:pHFetchOptions]; + self.phAssetCollection = phAssetCollection; + } + return self; +} + +- (instancetype)initWithPHCollection:(PHAssetCollection *)phAssetCollection { + return [self initWithPHCollection:phAssetCollection fetchAssetsOptions:nil]; +} + +- (NSInteger)numberOfAssets { + return self.phFetchResult.count; +} + +- (NSString *)name { + NSString *resultName = self.phAssetCollection.localizedTitle; + return NSLocalizedString(resultName, resultName); +} + +- (UIImage *)posterImageWithSize:(CGSize)size { + // 系统的隐藏相册不应该显示缩略图 + if (self.phAssetCollection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumAllHidden) { + return [QMUIHelper imageWithName:@"QMUI_hiddenAlbum"]; + } + + __block UIImage *resultImage; + NSInteger count = self.phFetchResult.count; + if (count > 0) { + PHAsset *asset = self.phFetchResult[count - 1]; + PHImageRequestOptions *pHImageRequestOptions = [[PHImageRequestOptions alloc] init]; + pHImageRequestOptions.synchronous = YES; // 同步请求 + pHImageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact; + // targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片 + [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:asset targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale) contentMode:PHImageContentModeAspectFill options:pHImageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) { + resultImage = result; + }]; + } + return resultImage; +} + +- (void)enumerateAssetsWithOptions:(QMUIAlbumSortType)albumSortType usingBlock:(void (^)(QMUIAsset *resultAsset))enumerationBlock { + NSInteger resultCount = self.phFetchResult.count; + if (albumSortType == QMUIAlbumSortTypeReverse) { + for (NSInteger i = resultCount - 1; i >= 0; i--) { + PHAsset *pHAsset = self.phFetchResult[i]; + QMUIAsset *asset = [[QMUIAsset alloc] initWithPHAsset:pHAsset]; + if (enumerationBlock) { + enumerationBlock(asset); + } + } + } else { + for (NSInteger i = 0; i < resultCount; i++) { + PHAsset *pHAsset = self.phFetchResult[i]; + QMUIAsset *asset = [[QMUIAsset alloc] initWithPHAsset:pHAsset]; + if (enumerationBlock) { + enumerationBlock(asset); + } + } + } + /** + * For 循环遍历完毕,这时再调用一次 enumerationBlock,并传递 nil 作为实参,作为枚举资源结束的标记。 + */ + if (enumerationBlock) { + enumerationBlock(nil); + } +} + +- (void)enumerateAssetsUsingBlock:(void (^)(QMUIAsset *resultAsset))enumerationBlock { + [self enumerateAssetsWithOptions:QMUIAlbumSortTypePositive usingBlock:enumerationBlock]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h new file mode 100644 index 00000000..178da093 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h @@ -0,0 +1,143 @@ +/** + * 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. + */ + +// +// QMUIAssetsManager.h +// qmui +// +// Created by QMUI Team on 15/6/9. +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import "QMUIAssetsGroup.h" + +@class PHCachingImageManager; +@class QMUIAsset; + +/// Asset 授权的状态 +typedef NS_ENUM(NSUInteger, QMUIAssetAuthorizationStatus) { + QMUIAssetAuthorizationStatusNotDetermined, // 还不确定有没有授权 + QMUIAssetAuthorizationStatusAuthorized, // 已经授权 + QMUIAssetAuthorizationStatusNotAuthorized // 手动禁止了授权 +}; + +typedef void (^QMUIWriteAssetCompletionBlock)(QMUIAsset *asset, NSError *error); + + +/// 保存图片到指定相册(传入 UIImage) +extern void QMUIImageWriteToSavedPhotosAlbumWithAlbumAssetsGroup(UIImage *image, QMUIAssetsGroup *albumAssetsGroup, QMUIWriteAssetCompletionBlock completionBlock); + +/// 保存图片到指定相册(传入图片路径) +extern void QMUISaveImageAtPathToSavedPhotosAlbumWithAlbumAssetsGroup(NSString *imagePath, QMUIAssetsGroup *albumAssetsGroup, QMUIWriteAssetCompletionBlock completionBlock); + +/// 保存视频到指定相册 +extern void QMUISaveVideoAtPathToSavedPhotosAlbumWithAlbumAssetsGroup(NSString *videoPath, QMUIAssetsGroup *albumAssetsGroup, QMUIWriteAssetCompletionBlock completionBlock); + +/** + * 构建 QMUIAssetsManager 这个对象并提供单例的调用方式主要出于下面两点考虑: + * 1. 保存照片/视频的方法较为复杂,为了方便封装系统接口,同时灵活地扩展功能,需要有一个独立对象去管理这些方法。 + * 2. 使用 PhotoKit 获取图片,基本都需要一个 PHCachingImageManager 的实例,为了减少消耗, + * QMUIAssetsManager 单例内部也构建了一个 PHCachingImageManager,并且暴露给外面,方便获取 + * PHCachingImageManager 的实例。 + */ +@interface QMUIAssetsManager : NSObject + +/// 获取 QMUIAssetsManager 的单例 ++ (instancetype)sharedInstance; + +/// 获取当前应用的“照片”访问授权状态 ++ (QMUIAssetAuthorizationStatus)authorizationStatus; + +/** + * 调起系统询问是否授权访问“照片”的 UIAlertView + * @param handler 授权结束后调用的 block,默认不在主线程上执行,如果需要在 block 中修改 UI,记得 dispatch 到 mainqueue + */ ++ (void)requestAuthorization:(void(^)(QMUIAssetAuthorizationStatus status))handler; + +/** + * 获取所有的相册,包括个人收藏,最近添加,自拍这类“智能相册” + * + * @param contentType 相册的内容类型,设定了内容类型后,所获取的相册中只包含对应类型的资源 + * @param showEmptyAlbum 是否显示空相册(经过 contentType 过滤后仍为空的相册) + * @param showSmartAlbumIfSupported 是否显示"智能相册" + * @param enumerationBlock 参数 resultAssetsGroup 表示每次枚举时对应的相册。枚举所有相册结束后,enumerationBlock 会被再调用一次, + * 这时 resultAssetsGroup 的值为 nil。可以以此作为判断枚举结束的标记。 + */ +- (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbumIfSupported:(BOOL)showSmartAlbumIfSupported usingBlock:(void (^)(QMUIAssetsGroup *resultAssetsGroup))enumerationBlock; + +/// 获取所有相册,默认显示系统的“智能相册”,不显示空相册(经过 contentType 过滤后为空的相册) +- (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType usingBlock:(void (^)(QMUIAssetsGroup *resultAssetsGroup))enumerationBlock; + +/** + * 保存图片或视频到指定的相册 + * + * @warning 无论用户保存到哪个自行创建的相册,系统都会在“相机胶卷”相册中同时保存这个图片。 + * 因为系统没有把图片和视频直接保存到指定相册的接口,都只能先保存到“相机胶卷”,从而生成了 Asset 对象, + * 再把 Asset 对象添加到指定相册中,从而达到保存资源到指定相册的效果。 + * 即使调用 PhotoKit 保存图片或视频到指定相册的新接口也是如此,并且官方 PhotoKit SampleCode 中例子也是表现如此, + * 因此这应该是一个合符官方预期的表现。 + * @warning 无法通过该方法把图片保存到“智能相册”,“智能相册”只能由系统控制资源的增删。 + */ +- (void)saveImageWithImageRef:(CGImageRef)imageRef albumAssetsGroup:(QMUIAssetsGroup *)albumAssetsGroup orientation:(UIImageOrientation)orientation completionBlock:(QMUIWriteAssetCompletionBlock)completionBlock; + +- (void)saveImageWithImagePathURL:(NSURL *)imagePathURL albumAssetsGroup:(QMUIAssetsGroup *)albumAssetsGroup completionBlock:(QMUIWriteAssetCompletionBlock)completionBlock; + +- (void)saveVideoWithVideoPathURL:(NSURL *)videoPathURL albumAssetsGroup:(QMUIAssetsGroup *)albumAssetsGroup completionBlock:(QMUIWriteAssetCompletionBlock)completionBlock; + +/// 获取一个 PHCachingImageManager 的实例 +- (PHCachingImageManager *)phCachingImageManager; + +@end + + +@interface PHPhotoLibrary (QMUI) + +/** + * 根据 contentType 的值产生一个合适的 PHFetchOptions,并把内容以资源创建日期排序,创建日期较新的资源排在前面 + * + * @param contentType 相册的内容类型 + * + * @return 返回一个合适的 PHFetchOptions + */ ++ (PHFetchOptions *)createFetchOptionsWithAlbumContentType:(QMUIAlbumContentType)contentType; + +/** + * 获取所有相册 + * + * @param contentType 相册的内容类型,设定了内容类型后,所获取的相册中只包含对应类型的资源 + * @param showEmptyAlbum 是否显示空相册(经过 contentType 过滤后仍为空的相册) + * @param showSmartAlbum 是否显示“智能相册” + * + * @return 返回包含所有合适相册的数组 + */ ++ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum; + +/// 获取一个 PHAssetCollection 中创建日期最新的资源 ++ (PHAsset *)fetchLatestAssetWithAssetCollection:(PHAssetCollection *)assetCollection; + +/** + * 保存图片或视频到指定的相册 + * + * @warning 无论用户保存到哪个自行创建的相册,系统都会在“相机胶卷”相册中同时保存这个图片。 + * 原因请参考 QMUIAssetsManager 对象的保存图片和视频方法的注释。 + * @warning 无法通过该方法把图片保存到“智能相册”,“智能相册”只能由系统控制资源的增删。 + */ +- (void)addImageToAlbum:(CGImageRef)imageRef albumAssetCollection:(PHAssetCollection *)albumAssetCollection orientation:(UIImageOrientation)orientation completionHandler:(void(^)(BOOL success, NSDate *creationDate, NSError *error))completionHandler; + +- (void)addImageToAlbum:(NSURL *)imagePathURL albumAssetCollection:(PHAssetCollection *)albumAssetCollection completionHandler:(void(^)(BOOL success, NSDate *creationDate, NSError *error))completionHandler; + +- (void)addVideoToAlbum:(NSURL *)videoPathURL albumAssetCollection:(PHAssetCollection *)albumAssetCollection completionHandler:(void(^)(BOOL success, NSDate *creationDate, NSError *error))completionHandler; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m new file mode 100644 index 00000000..ff0cf089 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m @@ -0,0 +1,395 @@ +/** + * 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. + */ + +// +// QMUIAssetsManager.m +// qmui +// +// Created by QMUI Team on 15/6/9. +// + +#import "QMUIAssetsManager.h" +#import "QMUICore.h" +#import "QMUIAsset.h" +#import "QMUILog.h" + +void QMUIImageWriteToSavedPhotosAlbumWithAlbumAssetsGroup(UIImage *image, QMUIAssetsGroup *albumAssetsGroup, QMUIWriteAssetCompletionBlock completionBlock) { + [[QMUIAssetsManager sharedInstance] saveImageWithImageRef:image.CGImage albumAssetsGroup:albumAssetsGroup orientation:image.imageOrientation completionBlock:completionBlock]; +} + +void QMUISaveImageAtPathToSavedPhotosAlbumWithAlbumAssetsGroup(NSString *imagePath, QMUIAssetsGroup *albumAssetsGroup, QMUIWriteAssetCompletionBlock completionBlock) { + [[QMUIAssetsManager sharedInstance] saveImageWithImagePathURL:[NSURL fileURLWithPath:imagePath] albumAssetsGroup:albumAssetsGroup completionBlock:completionBlock]; +} + +void QMUISaveVideoAtPathToSavedPhotosAlbumWithAlbumAssetsGroup(NSString *videoPath, QMUIAssetsGroup *albumAssetsGroup, QMUIWriteAssetCompletionBlock completionBlock) { + [[QMUIAssetsManager sharedInstance] saveVideoWithVideoPathURL:[NSURL fileURLWithPath:videoPath] albumAssetsGroup:albumAssetsGroup completionBlock:completionBlock]; +} + + + +@implementation QMUIAssetsManager { + PHCachingImageManager *_phCachingImageManager; +} + ++ (QMUIAssetsManager *)sharedInstance { + static dispatch_once_t onceToken; + static QMUIAssetsManager *instance = nil; + dispatch_once(&onceToken,^{ + instance = [[super allocWithZone:NULL] init]; + }); + return instance; +} + +/** + * 重写 +allocWithZone 方法,使得在给对象分配内存空间的时候,就指向同一份数据 + */ + ++ (id)allocWithZone:(struct _NSZone *)zone { + return [self sharedInstance]; +} + +- (instancetype)init { + if (self = [super init]) { + } + return self; +} + ++ (QMUIAssetAuthorizationStatus)authorizationStatus { + __block QMUIAssetAuthorizationStatus status; + // 获取当前应用对照片的访问授权状态 + PHAuthorizationStatus authorizationStatus = [PHPhotoLibrary authorizationStatus]; + if (authorizationStatus == PHAuthorizationStatusRestricted || authorizationStatus == PHAuthorizationStatusDenied) { + status = QMUIAssetAuthorizationStatusNotAuthorized; + } else if (authorizationStatus == PHAuthorizationStatusNotDetermined) { + status = QMUIAssetAuthorizationStatusNotDetermined; + } else { + status = QMUIAssetAuthorizationStatusAuthorized; + } + return status; +} + ++ (void)requestAuthorization:(void(^)(QMUIAssetAuthorizationStatus status))handler { + [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus phStatus) { + QMUIAssetAuthorizationStatus status; + if (phStatus == PHAuthorizationStatusRestricted || phStatus == PHAuthorizationStatusDenied) { + status = QMUIAssetAuthorizationStatusNotAuthorized; + } else if (phStatus == PHAuthorizationStatusNotDetermined) { + status = QMUIAssetAuthorizationStatusNotDetermined; + } else { + status = QMUIAssetAuthorizationStatusAuthorized; + } + if (handler) { + handler(status); + } + }]; +} + +- (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbumIfSupported:(BOOL)showSmartAlbumIfSupported usingBlock:(void (^)(QMUIAssetsGroup *resultAssetsGroup))enumerationBlock { + // 根据条件获取所有合适的相册,并保存到临时数组中 + 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[i]; + QMUIAssetsGroup *assetsGroup = [[QMUIAssetsGroup alloc] initWithPHCollection:phAssetCollection fetchAssetsOptions:phFetchOptions]; + if (enumerationBlock) { + enumerationBlock(assetsGroup); + } + } + + /** + * 所有结果遍历完毕,这时再调用一次 enumerationBlock,并传递 nil 作为实参,作为枚举相册结束的标记。 + */ + if (enumerationBlock) { + enumerationBlock(nil); + } +} + +- (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType usingBlock:(void (^)(QMUIAssetsGroup *resultAssetsGroup))enumerationBlock { + [self enumerateAllAlbumsWithAlbumContentType:contentType showEmptyAlbum:NO showSmartAlbumIfSupported:YES usingBlock:enumerationBlock]; +} + +- (void)saveImageWithImageRef:(CGImageRef)imageRef albumAssetsGroup:(QMUIAssetsGroup *)albumAssetsGroup orientation:(UIImageOrientation)orientation completionBlock:(QMUIWriteAssetCompletionBlock)completionBlock { + PHAssetCollection *albumPhAssetCollection = albumAssetsGroup.phAssetCollection; + // 把图片加入到指定的相册对应的 PHAssetCollection + [[PHPhotoLibrary sharedPhotoLibrary] addImageToAlbum:imageRef + albumAssetCollection:albumPhAssetCollection + orientation:orientation + completionHandler:^(BOOL success, NSDate *creationDate, NSError *error) { + if (success) { + PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; + fetchOptions.predicate = [NSPredicate predicateWithFormat:@"creationDate = %@", creationDate]; + PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:albumPhAssetCollection options:fetchOptions]; + PHAsset *phAsset = fetchResult.lastObject; + QMUIAsset *asset = [[QMUIAsset alloc] initWithPHAsset:phAsset]; + completionBlock(asset, error); + } else { + QMUILog(@"QMUIAssetLibrary", @"Get PHAsset of image error: %@", error); + completionBlock(nil, error); + } + }]; +} + +- (void)saveImageWithImagePathURL:(NSURL *)imagePathURL albumAssetsGroup:(QMUIAssetsGroup *)albumAssetsGroup completionBlock:(QMUIWriteAssetCompletionBlock)completionBlock { + PHAssetCollection *albumPhAssetCollection = albumAssetsGroup.phAssetCollection; + // 把图片加入到指定的相册对应的 PHAssetCollection + [[PHPhotoLibrary sharedPhotoLibrary] addImageToAlbum:imagePathURL + albumAssetCollection:albumPhAssetCollection + completionHandler:^(BOOL success, NSDate *creationDate, NSError *error) { + if (success) { + PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; + fetchOptions.predicate = [NSPredicate predicateWithFormat:@"creationDate = %@", creationDate]; + PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:albumPhAssetCollection options:fetchOptions]; + PHAsset *phAsset = fetchResult.lastObject; + QMUIAsset *asset = [[QMUIAsset alloc] initWithPHAsset:phAsset]; + completionBlock(asset, error); + } else { + QMUILog(@"QMUIAssetLibrary", @"Get PHAsset of image error: %@", error); + completionBlock(nil, error); + } + }]; +} + +- (void)saveVideoWithVideoPathURL:(NSURL *)videoPathURL albumAssetsGroup:(QMUIAssetsGroup *)albumAssetsGroup completionBlock:(QMUIWriteAssetCompletionBlock)completionBlock { + PHAssetCollection *albumPhAssetCollection = albumAssetsGroup.phAssetCollection; + // 把视频加入到指定的相册对应的 PHAssetCollection + [[PHPhotoLibrary sharedPhotoLibrary] addVideoToAlbum:videoPathURL + albumAssetCollection:albumPhAssetCollection + completionHandler:^(BOOL success, NSDate *creationDate, NSError *error) { + if (success) { + PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; + fetchOptions.predicate = [NSPredicate predicateWithFormat:@"creationDate = %@", creationDate]; + PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:albumPhAssetCollection options:fetchOptions]; + PHAsset *phAsset = fetchResult.lastObject; + QMUIAsset *asset = [[QMUIAsset alloc] initWithPHAsset:phAsset]; + completionBlock(asset, error); + } else { + QMUILog(@"QMUIAssetLibrary", @"Get PHAsset of video Error: %@", error); + completionBlock(nil, error); + } + }]; +} + +- (PHCachingImageManager *)phCachingImageManager { + if (!_phCachingImageManager) { + _phCachingImageManager = [[PHCachingImageManager alloc] init]; + } + return _phCachingImageManager; +} + +@end + + +@implementation PHPhotoLibrary (QMUI) + ++ (PHFetchOptions *)createFetchOptionsWithAlbumContentType:(QMUIAlbumContentType)contentType { + PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; + // 根据输入的内容类型过滤相册内的资源 + switch (contentType) { + case QMUIAlbumContentTypeOnlyPhoto: + fetchOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType = %i", PHAssetMediaTypeImage]; + break; + + case QMUIAlbumContentTypeOnlyVideo: + fetchOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType = %i",PHAssetMediaTypeVideo]; + break; + + case QMUIAlbumContentTypeOnlyAudio: + fetchOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType = %i",PHAssetMediaTypeAudio]; + break; + + default: + break; + } + return fetchOptions; +} + ++ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum { + NSMutableArray *tempAlbumsArray = [[NSMutableArray alloc] init]; + + // 创建一个 PHFetchOptions,用于创建 QMUIAssetsGroup 对资源的排序和类型进行控制 + PHFetchOptions *fetchOptions = [PHPhotoLibrary createFetchOptionsWithAlbumContentType:contentType]; + + PHFetchResult *fetchResult; + if (showSmartAlbum) { + // 允许显示系统的“智能相册” + // 获取保存了所有“智能相册”的 PHFetchResult + fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAny options:nil]; + } else { + // 不允许显示系统的智能相册,但由于在 PhotoKit 中,“相机胶卷”也属于“智能相册”,因此这里从“智能相册”中单独获取到“相机胶卷” + fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeSmartAlbumUserLibrary options:nil]; + } + // 循环遍历相册列表 + for (NSInteger i = 0; i < fetchResult.count; i++) { + // 获取一个相册 + PHCollection *collection = fetchResult[i]; + if ([collection isKindOfClass:[PHAssetCollection class]]) { + PHAssetCollection *assetCollection = (PHAssetCollection *)collection; + // 获取相册内的资源对应的 fetchResult,用于判断根据内容类型过滤后的资源数量是否大于 0,只有资源数量大于 0 的相册才会作为有效的相册显示 + PHFetchResult *currentFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions]; + if (currentFetchResult.count > 0 || showEmptyAlbum) { + // 若相册不为空,或者允许显示空相册,则保存相册到结果数组 + // 判断如果是“相机胶卷”,则放到结果列表的第一位 + if (assetCollection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumUserLibrary) { + [tempAlbumsArray insertObject:assetCollection atIndex:0]; + } else { + [tempAlbumsArray addObject:assetCollection]; + } + } + } else { + NSAssert(NO, @"Fetch collection not PHCollection: %@", collection); + } + } + + // 获取所有用户自己建立的相册 + PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil]; + // 循环遍历用户自己建立的相册 + for (NSInteger i = 0; i < topLevelUserCollections.count; i++) { + // 获取一个相册 + PHCollection *collection = topLevelUserCollections[i]; + if ([collection isKindOfClass:[PHAssetCollection class]]) { + PHAssetCollection *assetCollection = (PHAssetCollection *)collection; + + if (showEmptyAlbum) { + // 允许显示空相册,直接保存相册到结果数组中 + [tempAlbumsArray addObject:assetCollection]; + } else { + // 不允许显示空相册,需要判断当前相册是否为空 + PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions]; + // 获取相册内的资源对应的 fetchResult,用于判断根据内容类型过滤后的资源数量是否大于 0 + if (fetchResult.count > 0) { + [tempAlbumsArray addObject:assetCollection]; + } + } + } + } + + // 获取从 macOS 设备同步过来的相册,同步过来的相册不允许删除照片,因此不会为空 + PHFetchResult *macCollections = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumSyncedAlbum options:nil]; + // 循环从 macOS 设备同步过来的相册 + for (NSInteger i = 0; i < macCollections.count; i++) { + // 获取一个相册 + PHCollection *collection = macCollections[i]; + if ([collection isKindOfClass:[PHAssetCollection class]]) { + PHAssetCollection *assetCollection = (PHAssetCollection *)collection; + [tempAlbumsArray addObject:assetCollection]; + } + } + + NSArray *resultAlbumsArray = [tempAlbumsArray copy]; + return resultAlbumsArray; +} + ++ (PHAsset *)fetchLatestAssetWithAssetCollection:(PHAssetCollection *)assetCollection { + PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; + // 按时间的先后对 PHAssetCollection 内的资源进行排序,最新的资源排在数组最后面 + fetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]]; + PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions]; + // 获取 PHAssetCollection 内最后一个资源,即最新的资源 + PHAsset *latestAsset = fetchResult.lastObject; + return latestAsset; +} + +- (void)addImageToAlbum:(CGImageRef)imageRef albumAssetCollection:(PHAssetCollection *)albumAssetCollection orientation:(UIImageOrientation)orientation completionHandler:(void(^)(BOOL success, NSDate *creationDate, NSError *error))completionHandler { + UIImage *targetImage = [UIImage imageWithCGImage:imageRef scale:ScreenScale orientation:orientation]; + [[PHPhotoLibrary sharedPhotoLibrary] addImageToAlbum:targetImage imagePathURL:nil albumAssetCollection:albumAssetCollection completionHandler:completionHandler]; +} + +- (void)addImageToAlbum:(NSURL *)imagePathURL albumAssetCollection:(PHAssetCollection *)albumAssetCollection completionHandler:(void (^)(BOOL success, NSDate *creationDate, NSError *error))completionHandler { + [[PHPhotoLibrary sharedPhotoLibrary] addImageToAlbum:nil imagePathURL:imagePathURL albumAssetCollection:albumAssetCollection completionHandler:completionHandler]; +} + +- (void)addImageToAlbum:(UIImage *)image imagePathURL:(NSURL *)imagePathURL albumAssetCollection:(PHAssetCollection *)albumAssetCollection completionHandler:(void(^)(BOOL success, NSDate *creationDate, NSError *error))completionHandler { + __block NSDate *creationDate = nil; + [self performChanges:^{ + // 创建一个以图片生成新的 PHAsset,这时图片已经被添加到“相机胶卷” + + PHAssetChangeRequest *assetChangeRequest; + if (image) { + assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:image]; + } else if (imagePathURL) { + assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:imagePathURL]; + } else { + QMUILog(@"QMUIAssetLibrary", @"Creating asset with empty data"); + return; + } + assetChangeRequest.creationDate = [NSDate date]; + creationDate = assetChangeRequest.creationDate; + + if (albumAssetCollection.assetCollectionType == PHAssetCollectionTypeAlbum) { + // 如果传入的相册类型为标准的相册(非“智能相册”和“时刻”),则把刚刚创建的 Asset 添加到传入的相册中。 + + // 创建一个改变 PHAssetCollection 的请求,并指定相册对应的 PHAssetCollection + PHAssetCollectionChangeRequest *assetCollectionChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:albumAssetCollection]; + /** + * 把 PHAsset 加入到对应的 PHAssetCollection 中,系统推荐的方法是调用 placeholderForCreatedAsset , + * 返回一个的 placeholder 来代替刚创建的 PHAsset 的引用,并把该引用加入到一个 PHAssetCollectionChangeRequest 中。 + */ + [assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]]; + } + + } completionHandler:^(BOOL success, NSError *error) { + if (!success) { + QMUILog(@"QMUIAssetLibrary", @"Creating asset of image error : %@", error); + } + + if (completionHandler) { + /** + * performChanges:completionHandler 不在主线程执行,若用户在该 block 中操作 UI 时会产生一些问题, + * 为了避免这种情况,这里该 block 主动放到主线程执行。 + */ + dispatch_async(dispatch_get_main_queue(), ^{ + BOOL creatingSuccess = success && creationDate; // 若创建时间为 nil,则说明 performChanges 中传入的资源为空,因此需要同时判断 performChanges 是否执行成功以及资源是否有创建时间。 + completionHandler(creatingSuccess, creationDate, error); + }); + } + }]; +} + + +- (void)addVideoToAlbum:(NSURL *)videoPathURL albumAssetCollection:(PHAssetCollection *)albumAssetCollection completionHandler:(void(^)(BOOL success, NSDate *creationDate, NSError *error))completionHandler { + __block NSDate *creationDate = nil; + [self performChanges:^{ + // 创建一个以视频生成新的 PHAsset 的请求 + PHAssetChangeRequest *assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:videoPathURL]; + assetChangeRequest.creationDate = [NSDate date]; + creationDate = assetChangeRequest.creationDate; + + if (albumAssetCollection.assetCollectionType == PHAssetCollectionTypeAlbum) { + // 如果传入的相册类型为标准的相册(非“智能相册”和“时刻”),则把刚刚创建的 Asset 添加到传入的相册中。 + + // 创建一个改变 PHAssetCollection 的请求,并指定相册对应的 PHAssetCollection + PHAssetCollectionChangeRequest *assetCollectionChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:albumAssetCollection]; + /** + * 把 PHAsset 加入到对应的 PHAssetCollection 中,系统推荐的方法是调用 placeholderForCreatedAsset , + * 返回一个的 placeholder 来代替刚创建的 PHAsset 的引用,并把该引用加入到一个 PHAssetCollectionChangeRequest 中。 + */ + [assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]]; + } + + } completionHandler:^(BOOL success, NSError *error) { + if (!success) { + QMUILog(@"QMUIAssetLibrary", @"Creating asset of video error: %@", error); + } + + if (completionHandler) { + /** + * performChanges:completionHandler 不在主线程执行,若用户在该 block 中操作 UI 时会产生一些问题, + * 为了避免这种情况,这里该 block 主动放到主线程执行。 + */ + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(success, creationDate, error); + }); + } + }]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.h b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.h new file mode 100644 index 00000000..ec7f73fe --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.h @@ -0,0 +1,24 @@ +/** + * 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. + */ + +// +// CAAnimation+QMUI.h +// QMUIKit +// +// Created by QMUI Team on 2018/7/31. +// + +#import + +// 这个文件依赖了 QMUIMultipleDelegates,无法作为 UIKitExtensions 的一部分,所以放在 QMUIComponents 内 + +@interface CAAnimation (QMUI) + +@property(nonatomic, copy) void (^qmui_animationDidStartBlock)(__kindof CAAnimation *aAnimation); +@property(nonatomic, copy) void (^qmui_animationDidStopBlock)(__kindof CAAnimation *aAnimation, BOOL finished); +@end diff --git a/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.m b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.m new file mode 100644 index 00000000..9a1e5831 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/CAAnimation+QMUI.m @@ -0,0 +1,97 @@ +/** + * 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. + */ + +// +// CAAnimation+QMUI.m +// QMUIKit +// +// Created by QMUI Team on 2018/7/31. +// + +#import "CAAnimation+QMUI.h" +#import "QMUICore.h" +#import "QMUIMultipleDelegates.h" + +@interface _QMUICAAnimationDelegator : NSObject + +@end + +@implementation CAAnimation (QMUI) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ExtendImplementationOfNonVoidMethodWithSingleArgument([CAAnimation class], @selector(copyWithZone:), NSZone *, id, ^id(CAAnimation *selfObject, NSZone *firstArgv, id originReturnValue) { + CAAnimation *animation = (CAAnimation *)originReturnValue; + animation.qmui_multipleDelegatesEnabled = selfObject.qmui_multipleDelegatesEnabled; + animation.qmui_animationDidStartBlock = selfObject.qmui_animationDidStartBlock; + animation.qmui_animationDidStopBlock = selfObject.qmui_animationDidStopBlock; + return animation; + }); + }); +} + +- (void)enabledDelegateBlocks { + self.qmui_multipleDelegatesEnabled = YES; + BOOL shouldSetDelegator = !self.delegate; + if (!shouldSetDelegator && [self.delegate isKindOfClass:[QMUIMultipleDelegates class]]) { + QMUIMultipleDelegates *delegates = (QMUIMultipleDelegates *)self.delegate; + NSPointerArray *array = delegates.delegates; + for (NSUInteger i = 0; i < array.count; i++) { + if ([((NSObject *)[array pointerAtIndex:i]) isKindOfClass:[_QMUICAAnimationDelegator class]]) { + shouldSetDelegator = NO; + break; + } + } + } + if (shouldSetDelegator) { + self.delegate = [[_QMUICAAnimationDelegator alloc] init];// delegate is a strong property, it can retain _QMUICAAnimationDelegator + } +} + +static char kAssociatedObjectKey_animationDidStartBlock; +- (void)setQmui_animationDidStartBlock:(void (^)(__kindof CAAnimation *))qmui_animationDidStartBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_animationDidStartBlock, qmui_animationDidStartBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_animationDidStartBlock) { + [self enabledDelegateBlocks]; + } +} + +- (void (^)(__kindof CAAnimation *))qmui_animationDidStartBlock { + return (void (^)(__kindof CAAnimation *))objc_getAssociatedObject(self, &kAssociatedObjectKey_animationDidStartBlock); +} + +static char kAssociatedObjectKey_animationDidStopBlock; +- (void)setQmui_animationDidStopBlock:(void (^)(__kindof CAAnimation *, BOOL))qmui_animationDidStopBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_animationDidStopBlock, qmui_animationDidStopBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_animationDidStopBlock) { + [self enabledDelegateBlocks]; + } +} + +- (void (^)(__kindof CAAnimation *, BOOL))qmui_animationDidStopBlock { + return (void (^)(__kindof CAAnimation *, BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_animationDidStopBlock); +} + +@end + +@implementation _QMUICAAnimationDelegator + +- (void)animationDidStart:(CAAnimation *)anim { + if (anim.qmui_animationDidStartBlock) { + anim.qmui_animationDidStartBlock(anim); + } +} + +- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { + if (anim.qmui_animationDidStopBlock) { + anim.qmui_animationDidStopBlock(anim, flag); + } +} + +@end 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 new file mode 100644 index 00000000..aef27cd3 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.h @@ -0,0 +1,107 @@ +/** + * 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. + */ + +// +// QMUIAlbumViewController.h +// qmui +// +// Created by QMUI Team on 15/5/3. +// + +#import +#import "QMUICommonTableViewController.h" +#import "QMUITableViewCell.h" +#import "QMUIAssetsGroup.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIImagePickerViewController; +@class QMUIAlbumViewController; +@class QMUITableViewCell; + +@protocol QMUIAlbumViewControllerDelegate + +@required +/// 点击相簿里某一行时,需要给一个 QMUIImagePickerViewController 对象用于展示九宫格图片列表 +- (QMUIImagePickerViewController *)imagePickerViewControllerForAlbumViewController:(QMUIAlbumViewController *)albumViewController; + +@optional +/** + * 取消查看相册列表后被调用 + */ +- (void)albumViewControllerDidCancel:(QMUIAlbumViewController *)albumViewController; + +/** + * 即将需要显示 Loading 时调用 + * + * @see shouldShowDefaultLoadingView + */ +- (void)albumViewControllerWillStartLoading:(QMUIAlbumViewController *)albumViewController; + +/** + * 即将需要隐藏 Loading 时调用 + * + * @see shouldShowDefaultLoadingView + */ +- (void)albumViewControllerWillFinishLoading:(QMUIAlbumViewController *)albumViewController; + +@end + + +@interface QMUIAlbumTableViewCell : QMUITableViewCell + +@property(nonatomic, assign) CGFloat albumImageSize UI_APPEARANCE_SELECTOR; // 相册缩略图的大小 +@property(nonatomic, assign) CGFloat albumImageMarginLeft UI_APPEARANCE_SELECTOR; // 相册缩略图的 left,-1 表示自动保持与上下 margin 相等 +@property(nonatomic, assign) UIEdgeInsets albumNameInsets UI_APPEARANCE_SELECTOR; // 相册名称的上下左右间距 +@property(nullable, nonatomic, strong) UIFont *albumNameFont UI_APPEARANCE_SELECTOR; // 相册名的字体 +@property(nullable, nonatomic, strong) UIColor *albumNameColor UI_APPEARANCE_SELECTOR; // 相册名的颜色 +@property(nullable, nonatomic, strong) UIFont *albumAssetsNumberFont UI_APPEARANCE_SELECTOR; // 相册资源数量的字体 +@property(nullable, nonatomic, strong) UIColor *albumAssetsNumberColor UI_APPEARANCE_SELECTOR; // 相册资源数量的颜色 + +@end + +/** + * 当前设备照片里的相簿列表,使用方式: + * 1. 使用 init 初始化。 + * 2. 指定一个 albumViewControllerDelegate,并实现 @required 方法。 + * + * @warning 注意,iOS 访问相册需要得到授权,建议先询问用户授权,通过了再进行 QMUIAlbumViewController 的初始化工作。关于授权的代码,可参考 QMUI Demo 项目里的 [QDImagePickerExampleViewController authorizationPresentAlbumViewControllerWithTitle] 方法。 + * @see [QMUIAssetsManager requestAuthorization:] + */ +@interface QMUIAlbumViewController : QMUICommonTableViewController + +@property(nullable, nonatomic, weak) id albumViewControllerDelegate; + +/// 相册列表 cell 的高度,同时也是相册预览图的宽高,默认57 +@property(nonatomic, assign) CGFloat albumTableViewCellHeight UI_APPEARANCE_SELECTOR; + +/// 相册展示内容的类型,可以控制只展示照片、视频或音频的其中一种,也可以同时展示所有类型的资源,默认展示所有类型的资源。 +@property(nonatomic, assign) QMUIAlbumContentType contentType; + +@property(nullable, nonatomic, copy) NSString *tipTextWhenNoPhotosAuthorization; +@property(nullable, nonatomic, copy) NSString *tipTextWhenPhotosEmpty; + +/** + * 加载相册列表时会出现 loading,若需要自定义 loading 的形式,可将该属性置为 NO,默认为 YES。 + * @see albumViewControllerWillStartLoading: & albumViewControllerWillFinishLoading: + */ +@property(nonatomic, assign) BOOL shouldShowDefaultLoadingView; + +/// 在 QMUIAlbumViewController 被放到 UINavigationController 里之后,可通过调用这个方法,来尝试直接进入上一次选中的相册列表 +- (void)pickLastAlbumGroupDirectlyIfCan; + +@end + + +@interface QMUIAlbumViewController (UIAppearance) + ++ (instancetype)appearance; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m new file mode 100644 index 00000000..201b7c08 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m @@ -0,0 +1,280 @@ +/** + * 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. + */ + +// +// QMUIAlbumViewController.m +// qmui +// +// Created by QMUI Team on 15/5/3. +// + +#import "QMUIAlbumViewController.h" +#import "QMUICore.h" +#import "QMUINavigationButton.h" +#import "UIView+QMUI.h" +#import "QMUIAssetsManager.h" +#import "QMUIImagePickerViewController.h" +#import "QMUIImagePickerHelper.h" +#import "QMUIAppearance.h" +#import +#import +#import +#import +#import + +#pragma mark - QMUIAlbumTableViewCell + +@implementation QMUIAlbumTableViewCell + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [QMUIAlbumTableViewCell appearance].albumImageSize = 72; + [QMUIAlbumTableViewCell appearance].albumImageMarginLeft = 16; + [QMUIAlbumTableViewCell appearance].albumNameInsets = UIEdgeInsetsMake(0, 14, 0, 3); + [QMUIAlbumTableViewCell appearance].albumNameFont = UIFontMake(17); + [QMUIAlbumTableViewCell appearance].albumNameColor = TableViewCellTitleLabelColor; + [QMUIAlbumTableViewCell appearance].albumAssetsNumberFont = UIFontMake(17); + [QMUIAlbumTableViewCell appearance].albumAssetsNumberColor = TableViewCellTitleLabelColor; + }); +} + +- (void)didInitializeWithStyle:(UITableViewCellStyle)style { + [super didInitializeWithStyle:style]; + + [self qmui_applyAppearance]; + + self.imageView.contentMode = UIViewContentModeScaleAspectFill; + self.imageView.clipsToBounds = YES; + self.imageView.layer.borderWidth = PixelOne; + self.imageView.layer.borderColor = UIColorMakeWithRGBA(0, 0, 0, .1).CGColor; +} + +- (void)updateCellAppearanceWithIndexPath:(NSIndexPath *)indexPath { + [super updateCellAppearanceWithIndexPath:indexPath]; + self.textLabel.font = self.albumNameFont; + self.detailTextLabel.font = self.albumAssetsNumberFont; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + CGFloat imageEdgeTop = CGFloatGetCenter(CGRectGetHeight(self.contentView.bounds), self.albumImageSize); + CGFloat imageEdgeLeft = self.albumImageMarginLeft == -1 ? imageEdgeTop : self.albumImageMarginLeft; + self.imageView.frame = CGRectMake(imageEdgeLeft, imageEdgeTop, self.albumImageSize, self.albumImageSize); + + self.textLabel.frame = CGRectSetXY(self.textLabel.frame, CGRectGetMaxX(self.imageView.frame) + self.albumNameInsets.left, [self.textLabel qmui_topWhenCenterInSuperview]); + + CGFloat textLabelMaxWidth = CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(self.textLabel.frame) - CGRectGetWidth(self.detailTextLabel.bounds) - self.albumNameInsets.right; + if (CGRectGetWidth(self.textLabel.bounds) > textLabelMaxWidth) { + self.textLabel.frame = CGRectSetWidth(self.textLabel.frame, textLabelMaxWidth); + } + + self.detailTextLabel.frame = CGRectSetXY(self.detailTextLabel.frame, CGRectGetMaxX(self.textLabel.frame) + self.albumNameInsets.right, [self.detailTextLabel qmui_topWhenCenterInSuperview]); +} + +- (void)setAlbumNameFont:(UIFont *)albumNameFont { + _albumNameFont = albumNameFont; + self.textLabel.font = albumNameFont; +} + +- (void)setAlbumNameColor:(UIColor *)albumNameColor { + _albumNameColor = albumNameColor; + self.textLabel.textColor = albumNameColor; +} + +- (void)setAlbumAssetsNumberFont:(UIFont *)albumAssetsNumberFont { + _albumAssetsNumberFont = albumAssetsNumberFont; + self.detailTextLabel.font = albumAssetsNumberFont; +} + +- (void)setAlbumAssetsNumberColor:(UIColor *)albumAssetsNumberColor { + _albumAssetsNumberColor = albumAssetsNumberColor; + self.detailTextLabel.textColor = albumAssetsNumberColor; +} + +@end + + +#pragma mark - QMUIAlbumViewController (UIAppearance) + +@implementation QMUIAlbumViewController (UIAppearance) + ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self initAppearance]; + }); +} + ++ (void)initAppearance { + QMUIAlbumViewController.appearance.albumTableViewCellHeight = 88; +} + +@end + + +#pragma mark - QMUIAlbumViewController + +@interface QMUIAlbumViewController () + +@property(nonatomic, strong) NSMutableArray *albumsArray; +@property(nonatomic, strong) QMUIImagePickerViewController *imagePickerViewController; +@end + +@implementation QMUIAlbumViewController + +- (void)didInitialize { + [super didInitialize]; + _shouldShowDefaultLoadingView = YES; + [self qmui_applyAppearance]; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + if (!self.title) { + self.title = @"照片"; + } + self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithTitle:@"取消" target:self action:@selector(handleCancelSelectAlbum:)]; +} + +- (void)initTableView { + [super initTableView]; + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + if ([QMUIAssetsManager authorizationStatus] == QMUIAssetAuthorizationStatusNotAuthorized) { + // 如果没有获取访问授权,或者访问授权状态已经被明确禁止,则显示提示语,引导用户开启授权 + NSString *tipString = self.tipTextWhenNoPhotosAuthorization; + if (!tipString) { + NSDictionary *mainInfoDictionary = [[NSBundle mainBundle] infoDictionary]; + NSString *appName = [mainInfoDictionary objectForKey:@"CFBundleDisplayName"]; + if (!appName) { + appName = [mainInfoDictionary objectForKey:(NSString *)kCFBundleNameKey]; + } + tipString = [NSString stringWithFormat:@"请在设备的\"设置-隐私-照片\"选项中,允许%@访问你的手机相册", appName]; + } + [self showEmptyViewWithText:tipString detailText:nil buttonTitle:nil buttonAction:nil]; + } else { + self.albumsArray = [[NSMutableArray alloc] init]; + // 获取相册列表较为耗时,交给子线程去处理,因此这里需要显示 Loading + if ([self.albumViewControllerDelegate respondsToSelector:@selector(albumViewControllerWillStartLoading:)]) { + [self.albumViewControllerDelegate albumViewControllerWillStartLoading:self]; + } + if (self.shouldShowDefaultLoadingView) { + [self showEmptyViewWithLoading]; + } + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [[QMUIAssetsManager sharedInstance] enumerateAllAlbumsWithAlbumContentType:self.contentType usingBlock:^(QMUIAssetsGroup *resultAssetsGroup) { + 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:)]) { + [self.albumViewControllerDelegate albumViewControllerWillFinishLoading:self]; + } + if (self.shouldShowDefaultLoadingView) { + [self hideEmptyView]; + } + [self.tableView reloadData]; + } else { + NSString *tipString = self.tipTextWhenPhotosEmpty ? : @"空照片"; + [self showEmptyViewWithText:tipString detailText:nil buttonTitle:nil buttonAction:nil]; + } +} + +- (void)pickAlbumsGroup:(QMUIAssetsGroup *)assetsGroup animated:(BOOL)animated { + if (!assetsGroup) return; + + if (!self.imagePickerViewController) { + self.imagePickerViewController = [self.albumViewControllerDelegate imagePickerViewControllerForAlbumViewController:self]; + } + 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]; + [self.navigationController pushViewController:self.imagePickerViewController animated:animated]; +} + +- (void)pickLastAlbumGroupDirectlyIfCan { + QMUIAssetsGroup *assetsGroup = [QMUIImagePickerHelper assetsGroupOfLastPickerAlbumWithUserIdentify:nil]; + [self pickAlbumsGroup:assetsGroup animated:NO]; +} + +#pragma mark - + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return [self.albumsArray count]; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + return self.albumTableViewCellHeight; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *kCellIdentifer = @"cell"; + QMUIAlbumTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifer]; + if (!cell) { + cell = [[QMUIAlbumTableViewCell alloc] initForTableView:tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:kCellIdentifer]; + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + } + 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; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [self pickAlbumsGroup:self.albumsArray[indexPath.row] animated:YES]; +} + +- (void)handleCancelSelectAlbum:(id)sender { + [self dismissViewControllerAnimated:YES completion:^(void) { + if (self.albumViewControllerDelegate && [self.albumViewControllerDelegate respondsToSelector:@selector(albumViewControllerDidCancel:)]) { + [self.albumViewControllerDelegate albumViewControllerDidCancel:self]; + } + [self.imagePickerViewController.selectedImageAssetArray removeAllObjects]; + }]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h new file mode 100644 index 00000000..55a1e61b --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h @@ -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. + */ + +// +// QMUIImagePickerCollectionViewCell.h +// qmui +// +// Created by QMUI Team on 16/8/29. +// + +#import +#import +#import "QMUIAsset.h" + +@class QMUIButton; + +/** + * 图片选择空间里的九宫格 cell,支持显示 checkbox、饼状进度条及重试按钮(iCloud 图片需要) + */ +@interface QMUIImagePickerCollectionViewCell : UICollectionViewCell + +/// 收藏的资源的心形图片 +@property(nonatomic, strong) UIImage *favoriteImage UI_APPEARANCE_SELECTOR; + +/// 收藏的资源的心形图片的上下左右间距,相对于 cell 左下角零点而言,也即如果 left 越大则越往右,bottom 越大则越往上,另外 top 会影响底部遮罩的高度 +@property(nonatomic, assign) UIEdgeInsets favoriteImageMargins UI_APPEARANCE_SELECTOR; + +/// checkbox 未被选中时显示的图片 +@property(nonatomic, strong) UIImage *checkboxImage UI_APPEARANCE_SELECTOR; + +/// checkbox 被选中时显示的图片 +@property(nonatomic, strong) UIImage *checkboxCheckedImage UI_APPEARANCE_SELECTOR; + +/// checkbox 的 margin,定位从每个 cell(即每张图片)的最右边开始计算 +@property(nonatomic, assign) UIEdgeInsets checkboxButtonMargins UI_APPEARANCE_SELECTOR; + +/// videoDurationLabel 的字号 +@property(nonatomic, strong) UIFont *videoDurationLabelFont UI_APPEARANCE_SELECTOR; + +/// videoDurationLabel 的字体颜色 +@property(nonatomic, strong) UIColor *videoDurationLabelTextColor UI_APPEARANCE_SELECTOR; + +/// 视频时长文字的间距,相对于 cell 右下角而言,也即如果 right 越大则越往左,bottom 越大则越往上,另外 top 会影响底部遮罩的高度 +@property(nonatomic, assign) UIEdgeInsets videoDurationLabelMargins UI_APPEARANCE_SELECTOR; + +@property(nonatomic, strong, readonly) UIImageView *contentImageView; +@property(nonatomic, strong, readonly) UIImageView *favoriteImageView; +@property(nonatomic, strong, readonly) QMUIButton *checkboxButton; +@property(nonatomic, strong, readonly) UILabel *videoDurationLabel; +@property(nonatomic, strong, readonly) CAGradientLayer *bottomShadowLayer;// 当出现收藏或者视频时长文字时就会显示遮罩,遮罩高度为 favoriteImage 和 videoDurationLabel 中最高者的高度 + +@property(nonatomic, assign, getter=isSelectable) BOOL selectable; +@property(nonatomic, assign, getter=isChecked) BOOL checked; +@property(nonatomic, assign) QMUIAssetDownloadStatus downloadStatus; // Cell 中对应资源的下载状态,这个值的变动会相应地调整 UI 表现 +@property(nonatomic, copy) NSString *assetIdentifier;// 当前这个 cell 正在展示的 QMUIAsset 的 identifier + +- (void)renderWithAsset:(QMUIAsset *)asset referenceSize:(CGSize)referenceSize; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m new file mode 100644 index 00000000..44404cd9 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m @@ -0,0 +1,217 @@ +/** + * 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. + */ + +// +// QMUIImagePickerCollectionViewCell.m +// qmui +// +// Created by QMUI Team on 16/8/29. +// + +#import "QMUIImagePickerCollectionViewCell.h" +#import "QMUICore.h" +#import "QMUIImagePickerHelper.h" +#import "QMUIPieProgressView.h" +#import "UIControl+QMUI.h" +#import "UILabel+QMUI.h" +#import "CALayer+QMUI.h" +#import "QMUIButton.h" +#import "UIView+QMUI.h" +#import "NSString+QMUI.h" +#import "QMUIAppearance.h" + +@interface QMUIImagePickerCollectionViewCell () + +@property(nonatomic, strong, readwrite) UIImageView *favoriteImageView; +@property(nonatomic, strong, readwrite) QMUIButton *checkboxButton; +@property(nonatomic, strong, readwrite) CAGradientLayer *bottomShadowLayer; + +@end + + +@implementation QMUIImagePickerCollectionViewCell + +@synthesize videoDurationLabel = _videoDurationLabel; + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [QMUIImagePickerCollectionViewCell appearance].favoriteImage = [QMUIHelper imageWithName:@"QMUI_pickerImage_favorite"]; + [QMUIImagePickerCollectionViewCell appearance].favoriteImageMargins = UIEdgeInsetsMake(6, 6, 6, 6); + [QMUIImagePickerCollectionViewCell appearance].checkboxImage = [QMUIHelper imageWithName:@"QMUI_pickerImage_checkbox"]; + [QMUIImagePickerCollectionViewCell appearance].checkboxCheckedImage = [QMUIHelper imageWithName:@"QMUI_pickerImage_checkbox_checked"]; + [QMUIImagePickerCollectionViewCell appearance].checkboxButtonMargins = UIEdgeInsetsMake(6, 6, 6, 6); + [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelFont = UIFontMake(12); + [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelTextColor = UIColorWhite; + [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelMargins = UIEdgeInsetsMake(5, 5, 5, 7); + }); +} + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self initImagePickerCollectionViewCellUI]; + [self qmui_applyAppearance]; + } + return self; +} + +- (void)initImagePickerCollectionViewCellUI { + _contentImageView = [[UIImageView alloc] init]; + self.contentImageView.contentMode = UIViewContentModeScaleAspectFill; + self.contentImageView.clipsToBounds = YES; + [self.contentView addSubview:self.contentImageView]; + + self.bottomShadowLayer = [CAGradientLayer layer]; + [self.bottomShadowLayer qmui_removeDefaultAnimations]; + self.bottomShadowLayer.colors = @[(id)UIColorMakeWithRGBA(0, 0, 0, 0).CGColor, (id)UIColorMakeWithRGBA(0, 0, 0, .6).CGColor]; + self.bottomShadowLayer.hidden = YES; + [self.contentView.layer addSublayer:self.bottomShadowLayer]; + [self setNeedsLayout]; + + self.favoriteImageView = [[UIImageView alloc] init]; + self.favoriteImageView.hidden = YES; + [self.contentView addSubview:self.favoriteImageView]; + + self.checkboxButton = [[QMUIButton alloc] init]; + self.checkboxButton.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + self.checkboxButton.qmui_outsideEdge = UIEdgeInsetsMake(-6, -6, -6, -6); + self.checkboxButton.hidden = YES; + [self.contentView addSubview:self.checkboxButton]; +} + +- (void)renderWithAsset:(QMUIAsset *)asset referenceSize:(CGSize)referenceSize { + self.assetIdentifier = asset.identifier; + + // 异步请求资源对应的缩略图 + [asset requestThumbnailImageWithSize:referenceSize completion:^(UIImage *result, NSDictionary *info) { + if ([self.assetIdentifier isEqualToString:asset.identifier]) { + self.contentImageView.image = result; + } else { + self.contentImageView.image = nil; + } + }]; + + if (asset.assetType == QMUIAssetTypeVideo) { + [self initVideoDurationLabelIfNeeded]; + self.videoDurationLabel.text = [NSString qmui_timeStringWithMinsAndSecsFromSecs:asset.duration]; + self.videoDurationLabel.hidden = NO; + } else { + self.videoDurationLabel.hidden = YES; + } + + self.favoriteImageView.hidden = !asset.phAsset.favorite; + + self.bottomShadowLayer.hidden = !((self.videoDurationLabel && !self.videoDurationLabel.hidden) || !self.favoriteImageView.hidden); + + [self setNeedsLayout]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.contentImageView.frame = self.contentView.bounds; + if (_selectable) { + self.checkboxButton.frame = CGRectSetXY(self.checkboxButton.frame, CGRectGetWidth(self.contentView.bounds) - self.checkboxButtonMargins.right - CGRectGetWidth(self.checkboxButton.bounds), self.checkboxButtonMargins.top); + } + + CGFloat bottomShadowLayerHeight = 0; + + if (!self.favoriteImageView.hidden) { + self.favoriteImageView.frame = CGRectSetXY(self.favoriteImageView.frame, self.favoriteImageMargins.left, CGRectGetHeight(self.contentView.bounds) - self.favoriteImageMargins.bottom - CGRectGetHeight(self.favoriteImageView.frame)); + bottomShadowLayerHeight = CGRectGetHeight(self.favoriteImageView.frame) + UIEdgeInsetsGetVerticalValue(self.favoriteImageMargins); + } + + if (self.videoDurationLabel && !self.videoDurationLabel.hidden) { + [self.videoDurationLabel sizeToFit]; + self.videoDurationLabel.frame = CGRectSetXY(self.videoDurationLabel.frame, CGRectGetWidth(self.contentView.bounds) - self.videoDurationLabelMargins.right - CGRectGetWidth(self.videoDurationLabel.frame), CGRectGetHeight(self.contentView.bounds) - self.videoDurationLabelMargins.bottom - CGRectGetHeight(self.videoDurationLabel.frame)); + bottomShadowLayerHeight = MAX(bottomShadowLayerHeight, CGRectGetHeight(self.videoDurationLabel.frame) + UIEdgeInsetsGetVerticalValue(self.videoDurationLabelMargins)); + } + + if (!self.bottomShadowLayer.hidden) { + self.bottomShadowLayer.frame = CGRectMake(0, CGRectGetHeight(self.contentView.bounds) - bottomShadowLayerHeight, CGRectGetWidth(self.contentView.bounds), bottomShadowLayerHeight); + } +} + +- (void)setFavoriteImage:(UIImage *)favoriteImage { + if (![self.favoriteImage isEqual:favoriteImage]) { + self.favoriteImageView.image = favoriteImage; + [self.favoriteImageView sizeToFit]; + [self setNeedsLayout]; + } + _favoriteImage = favoriteImage; +} + +- (void)setCheckboxImage:(UIImage *)checkboxImage { + if (![self.checkboxImage isEqual:checkboxImage]) { + [self.checkboxButton setImage:checkboxImage forState:UIControlStateNormal]; + [self.checkboxButton sizeToFit]; + [self setNeedsLayout]; + } + _checkboxImage = checkboxImage; +} + +- (void)setCheckboxCheckedImage:(UIImage *)checkboxCheckedImage { + if (![self.checkboxCheckedImage isEqual:checkboxCheckedImage]) { + [self.checkboxButton setImage:checkboxCheckedImage forState:UIControlStateSelected]; + [self.checkboxButton setImage:checkboxCheckedImage forState:UIControlStateSelected|UIControlStateHighlighted]; + [self.checkboxButton sizeToFit]; + [self setNeedsLayout]; + } + _checkboxCheckedImage = checkboxCheckedImage; +} + +- (void)setVideoDurationLabelFont:(UIFont *)videoDurationLabelFont { + if (![self.videoDurationLabelFont isEqual:videoDurationLabelFont]) { + _videoDurationLabel.font = videoDurationLabelFont; + [_videoDurationLabel qmui_calculateHeightAfterSetAppearance]; + [self setNeedsLayout]; + } + _videoDurationLabelFont = videoDurationLabelFont; +} + +- (void)setVideoDurationLabelTextColor:(UIColor *)videoDurationLabelTextColor { + if (![self.videoDurationLabelTextColor isEqual:videoDurationLabelTextColor]) { + _videoDurationLabel.textColor = videoDurationLabelTextColor; + } + _videoDurationLabelTextColor = videoDurationLabelTextColor; +} + +- (void)setChecked:(BOOL)checked { + _checked = checked; + if (_selectable) { + self.checkboxButton.selected = checked; + [QMUIImagePickerHelper removeSpringAnimationOfImageCheckedWithCheckboxButton:self.checkboxButton]; + if (checked) { + [QMUIImagePickerHelper springAnimationOfImageCheckedWithCheckboxButton:self.checkboxButton]; + } + } +} + +- (void)setSelectable:(BOOL)editing { + _selectable = editing; + if (self.downloadStatus == QMUIAssetDownloadStatusSucceed) { + self.checkboxButton.hidden = !_selectable; + } +} + +- (void)setDownloadStatus:(QMUIAssetDownloadStatus)downloadStatus { + _downloadStatus = downloadStatus; + if (_selectable) { + self.checkboxButton.hidden = !_selectable; + } +} + +- (void)initVideoDurationLabelIfNeeded { + if (!self.videoDurationLabel) { + _videoDurationLabel = [[UILabel alloc] qmui_initWithFont:self.videoDurationLabelFont textColor:self.videoDurationLabelTextColor]; + [self.contentView addSubview:_videoDurationLabel]; + [self setNeedsLayout]; + } +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h new file mode 100644 index 00000000..8ee0bfa1 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.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. + */ + +// +// QMUIImagePickerHelper.h +// qmui +// +// Created by QMUI Team on 15/5/9. +// + +#import +#import +#import "QMUIAsset.h" +#import "QMUIAssetsGroup.h" + +/** + * 配合 QMUIImagePickerViewController 使用的工具类 + */ +@interface QMUIImagePickerHelper : NSObject + +/** + * 选中图片数量改变时,展示图片数量的 Label 的动画,动画过程如下: + * Label 背景色改为透明,同时产生一个与背景颜色和形状、大小都相同的图形置于 Label 底下,做先缩小再放大的 spring 动画 + * 动画结束后移除该图形,并恢复 Label 的背景色 + * + * @warning iOS6 下降级处理不调用动画效果 + * + * @param label 需要做动画的 UILabel + */ ++ (void)springAnimationOfImageSelectedCountChangeWithCountLabel:(UILabel *)label; + +/** + * 图片 checkBox 被选中时的动画 + * @warning iOS6 下降级处理不调用动画效果 + * + * @param button 需要做动画的 checkbox 按钮 + */ ++ (void)springAnimationOfImageCheckedWithCheckboxButton:(UIButton *)button; + +/** + * 搭配springAnimationOfImageCheckedWithCheckboxButton:一起使用,添加animation之前建议先remove + */ ++ (void)removeSpringAnimationOfImageCheckedWithCheckboxButton:(UIButton *)button; + + +/** + * 获取最近一次调用 updateLastAlbumWithAssetsGroup 方法调用时储存的 QMUIAssetsGroup 对象 + * + * @param userIdentify 用户标识,由于每个用户可能需要分开储存一个最近调用过的 QMUIAssetsGroup,因此增加一个标识区分用户。 + * 一个常见的应用场景是选择图片时保存图片所在相册的对应的 QMUIAssetsGroup,并使用用户的 user id 作为区分不同用户的标识, + * 当用户再次选择图片时可以根据已经保存的 QMUIAssetsGroup 直接进入上次使用过的相册。 + */ ++ (QMUIAssetsGroup *)assetsGroupOfLastPickerAlbumWithUserIdentify:(NSString *)userIdentify; + +/** + * 储存一个 QMUIAssetsGroup,从而储存一个对应的相册,与 assetsGroupOfLatestPickerAlbumWithUserIdentify 方法对应使用 + * + * @param assetsGroup 要被储存的 QMUIAssetsGroup + * @param albumContentType 相册的内容类型 + * @param userIdentify 用户标识,由于每个用户可能需要分开储存一个最近调用过的 QMUIAssetsGroup,因此增加一个标识区分用户 + */ ++ (void)updateLastestAlbumWithAssetsGroup:(QMUIAssetsGroup *)assetsGroup ablumContentType:(QMUIAlbumContentType)albumContentType userIdentify:(NSString *)userIdentify; + +/** + * 检测一组资源是否全部下载成功,如果有资源仍未从 iCloud 中下载成功,则返回 NO + * + * 可以用于选择图片后,业务需要自行处理 iCloud 下载的场景。 + */ ++ (BOOL)imageAssetsDownloaded:(NSMutableArray *)imagesAssetArray; + +/** + * 检测资源是否已经在本地,如果资源仍未从 iCloud 中成功下载,则会发出请求从 iCloud 加载资源,并通过多次调用 block 返回请求结果 + * + * 可以用于选择图片后,业务需要自行处理 iCloud 下载的场景。 + */ ++ (void)requestImageAssetIfNeeded:(QMUIAsset *)asset completion: (void (^)(QMUIAssetDownloadStatus downloadStatus, NSError *error))completion; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m new file mode 100644 index 00000000..f932d96d --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m @@ -0,0 +1,147 @@ +/** + * 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. + */ + +// +// QMUIImagePickerHelper.m +// qmui +// +// Created by QMUI Team on 15/5/9. +// + +#import "QMUIImagePickerHelper.h" +#import "QMUICore.h" +#import "QMUIAssetsManager.h" +#import "QMUIAsset.h" +#import +#import +#import "UIImage+QMUI.h" +#import "QMUILog.h" + +static NSString * const kLastAlbumKeyPrefix = @"QMUILastestAlbumKeyWith"; +static NSString * const kContentTypeOfLastAlbumKeyPrefix = @"QMUIContentTypeOfLastestAlbumKeyWith"; + +@implementation QMUIImagePickerHelper + ++ (void)springAnimationOfImageSelectedCountChangeWithCountLabel:(UILabel *)label { + [self actionSpringAnimationForView:label]; +} + ++ (void)springAnimationOfImageCheckedWithCheckboxButton:(UIButton *)button { + [self actionSpringAnimationForView:button]; +} + ++ (void)actionSpringAnimationForView:(UIView *)view { + NSTimeInterval duration = 0.6; + CAKeyframeAnimation *springAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; + springAnimation.values = @[@.85, @1.15, @.9, @1.0,]; + springAnimation.keyTimes = @[@(0.0 / duration), @(0.15 / duration) , @(0.3 / duration), @(0.45 / duration),]; + springAnimation.duration = duration; + [view.layer addAnimation:springAnimation forKey:@"imagePickerActionSpring"]; +} + ++ (void)removeSpringAnimationOfImageCheckedWithCheckboxButton:(UIButton *)button { + [button.layer removeAnimationForKey:@"imagePickerActionSpring"]; +} + ++ (QMUIAssetsGroup *)assetsGroupOfLastPickerAlbumWithUserIdentify:(NSString *)userIdentify { + // 获取 NSUserDefaults,里面储存了所有 updateLastestAlbumWithAssetsGroup 的结果 + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + // 使用特定的前缀和可以标记不同用户的字符串拼接成 key,用于获取当前用户最近调用 updateLastestAlbumWithAssetsGroup 储存的相册以及对于的 QMUIAlbumContentType 值 + NSString *lastAlbumKey = [NSString stringWithFormat:@"%@%@", kLastAlbumKeyPrefix, userIdentify]; + NSString *contentTypeOflastAlbumKey = [NSString stringWithFormat:@"%@%@", kContentTypeOfLastAlbumKeyPrefix, userIdentify]; + + __block QMUIAssetsGroup *assetsGroup; + + QMUIAlbumContentType albumContentType = (QMUIAlbumContentType)[userDefaults integerForKey:contentTypeOflastAlbumKey]; + + NSString *groupIdentifier = [userDefaults valueForKey:lastAlbumKey]; + /** + * 如果获取到的 PHAssetCollection localIdentifier 不为空,则获取该 URL 对应的相册。 + * 在 QMUI 2.0.0 及较早的版本中,QMUI 兼容 AssetsLibrary 的使用, + * 因此原来储存的 groupIdentifier 实际上可能会是一个 NSURL 而不是我们需要的 NSString, + * 所以这里还需要判断一下实际拿到的数据的类型是否为 NSString,如果是才继续进行。 + */ + if (groupIdentifier && [groupIdentifier isKindOfClass:[NSString class]]) { + PHFetchResult *phFetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[groupIdentifier] options:nil]; + if (phFetchResult.count > 0) { + // 创建一个 PHFetchOptions,用于对内容类型进行控制 + PHFetchOptions *phFetchOptions; + // 旧版本中没有存储 albumContentType,因此为了防止 crash,这里做一下判断 + if (albumContentType) { + phFetchOptions = [PHPhotoLibrary createFetchOptionsWithAlbumContentType:albumContentType]; + } + PHAssetCollection *phAssetCollection = [phFetchResult firstObject]; + assetsGroup = [[QMUIAssetsGroup alloc] initWithPHCollection:phAssetCollection fetchAssetsOptions:phFetchOptions]; + } + } else { + QMUILog(@"QMUIImagePickerLibrary", @"Group For localIdentifier is not found! groupIdentifier is %@", groupIdentifier); + } + return assetsGroup; +} + ++ (void)updateLastestAlbumWithAssetsGroup:(QMUIAssetsGroup *)assetsGroup ablumContentType:(QMUIAlbumContentType)albumContentType userIdentify:(NSString *)userIdentify { + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + // 使用特定的前缀和可以标记不同用户的字符串拼接成 key,用于为当前用户储存相册对应的 QMUIAssetsGroup 与 QMUIAlbumContentType + NSString *lastAlbumKey = [NSString stringWithFormat:@"%@%@", kLastAlbumKeyPrefix, userIdentify]; + NSString *contentTypeOflastAlbumKey = [NSString stringWithFormat:@"%@%@", kContentTypeOfLastAlbumKeyPrefix, userIdentify]; + [userDefaults setValue:assetsGroup.phAssetCollection.localIdentifier forKey:lastAlbumKey]; + [userDefaults setInteger:albumContentType forKey:contentTypeOflastAlbumKey]; + [userDefaults synchronize]; +} + ++ (BOOL)imageAssetsDownloaded:(NSMutableArray *)imagesAssetArray { + for (QMUIAsset *asset in imagesAssetArray) { + if (asset.downloadStatus != QMUIAssetDownloadStatusSucceed) { + return NO; + } + } + return YES; +} + ++ (void)requestImageAssetIfNeeded:(QMUIAsset *)asset completion: (void (^)(QMUIAssetDownloadStatus downloadStatus, NSError *error))completion { + if (asset.downloadStatus != QMUIAssetDownloadStatusSucceed) { + + // 资源加载中 + if (completion) { + completion(QMUIAssetDownloadStatusDownloading, nil); + } + + [asset requestOriginImageWithCompletion:^(UIImage *result, NSDictionary *info) { + BOOL downloadSucceed = (result && !info) || (![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue]); + + if (downloadSucceed) { + // 资源资源已经在本地或下载成功 + [asset updateDownloadStatusWithDownloadResult:YES]; + + if (completion) { + completion(QMUIAssetDownloadStatusSucceed, nil); + } + + } else if ([info objectForKey:PHImageErrorKey]) { + // 下载错误 + [asset updateDownloadStatusWithDownloadResult:NO]; + + if (completion) { + completion(QMUIAssetDownloadStatusFailed, [info objectForKey:PHImageErrorKey]); + } + } + } withProgressHandler:^(double progress, NSError * _Nullable error, BOOL * _Nonnull stop, NSDictionary * _Nullable info) { + QMUILog(@"QMUIImagePickerLibrary", @"current progress is %f", progress); + asset.downloadProgress = progress; + }]; + } else { + // 资源资源已经在本地或下载成功 + if (completion) { + completion(QMUIAssetDownloadStatusSucceed, nil); + } + } +} + +@end + + diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h new file mode 100644 index 00000000..b9315298 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h @@ -0,0 +1,92 @@ +/** + * 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. + */ + +// +// QMUIImagePickerPreviewViewController.h +// qmui +// +// Created by QMUI Team on 15/5/3. +// + +#import +#import "QMUIImagePreviewViewController.h" +#import "QMUIAsset.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIButton, QMUINavigationButton; +@class QMUIImagePickerViewController; +@class QMUIImagePickerPreviewViewController; + +@protocol QMUIImagePickerPreviewViewControllerDelegate + +@optional + +/// 取消选择图片后被调用 +- (void)imagePickerPreviewViewControllerDidCancel:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController; +/// 即将选中图片 +- (void)imagePickerPreviewViewController:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController willCheckImageAtIndex:(NSInteger)index; +/// 已经选中图片 +- (void)imagePickerPreviewViewController:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController didCheckImageAtIndex:(NSInteger)index; +/// 即将取消选中图片 +- (void)imagePickerPreviewViewController:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController willUncheckImageAtIndex:(NSInteger)index; +/// 已经取消选中图片 +- (void)imagePickerPreviewViewController:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController didUncheckImageAtIndex:(NSInteger)index; + +@end + + +@interface QMUIImagePickerPreviewViewController : QMUIImagePreviewViewController + +@property(nullable, nonatomic, weak) id delegate; + +@property(nullable, nonatomic, strong) UIColor *toolBarBackgroundColor UI_APPEARANCE_SELECTOR; +@property(nullable, nonatomic, strong) UIColor *toolBarTintColor UI_APPEARANCE_SELECTOR; + +@property(nullable, nonatomic, strong, readonly) UIView *topToolBarView; +@property(nullable, nonatomic, strong, readonly) QMUINavigationButton *backButton; +@property(nullable, nonatomic, strong, readonly) QMUIButton *checkboxButton; + +/// 由于组件需要通过本地图片的 QMUIAsset 对象读取图片的详细信息,因此这里的需要传入的是包含一个或多个 QMUIAsset 对象的数组 +@property(nullable, nonatomic, strong) NSMutableArray *imagesAssetArray; +@property(nullable, nonatomic, strong) NSMutableArray *selectedImageAssetArray; + +@property(nonatomic, assign) QMUIAssetDownloadStatus downloadStatus; + +/// 最多可以选择的图片数,默认为无穷大 +@property(nonatomic, assign) NSUInteger maximumSelectImageCount; +/// 最少需要选择的图片数,默认为 0 +@property(nonatomic, assign) NSUInteger minimumSelectImageCount; +/// 选择图片超出最大图片限制时 alertView 的标题 +@property(nullable, nonatomic, copy) NSString *alertTitleWhenExceedMaxSelectImageCount; +/// 选择图片超出最大图片限制时 alertView 的标题 +@property(nullable, nonatomic, copy) NSString *alertButtonTitleWhenExceedMaxSelectImageCount; + +/** + * 更新数据并刷新 UI,手工调用 + * + * @param imageAssetArray 包含所有需要展示的图片的数组 + * @param selectedImageAssetArray 包含所有需要展示的图片中已经被选中的图片的数组 + * @param currentImageIndex 当前展示的图片在 imageAssetArray 的索引 + * @param singleCheckMode 是否为单选模式,如果是单选模式,则不显示 checkbox + */ +- (void)updateImagePickerPreviewViewWithImagesAssetArray:(NSMutableArray * _Nullable)imageAssetArray + selectedImageAssetArray:(NSMutableArray * _Nullable)selectedImageAssetArray + currentImageIndex:(NSInteger)currentImageIndex + singleCheckMode:(BOOL)singleCheckMode; + +@end + + +@interface QMUIImagePickerPreviewViewController (UIAppearance) + ++ (instancetype)appearance; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m new file mode 100644 index 00000000..68866778 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m @@ -0,0 +1,415 @@ +/** + * 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. + */ + +// +// QMUIImagePickerPreviewViewController.m +// qmui +// +// Created by QMUI Team on 15/5/3. +// + +#import "QMUIImagePickerPreviewViewController.h" +#import "QMUICore.h" +#import "QMUIImagePickerViewController.h" +#import "QMUIImagePickerHelper.h" +#import "QMUIAssetsManager.h" +#import "QMUIZoomImageView.h" +#import "QMUIAsset.h" +#import "QMUIButton.h" +#import "QMUINavigationButton.h" +#import "QMUIImagePickerHelper.h" +#import "QMUIPieProgressView.h" +#import "QMUIAlertController.h" +#import "UIImage+QMUI.h" +#import "UIView+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 initAppearance]; + }); +} + ++ (void)initAppearance { + QMUIImagePickerPreviewViewController.appearance.toolBarBackgroundColor = UIColorMakeWithRGBA(27, 27, 27, .9f); + QMUIImagePickerPreviewViewController.appearance.toolBarTintColor = UIColorWhite; +} + +@end + +@implementation QMUIImagePickerPreviewViewController { + BOOL _singleCheckMode; +} + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { + self.maximumSelectImageCount = INT_MAX; + self.minimumSelectImageCount = 0; + + [self qmui_applyAppearance]; + } + return self; +} + +- (void)initSubviews { + [super initSubviews]; + + self.imagePreviewView.delegate = self; + + _topToolBarView = [[UIView alloc] init]; + self.topToolBarView.backgroundColor = self.toolBarBackgroundColor; + self.topToolBarView.tintColor = self.toolBarTintColor; + [self.view addSubview:self.topToolBarView]; + + _backButton = [[QMUINavigationButton alloc] initWithType:QMUINavigationButtonTypeBack]; + [self.backButton addTarget:self action:@selector(handleCancelPreviewImage:) forControlEvents:UIControlEventTouchUpInside]; + self.backButton.qmui_outsideEdge = UIEdgeInsetsMake(-30, -20, -50, -80); + [self.topToolBarView addSubview:self.backButton]; + + _checkboxButton = [[QMUIButton alloc] init]; + self.checkboxButton.adjustsTitleTintColorAutomatically = YES; + self.checkboxButton.adjustsImageTintColorAutomatically = YES; + UIImage *checkboxImage = [QMUIHelper imageWithName:@"QMUI_previewImage_checkbox"]; + UIImage *checkedCheckboxImage = [QMUIHelper imageWithName:@"QMUI_previewImage_checkbox_checked"]; + [self.checkboxButton setImage:checkboxImage forState:UIControlStateNormal]; + [self.checkboxButton setImage:checkedCheckboxImage forState:UIControlStateSelected]; + [self.checkboxButton setImage:[self.checkboxButton imageForState:UIControlStateSelected] forState:UIControlStateSelected|UIControlStateHighlighted]; + [self.checkboxButton sizeToFit]; + [self.checkboxButton addTarget:self action:@selector(handleCheckButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + self.checkboxButton.qmui_outsideEdge = UIEdgeInsetsMake(-6, -6, -6, -6); + [self.topToolBarView addSubview:self.checkboxButton]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + if (!_singleCheckMode) { + QMUIAsset *imageAsset = self.imagesAssetArray[self.imagePreviewView.currentImageIndex]; + self.checkboxButton.selected = [self.selectedImageAssetArray containsObject:imageAsset]; + } + + if ([self conformsToProtocol:@protocol(QMUICustomNavigationBarTransitionDelegate)]) { + UIViewController *vc = (UIViewController *)self; + if ([vc respondsToSelector:@selector(shouldCustomizeNavigationBarTransitionIfHideable)] && + [vc shouldCustomizeNavigationBarTransitionIfHideable]) { + } else { + [self.navigationController setNavigationBarHidden:YES animated:NO]; + } + } +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + if ([self conformsToProtocol:@protocol(QMUICustomNavigationBarTransitionDelegate)]) { + UIViewController *vc = (UIViewController *)self; + if ([vc respondsToSelector:@selector(shouldCustomizeNavigationBarTransitionIfHideable)] && + [vc shouldCustomizeNavigationBarTransitionIfHideable]) { + } else { + [self.navigationController setNavigationBarHidden:NO animated:NO]; + } + } +} + +- (void)viewDidLayoutSubviews { + [super 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.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.safeAreaInsets.right - CGRectGetWidth(self.checkboxButton.frame), topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.checkboxButton.frame))); + } +} + +- (BOOL)preferredNavigationBarHidden { + return YES; +} + +- (BOOL)prefersStatusBarHidden { + return YES; +} + +- (void)setToolBarBackgroundColor:(UIColor *)toolBarBackgroundColor { + _toolBarBackgroundColor = toolBarBackgroundColor; + self.topToolBarView.backgroundColor = self.toolBarBackgroundColor; +} + +- (void)setToolBarTintColor:(UIColor *)toolBarTintColor { + _toolBarTintColor = toolBarTintColor; + self.topToolBarView.tintColor = toolBarTintColor; +} + +- (void)setDownloadStatus:(QMUIAssetDownloadStatus)downloadStatus { + _downloadStatus = downloadStatus; + if (!_singleCheckMode) { + self.checkboxButton.hidden = NO; + } +} + +- (void)updateImagePickerPreviewViewWithImagesAssetArray:(NSMutableArray *)imageAssetArray + selectedImageAssetArray:(NSMutableArray *)selectedImageAssetArray + currentImageIndex:(NSInteger)currentImageIndex + singleCheckMode:(BOOL)singleCheckMode { + self.imagesAssetArray = imageAssetArray; + self.selectedImageAssetArray = selectedImageAssetArray; + self.imagePreviewView.currentImageIndex = currentImageIndex; + _singleCheckMode = singleCheckMode; + if (singleCheckMode) { + self.checkboxButton.hidden = YES; + } +} + +#pragma mark - + +- (NSUInteger)numberOfImagesInImagePreviewView:(QMUIImagePreviewView *)imagePreviewView { + return [self.imagesAssetArray count]; +} + +- (QMUIImagePreviewMediaType)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView assetTypeAtIndex:(NSUInteger)index { + QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:index]; + if (imageAsset.assetType == QMUIAssetTypeImage) { + if (imageAsset.assetSubType == QMUIAssetSubTypeLivePhoto) { + return QMUIImagePreviewMediaTypeLivePhoto; + } + return QMUIImagePreviewMediaTypeImage; + } else if (imageAsset.assetType == QMUIAssetTypeVideo) { + return QMUIImagePreviewMediaTypeVideo; + } else { + return QMUIImagePreviewMediaTypeOthers; + } +} + +- (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView renderZoomImageView:(QMUIZoomImageView *)zoomImageView atIndex:(NSUInteger)index { + [self requestImageForZoomImageView:zoomImageView withIndex:index]; +} + +- (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView willScrollHalfToIndex:(NSUInteger)index { + if (!_singleCheckMode) { + QMUIAsset *imageAsset = self.imagesAssetArray[index]; + self.checkboxButton.selected = [self.selectedImageAssetArray containsObject:imageAsset]; + } +} + +#pragma mark - + +- (void)singleTouchInZoomingImageView:(QMUIZoomImageView *)zoomImageView location:(CGPoint)location { + self.topToolBarView.hidden = !self.topToolBarView.hidden; +} + +- (void)didTouchICloudRetryButtonInZoomImageView:(QMUIZoomImageView *)imageView { + NSInteger index = [self.imagePreviewView indexForZoomImageView:imageView]; + [self.imagePreviewView.collectionView reloadItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:index inSection:0]]]; +} + +- (void)zoomImageView:(QMUIZoomImageView *)imageView didHideVideoToolbar:(BOOL)didHide { + self.topToolBarView.hidden = didHide; +} + +#pragma mark - 按钮点击回调 + +- (void)handleCancelPreviewImage:(QMUIButton *)button { + if (self.navigationController) { + [self.navigationController popViewControllerAnimated:YES]; + } else { +// [self exitPreviewAutomatically]; + } + if (self.delegate && [self.delegate respondsToSelector:@selector(imagePickerPreviewViewControllerDidCancel:)]) { + [self.delegate imagePickerPreviewViewControllerDidCancel:self]; + } +} + +- (void)handleCheckButtonClick:(QMUIButton *)button { + [QMUIImagePickerHelper removeSpringAnimationOfImageCheckedWithCheckboxButton:button]; + + if (button.selected) { + if ([self.delegate respondsToSelector:@selector(imagePickerPreviewViewController:willUncheckImageAtIndex:)]) { + [self.delegate imagePickerPreviewViewController:self willUncheckImageAtIndex:self.imagePreviewView.currentImageIndex]; + } + + button.selected = NO; + QMUIAsset *imageAsset = self.imagesAssetArray[self.imagePreviewView.currentImageIndex]; + [self.selectedImageAssetArray removeObject:imageAsset]; + + if ([self.delegate respondsToSelector:@selector(imagePickerPreviewViewController:didUncheckImageAtIndex:)]) { + [self.delegate imagePickerPreviewViewController:self didUncheckImageAtIndex:self.imagePreviewView.currentImageIndex]; + } + } else { + if ([self.selectedImageAssetArray count] >= self.maximumSelectImageCount) { + if (!self.alertTitleWhenExceedMaxSelectImageCount) { + self.alertTitleWhenExceedMaxSelectImageCount = [NSString stringWithFormat:@"你最多只能选择%@张图片", @(self.maximumSelectImageCount)]; + } + if (!self.alertButtonTitleWhenExceedMaxSelectImageCount) { + self.alertButtonTitleWhenExceedMaxSelectImageCount = [NSString stringWithFormat:@"我知道了"]; + } + + QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:self.alertTitleWhenExceedMaxSelectImageCount message:nil preferredStyle:QMUIAlertControllerStyleAlert]; + [alertController addAction:[QMUIAlertAction actionWithTitle:self.alertButtonTitleWhenExceedMaxSelectImageCount style:QMUIAlertActionStyleCancel handler:nil]]; + [alertController showWithAnimated:YES]; + return; + } + + if (self.delegate && [self.delegate respondsToSelector:@selector(imagePickerPreviewViewController:willCheckImageAtIndex:)]) { + [self.delegate imagePickerPreviewViewController:self willCheckImageAtIndex:self.imagePreviewView.currentImageIndex]; + } + + button.selected = YES; + [QMUIImagePickerHelper springAnimationOfImageCheckedWithCheckboxButton:button]; + QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:self.imagePreviewView.currentImageIndex]; + [self.selectedImageAssetArray addObject:imageAsset]; + + if (self.delegate && [self.delegate respondsToSelector:@selector(imagePickerPreviewViewController:didCheckImageAtIndex:)]) { + [self.delegate imagePickerPreviewViewController:self didCheckImageAtIndex:self.imagePreviewView.currentImageIndex]; + } + } +} + +#pragma mark - Request Image + +- (void)requestImageForZoomImageView:(QMUIZoomImageView *)zoomImageView withIndex:(NSInteger)index { + QMUIZoomImageView *imageView = zoomImageView ? : [self.imagePreviewView zoomImageViewAtIndex:index]; + // 如果是走 PhotoKit 的逻辑,那么这个 block 会被多次调用,并且第一次调用时返回的图片是一张小图, + // 拉取图片的过程中可能会多次返回结果,且图片尺寸越来越大,因此这里调整 contentMode 以防止图片大小跳动 + imageView.contentMode = UIViewContentModeScaleAspectFit; + QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:index]; + // 获取资源图片的预览图,这是一张适合当前设备屏幕大小的图片,最终展示时把图片交给组件控制最终展示出来的大小。 + // 系统相册本质上也是这么处理的,因此无论是系统相册,还是这个系列组件,由始至终都没有显示照片原图, + // 这也是系统相册能加载这么快的原因。 + // 另外这里采用异步请求获取图片,避免获取图片时 UI 卡顿 + PHAssetImageProgressHandler phProgressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { + imageAsset.downloadProgress = progress; + dispatch_async(dispatch_get_main_queue(), ^{ + QMUILogInfo(@"QMUIImagePickerLibrary", @"Download iCloud image in preview, current progress is: %f", progress); + + if (self.downloadStatus != QMUIAssetDownloadStatusDownloading) { + self.downloadStatus = QMUIAssetDownloadStatusDownloading; + imageView.cloudDownloadStatus = QMUIAssetDownloadStatusDownloading; + + // 重置 progressView 的显示的进度为 0 + [imageView.cloudProgressView setProgress:0 animated:NO]; + } + // 拉取资源的初期,会有一段时间没有进度,猜测是发出网络请求以及与 iCloud 建立连接的耗时,这时预先给个 0.02 的进度值,看上去好看些 + float targetProgress = fmax(0.02, progress); + if (targetProgress < imageView.cloudProgressView.progress) { + [imageView.cloudProgressView setProgress:targetProgress animated:NO]; + } else { + imageView.cloudProgressView.progress = fmax(0.02, progress); + } + if (error) { + QMUILog(@"QMUIImagePickerLibrary", @"Download iCloud image Failed, current progress is: %f", progress); + self.downloadStatus = QMUIAssetDownloadStatusFailed; + imageView.cloudDownloadStatus = QMUIAssetDownloadStatusFailed; + } + }); + }; + if (imageAsset.assetType == QMUIAssetTypeVideo) { + imageView.tag = -1; + imageAsset.requestID = [imageAsset requestPlayerItemWithCompletion:^(AVPlayerItem *playerItem, NSDictionary *info) { + // 这里可能因为 imageView 复用,导致前面的请求得到的结果显示到别的 imageView 上, + // 因此判断如果是新请求(无复用问题)或者是当前的请求才把获得的图片结果展示出来 + dispatch_async(dispatch_get_main_queue(), ^{ + BOOL isNewRequest = (imageView.tag == -1 && imageAsset.requestID == 0); + BOOL isCurrentRequest = imageView.tag == imageAsset.requestID; + BOOL loadICloudImageFault = !playerItem || info[PHImageErrorKey]; + if (!loadICloudImageFault && (isNewRequest || isCurrentRequest)) { + imageView.videoPlayerItem = playerItem; + } + }); + } withProgressHandler:phProgressHandler]; + imageView.tag = imageAsset.requestID; + } else { + if (imageAsset.assetType != QMUIAssetTypeImage) { + return; + } + + // 这么写是为了消除 Xcode 的 API available warning + BOOL isLivePhoto = NO; + if (imageAsset.assetSubType == QMUIAssetSubTypeLivePhoto) { + isLivePhoto = YES; + imageView.tag = -1; + imageAsset.requestID = [imageAsset requestLivePhotoWithCompletion:^void(PHLivePhoto *livePhoto, NSDictionary *info) { + // 这里可能因为 imageView 复用,导致前面的请求得到的结果显示到别的 imageView 上, + // 因此判断如果是新请求(无复用问题)或者是当前的请求才把获得的图片结果展示出来 + dispatch_async(dispatch_get_main_queue(), ^{ + BOOL isNewRequest = (imageView.tag == -1 && imageAsset.requestID == 0); + BOOL isCurrentRequest = imageView.tag == imageAsset.requestID; + BOOL loadICloudImageFault = !livePhoto || info[PHImageErrorKey]; + if (!loadICloudImageFault && (isNewRequest || isCurrentRequest)) { + // 如果是走 PhotoKit 的逻辑,那么这个 block 会被多次调用,并且第一次调用时返回的图片是一张小图, + // 这时需要把图片放大到跟屏幕一样大,避免后面加载大图后图片的显示会有跳动 + imageView.livePhoto = livePhoto; + } + BOOL downloadSucceed = (livePhoto && !info) || (![[info objectForKey:PHLivePhotoInfoCancelledKey] boolValue] && ![info objectForKey:PHLivePhotoInfoErrorKey] && ![[info objectForKey:PHLivePhotoInfoIsDegradedKey] boolValue]); + if (downloadSucceed) { + // 资源资源已经在本地或下载成功 + [imageAsset updateDownloadStatusWithDownloadResult:YES]; + self.downloadStatus = QMUIAssetDownloadStatusSucceed; + imageView.cloudDownloadStatus = QMUIAssetDownloadStatusSucceed; + } else if ([info objectForKey:PHLivePhotoInfoErrorKey] ) { + // 下载错误 + [imageAsset updateDownloadStatusWithDownloadResult:NO]; + self.downloadStatus = QMUIAssetDownloadStatusFailed; + imageView.cloudDownloadStatus = QMUIAssetDownloadStatusFailed; + } + }); + } 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), ^{ + UIImage *resultImage = [UIImage qmui_animatedImageWithData:imageData]; + dispatch_async(dispatch_get_main_queue(), ^{ + imageView.image = resultImage; + }); + }); + }]; + } else { + imageView.tag = -1; + imageView.image = [imageAsset thumbnailWithSize:CGSizeMake([QMUIImagePickerViewController appearance].minimumImageWidth, [QMUIImagePickerViewController appearance].minimumImageWidth)]; + imageAsset.requestID = [imageAsset requestOriginImageWithCompletion:^void(UIImage *result, NSDictionary *info) { + // 这里可能因为 imageView 复用,导致前面的请求得到的结果显示到别的 imageView 上, + // 因此判断如果是新请求(无复用问题)或者是当前的请求才把获得的图片结果展示出来 + dispatch_async(dispatch_get_main_queue(), ^{ + BOOL isNewRequest = (imageView.tag == -1 && imageAsset.requestID == 0); + BOOL isCurrentRequest = imageView.tag == imageAsset.requestID; + BOOL loadICloudImageFault = !result || info[PHImageErrorKey]; + if (!loadICloudImageFault && (isNewRequest || isCurrentRequest)) { + imageView.image = result; + } + BOOL downloadSucceed = (result && !info) || (![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue]); + if (downloadSucceed) { + // 资源资源已经在本地或下载成功 + [imageAsset updateDownloadStatusWithDownloadResult:YES]; + self.downloadStatus = QMUIAssetDownloadStatusSucceed; + imageView.cloudDownloadStatus = QMUIAssetDownloadStatusSucceed; + } else if ([info objectForKey:PHImageErrorKey] ) { + // 下载错误 + [imageAsset updateDownloadStatusWithDownloadResult:NO]; + self.downloadStatus = QMUIAssetDownloadStatusFailed; + imageView.cloudDownloadStatus = QMUIAssetDownloadStatusFailed; + } + }); + } withProgressHandler:phProgressHandler]; + imageView.tag = imageAsset.requestID; + } + } +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h new file mode 100644 index 00000000..bd0082b7 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h @@ -0,0 +1,153 @@ +/** + * 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. + */ + +// +// QMUIImagePickerViewController.h +// qmui +// +// Created by QMUI Team on 15/5/2. +// + +#import +#import "QMUICommonViewController.h" +#import "QMUIImagePickerPreviewViewController.h" +#import "QMUIAsset.h" +#import "QMUIAssetsGroup.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIImagePickerViewController; +@class QMUIButton; + +@protocol QMUIImagePickerViewControllerDelegate + +@optional + +/** + * 创建一个 ImagePickerPreviewViewController 用于预览图片 + */ +- (QMUIImagePickerPreviewViewController *)imagePickerPreviewViewControllerForImagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController; + +/** + * 控制照片的排序,若不实现,默认为 QMUIAlbumSortTypePositive + * @note 注意返回值会决定第一次进来相片列表时列表默认的滚动位置,如果为 QMUIAlbumSortTypePositive,则列表默认滚动到底部,如果为 QMUIAlbumSortTypeReverse,则列表默认滚动到顶部。 + */ +- (QMUIAlbumSortType)albumSortTypeForImagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController; + +/** + * 多选模式下选择图片完毕后被调用(点击 sendButton 后被调用),单选模式下没有底部发送按钮,所以也不会走到这个delegate + * + * @param imagePickerViewController 对应的 QMUIImagePickerViewController + * @param imagesAssetArray 包含被选择的图片的 QMUIAsset 对象的数组。 + */ +- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController didFinishPickingImageWithImagesAssetArray:(NSMutableArray *)imagesAssetArray; + +/** + * cell 被点击时调用(先调用这个接口,然后才去走预览大图的逻辑),注意这并非指选中 checkbox 事件 + * + * @param imagePickerViewController 对应的 QMUIImagePickerViewController + * @param imageAsset 被选中的图片的 QMUIAsset 对象 + * @param imagePickerPreviewViewController 选中图片后进行图片预览的 viewController + */ +- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController didSelectImageWithImagesAsset:(QMUIAsset *)imageAsset afterImagePickerPreviewViewControllerUpdate:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController; + +/// 是否能够选中 checkbox +- (BOOL)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController shouldCheckImageAtIndex:(NSInteger)index; + +/// 即将选中 checkbox 时调用 +- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController willCheckImageAtIndex:(NSInteger)index; + +/// 选中了 checkbox 之后调用 +- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController didCheckImageAtIndex:(NSInteger)index; + +/// 即将取消选中 checkbox 时调用 +- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController willUncheckImageAtIndex:(NSInteger)index; + +/// 取消了 checkbox 选中之后调用 +- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController didUncheckImageAtIndex:(NSInteger)index; + +/** + * 取消选择图片后被调用 + */ +- (void)imagePickerViewControllerDidCancel:(QMUIImagePickerViewController *)imagePickerViewController; + +/** + * 即将需要显示 Loading 时调用 + * + * @see shouldShowDefaultLoadingView + */ +- (void)imagePickerViewControllerWillStartLoading:(QMUIImagePickerViewController *)imagePickerViewController; + +/** + * 即将需要隐藏 Loading 时调用 + * + * @see shouldShowDefaultLoadingView + */ +- (void)imagePickerViewControllerDidFinishLoading:(QMUIImagePickerViewController *)imagePickerViewController; + +@end + + +@interface QMUIImagePickerViewController : QMUICommonViewController + +@property(nullable, nonatomic, weak) id imagePickerViewControllerDelegate; + +/* + * 图片的最小尺寸,布局时如果有剩余空间,会将空间分配给图片大小,所以最终显示出来的大小不一定等于minimumImageWidth。默认是75。 + * @warning collectionViewLayout 和 collectionView 可能有设置 sectionInsets 和 contentInsets,所以设置几行不可以简单的通过 screenWdith / columnCount 来获得 + */ +@property(nonatomic, assign) CGFloat minimumImageWidth UI_APPEARANCE_SELECTOR; + +@property(nullable, nonatomic, strong, readonly) UICollectionViewFlowLayout *collectionViewLayout; +@property(nullable, nonatomic, strong, readonly) UICollectionView *collectionView; + +@property(nullable, nonatomic, strong, readonly) UIView *operationToolBarView; +@property(nullable, nonatomic, strong, readonly) QMUIButton *previewButton; +@property(nullable, nonatomic, strong, readonly) QMUIButton *sendButton; +@property(nullable, nonatomic, strong, readonly) UILabel *imageCountLabel; + +/// 也可以直接传入 QMUIAssetsGroup,然后读取其中的 QMUIAsset 并储存到 imagesAssetArray 中,传入后会赋值到 QMUIAssetsGroup,并自动刷新 UI 展示 +- (void)refreshWithAssetsGroup:(QMUIAssetsGroup * _Nullable)assetsGroup; + +@property(nullable, nonatomic, strong, readonly) NSMutableArray *imagesAssetArray; +@property(nullable, nonatomic, strong, readonly) QMUIAssetsGroup *assetsGroup; + +/// 当前被选择的图片对应的 QMUIAsset 对象数组 +@property(nullable, nonatomic, strong, readonly) NSMutableArray *selectedImageAssetArray; + +/// 是否允许图片多选,默认为 YES。如果为 NO,则不显示 checkbox 和底部工具栏。 +@property(nonatomic, assign) BOOL allowsMultipleSelection; + +/// 最多可以选择的图片数,默认为无符号整形数的最大值,相当于没有限制 +@property(nonatomic, assign) NSUInteger maximumSelectImageCount; + +/// 最少需要选择的图片数,默认为 0 +@property(nonatomic, assign) NSUInteger minimumSelectImageCount; + +/// 选择图片超出最大图片限制时 alertView 的标题 +@property(nullable, nonatomic, copy) NSString *alertTitleWhenExceedMaxSelectImageCount; + +/// 选择图片超出最大图片限制时 alertView 底部按钮的标题 +@property(nullable, nonatomic, copy) NSString *alertButtonTitleWhenExceedMaxSelectImageCount; + +/** + * 加载相册列表时会出现 loading,若需要自定义 loading 的形式,可将该属性置为 NO,默认为 YES。 + * @see imagePickerViewControllerWillStartLoading: & imagePickerViewControllerDidFinishLoading: + */ +@property(nonatomic, assign) BOOL shouldShowDefaultLoadingView; + +@end + + +@interface QMUIImagePickerViewController (UIAppearance) + ++ (instancetype)appearance; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m new file mode 100644 index 00000000..d1bd78c5 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m @@ -0,0 +1,566 @@ +/** + * 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. + */ + +// +// QMUIImagePickerViewController.m +// qmui +// +// Created by QMUI Team on 15/5/2. +// + +#import "QMUIImagePickerViewController.h" +#import "QMUICore.h" +#import "QMUIImagePickerCollectionViewCell.h" +#import "QMUIButton.h" +#import "QMUINavigationButton.h" +#import "QMUIAssetsManager.h" +#import "QMUIAlertController.h" +#import "QMUIImagePickerHelper.h" +#import "QMUIImagePickerHelper.h" +#import "UICollectionView+QMUI.h" +#import "UIScrollView+QMUI.h" +#import "CALayer+QMUI.h" +#import "UIView+QMUI.h" +#import +#import "QMUIEmptyView.h" +#import "UIViewController+QMUI.h" +#import "QMUILog.h" +#import "QMUIAppearance.h" + +static NSString * const kVideoCellIdentifier = @"video"; +static NSString * const kImageOrUnknownCellIdentifier = @"imageorunknown"; + + +#pragma mark - QMUIImagePickerViewController (UIAppearance) + +@implementation QMUIImagePickerViewController (UIAppearance) + ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self initAppearance]; + }); +} + ++ (void)initAppearance { + QMUIImagePickerViewController.appearance.minimumImageWidth = 75; +} + +@end + +#pragma mark - QMUIImagePickerViewController + +@interface QMUIImagePickerViewController () + +@property(nonatomic, strong) QMUIImagePickerPreviewViewController *imagePickerPreviewViewController; +@property(nonatomic, assign) BOOL isImagesAssetLoaded;// 这个属性的作用描述:https://github.com/Tencent/QMUI_iOS/issues/219 +@property(nonatomic, assign) BOOL hasScrollToInitialPosition; +@property(nonatomic, assign) BOOL canScrollToInitialPosition;// 要等数据加载完才允许滚动 +@end + +@implementation QMUIImagePickerViewController + +- (void)didInitialize { + [super didInitialize]; + + [self qmui_applyAppearance]; + + _allowsMultipleSelection = YES; + _maximumSelectImageCount = INT_MAX; + _minimumSelectImageCount = 0; + _shouldShowDefaultLoadingView = YES; +} + +- (void)dealloc { + _collectionView.dataSource = nil; + _collectionView.delegate = nil; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.view.backgroundColor = UIColorWhite; + [self.view addSubview:self.collectionView]; + if (self.allowsMultipleSelection) { + [self.view addSubview:self.operationToolBarView]; + } +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithTitle:@"取消" target:self action:@selector(handleCancelPickerImage:)]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + // 由于被选中的图片 selectedImageAssetArray 是 property,所以可以由外部改变, + // 因此 viewWillAppear 时检查一下图片被选中的情况,并刷新 collectionView + if (self.allowsMultipleSelection) { + // 只有允许多选,即底部工具栏显示时,需要重新设置底部工具栏的元素 + NSInteger selectedImageCount = [self.selectedImageAssetArray count]; + if (selectedImageCount > 0) { + // 如果有图片被选择,则预览按钮和发送按钮可点击,并刷新当前被选中的图片数量 + self.previewButton.enabled = YES; + self.sendButton.enabled = YES; + self.imageCountLabel.text = [NSString stringWithFormat:@"%@", @(selectedImageCount)]; + self.imageCountLabel.hidden = NO; + } else { + // 如果没有任何图片被选择,则预览和发送按钮不可点击,并且隐藏显示图片数量的 Label + self.previewButton.enabled = NO; + self.sendButton.enabled = NO; + self.imageCountLabel.hidden = YES; + } + } + [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 先滚到列表顶部然后跳到列表底部) +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + CGFloat operationToolBarViewHeight = 0; + if (self.allowsMultipleSelection) { + operationToolBarViewHeight = ToolBarHeight; + CGFloat toolbarPaddingHorizontal = 12; + self.operationToolBarView.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - operationToolBarViewHeight, CGRectGetWidth(self.view.bounds), operationToolBarViewHeight); + self.previewButton.frame = CGRectSetXY(self.previewButton.frame, toolbarPaddingHorizontal, CGFloatGetCenter(CGRectGetHeight(self.operationToolBarView.bounds) - SafeAreaInsetsConstantForDeviceWithNotch.bottom, CGRectGetHeight(self.previewButton.frame))); + self.sendButton.frame = CGRectMake(CGRectGetWidth(self.operationToolBarView.bounds) - toolbarPaddingHorizontal - CGRectGetWidth(self.sendButton.frame), CGFloatGetCenter(CGRectGetHeight(self.operationToolBarView.frame) - SafeAreaInsetsConstantForDeviceWithNotch.bottom, CGRectGetHeight(self.sendButton.frame)), CGRectGetWidth(self.sendButton.frame), CGRectGetHeight(self.sendButton.frame)); + CGSize imageCountLabelSize = CGSizeMake(18, 18); + self.imageCountLabel.frame = CGRectMake(CGRectGetMinX(self.sendButton.frame) - imageCountLabelSize.width - 5, CGRectGetMinY(self.sendButton.frame) + CGFloatGetCenter(CGRectGetHeight(self.sendButton.frame), imageCountLabelSize.height), imageCountLabelSize.width, imageCountLabelSize.height); + self.imageCountLabel.layer.cornerRadius = CGRectGetHeight(self.imageCountLabel.bounds) / 2; + operationToolBarViewHeight = CGRectGetHeight(self.operationToolBarView.frame); + } + + if (!CGSizeEqualToSize(self.collectionView.frame.size, self.view.bounds.size)) { + self.collectionView.frame = self.view.bounds; + } + 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); + // 放在这里是因为有时候会先走完 refreshWithAssetsGroup 里的 completion 再走到这里,此时前者不会导致 scollToInitialPosition 的滚动,所以在这里再调用一次保证一定会滚 + [self scrollToInitialPositionIfNeeded]; + } +} + +- (void)refreshWithAssetsGroup:(QMUIAssetsGroup *)assetsGroup { + _assetsGroup = assetsGroup; + if (!self.imagesAssetArray) { + _imagesAssetArray = [[NSMutableArray alloc] init]; + _selectedImageAssetArray = [[NSMutableArray alloc] init]; + } else { + [self.imagesAssetArray removeAllObjects]; + // 这里不用 remove 选中的图片,因为支持跨相簿选图 +// [self.selectedImageAssetArray removeAllObjects]; + } + // 通过 QMUIAssetsGroup 获取该相册所有的图片 QMUIAsset,并且储存到数组中 + QMUIAlbumSortType albumSortType = QMUIAlbumSortTypePositive; + // 从 delegate 中获取相册内容的排序方式,如果没有实现这个 delegate,则使用 QMUIAlbumSortType 的默认值,即最新的内容排在最后面 + if (self.imagePickerViewControllerDelegate && [self.imagePickerViewControllerDelegate respondsToSelector:@selector(albumSortTypeForImagePickerViewController:)]) { + albumSortType = [self.imagePickerViewControllerDelegate albumSortTypeForImagePickerViewController:self]; + } + // 遍历相册内的资源较为耗时,交给子线程去处理,因此这里需要显示 Loading + if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewControllerWillStartLoading:)]) { + [self.imagePickerViewControllerDelegate imagePickerViewControllerWillStartLoading:self]; + } + if (self.shouldShowDefaultLoadingView) { + [self showEmptyViewWithLoading]; + } + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [assetsGroup enumerateAssetsWithOptions:albumSortType usingBlock:^(QMUIAsset *resultAsset) { + // 这里需要对 UI 进行操作,因此放回主线程处理 + dispatch_async(dispatch_get_main_queue(), ^{ + if (resultAsset) { + self.isImagesAssetLoaded = NO; + [self.imagesAssetArray addObject:resultAsset]; + } else { + // result 为 nil,即遍历相片或视频完毕 + self.isImagesAssetLoaded = YES;// 这个属性的作用描述: https://github.com/Tencent/QMUI_iOS/issues/219 + [self.collectionView reloadData]; + [self.collectionView performBatchUpdates:^{ + } completion:^(BOOL finished) { + [self scrollToInitialPositionIfNeeded]; + if (self.shouldShowDefaultLoadingView) { + [self hideEmptyView]; + } + if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewControllerDidFinishLoading:)]) { + [self.imagePickerViewControllerDelegate imagePickerViewControllerDidFinishLoading:self]; + } + }]; + } + }); + }]; + }); +} + +- (void)initPreviewViewControllerIfNeeded { + if (!self.imagePickerPreviewViewController) { + self.imagePickerPreviewViewController = [self.imagePickerViewControllerDelegate imagePickerPreviewViewControllerForImagePickerViewController:self]; + self.imagePickerPreviewViewController.maximumSelectImageCount = self.maximumSelectImageCount; + self.imagePickerPreviewViewController.minimumSelectImageCount = self.minimumSelectImageCount; + } +} + +- (CGSize)referenceImageSize { + CGFloat collectionViewWidth = CGRectGetWidth(self.collectionView.bounds); + CGFloat collectionViewContentSpacing = collectionViewWidth - UIEdgeInsetsGetHorizontalValue(self.collectionView.contentInset) - UIEdgeInsetsGetHorizontalValue(self.collectionViewLayout.sectionInset); + NSInteger columnCount = floor(collectionViewContentSpacing / self.minimumImageWidth); + CGFloat referenceImageWidth = self.minimumImageWidth; + BOOL isSpacingEnoughWhenDisplayInMinImageSize = (self.minimumImageWidth + self.collectionViewLayout.minimumInteritemSpacing) * columnCount - self.collectionViewLayout.minimumInteritemSpacing <= collectionViewContentSpacing; + if (!isSpacingEnoughWhenDisplayInMinImageSize) { + // 算上图片之间的间隙后发现其实还是放不下啦,所以得把列数减少,然后放大图片以撑满剩余空间 + columnCount -= 1; + } + referenceImageWidth = floor((collectionViewContentSpacing - self.collectionViewLayout.minimumInteritemSpacing * (columnCount - 1)) / columnCount); + return CGSizeMake(referenceImageWidth, referenceImageWidth); +} + +- (void)setMinimumImageWidth:(CGFloat)minimumImageWidth { + _minimumImageWidth = minimumImageWidth; + [self referenceImageSize]; + [self.collectionView.collectionViewLayout invalidateLayout]; +} + +- (void)scrollToInitialPositionIfNeeded { + if (_collectionView.qmui_visible && self.isImagesAssetLoaded && !self.hasScrollToInitialPosition) { + if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(albumSortTypeForImagePickerViewController:)] && [self.imagePickerViewControllerDelegate albumSortTypeForImagePickerViewController:self] == QMUIAlbumSortTypeReverse) { + [_collectionView qmui_scrollToTop]; + } else { + [_collectionView qmui_scrollToBottom]; + } + self.hasScrollToInitialPosition = YES; + } +} + +#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 - + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + return 1; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return [self.imagesAssetArray count]; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + return [self referenceImageSize]; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item]; + + NSString *identifier = nil; + if (imageAsset.assetType == QMUIAssetTypeVideo) { + identifier = kVideoCellIdentifier; + } else { + identifier = kImageOrUnknownCellIdentifier; + } + QMUIImagePickerCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath]; + [cell renderWithAsset:imageAsset referenceSize:[self referenceImageSize]]; + + [cell.checkboxButton addTarget:self action:@selector(handleCheckBoxButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + cell.selectable = self.allowsMultipleSelection; + if (cell.selectable) { + // 如果该图片的 QMUIAsset 被包含在已选择图片的数组中,则控制该图片被选中 + cell.checked = [self.selectedImageAssetArray containsObject:imageAsset]; + } + return cell; +} + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + QMUIAsset *imageAsset = self.imagesAssetArray[indexPath.item]; + if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:didSelectImageWithImagesAsset:afterImagePickerPreviewViewControllerUpdate:)]) { + [self.imagePickerViewControllerDelegate imagePickerViewController:self didSelectImageWithImagesAsset:imageAsset afterImagePickerPreviewViewControllerUpdate:self.imagePickerPreviewViewController]; + } + if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerPreviewViewControllerForImagePickerViewController:)]) { + [self initPreviewViewControllerIfNeeded]; + if (!self.allowsMultipleSelection) { + // 单选的情况下 + [self.imagePickerPreviewViewController updateImagePickerPreviewViewWithImagesAssetArray:@[imageAsset].mutableCopy + selectedImageAssetArray:nil + currentImageIndex:0 + singleCheckMode:YES]; + } else { + // cell 处于编辑状态,即图片允许多选 + [self.imagePickerPreviewViewController updateImagePickerPreviewViewWithImagesAssetArray:self.imagesAssetArray + selectedImageAssetArray:self.selectedImageAssetArray + currentImageIndex:indexPath.item + singleCheckMode:NO]; + } + [self.navigationController pushViewController:self.imagePickerPreviewViewController animated:YES]; + } +} + +#pragma mark - 按钮点击回调 + +- (void)handleSendButtonClick:(id)sender { + if (self.imagePickerViewControllerDelegate && [self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:didFinishPickingImageWithImagesAssetArray:)]) { + [self.imagePickerViewControllerDelegate imagePickerViewController:self didFinishPickingImageWithImagesAssetArray:self.selectedImageAssetArray]; + } + [self.selectedImageAssetArray removeAllObjects]; + [self dismissViewControllerAnimated:YES completion:NULL]; +} + +- (void)handlePreviewButtonClick:(id)sender { + [self initPreviewViewControllerIfNeeded]; + // 手工更新图片预览界面 + [self.imagePickerPreviewViewController updateImagePickerPreviewViewWithImagesAssetArray:[self.selectedImageAssetArray copy] + selectedImageAssetArray:self.selectedImageAssetArray + currentImageIndex:0 + singleCheckMode:NO]; + [self.navigationController pushViewController:self.imagePickerPreviewViewController animated:YES]; +} + +- (void)handleCancelPickerImage:(id)sender { + [self dismissViewControllerAnimated:YES completion:^() { + if (self.imagePickerViewControllerDelegate && [self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewControllerDidCancel:)]) { + [self.imagePickerViewControllerDelegate imagePickerViewControllerDidCancel:self]; + } + [self.selectedImageAssetArray removeAllObjects]; + }]; +} + +- (void)handleCheckBoxButtonClick:(UIButton *)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 *)[_collectionView cellForItemAtIndexPath:indexPath]; + QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item]; + if (cell.checked) { + // 移除选中状态 + if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:willUncheckImageAtIndex:)]) { + [self.imagePickerViewControllerDelegate imagePickerViewController:self willUncheckImageAtIndex:indexPath.item]; + } + + cell.checked = NO; + [self.selectedImageAssetArray removeObject:imageAsset]; + + if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:didUncheckImageAtIndex:)]) { + [self.imagePickerViewControllerDelegate imagePickerViewController:self didUncheckImageAtIndex:indexPath.item]; + } + + // 根据选择图片数控制预览和发送按钮的 enable,以及修改已选中的图片数 + [self updateImageCountAndCheckLimited]; + } else { + // 选中该资源 + if ([self.selectedImageAssetArray count] >= _maximumSelectImageCount) { + if (!_alertTitleWhenExceedMaxSelectImageCount) { + _alertTitleWhenExceedMaxSelectImageCount = [NSString stringWithFormat:@"你最多只能选择%@张图片", @(_maximumSelectImageCount)]; + } + if (!_alertButtonTitleWhenExceedMaxSelectImageCount) { + _alertButtonTitleWhenExceedMaxSelectImageCount = [NSString stringWithFormat:@"我知道了"]; + } + + QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:_alertTitleWhenExceedMaxSelectImageCount message:nil preferredStyle:QMUIAlertControllerStyleAlert]; + [alertController addAction:[QMUIAlertAction actionWithTitle:_alertButtonTitleWhenExceedMaxSelectImageCount style:QMUIAlertActionStyleCancel handler:nil]]; + [alertController showWithAnimated:YES]; + return; + } + + if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:willCheckImageAtIndex:)]) { + [self.imagePickerViewControllerDelegate imagePickerViewController:self willCheckImageAtIndex:indexPath.item]; + } + + cell.checked = YES; + [self.selectedImageAssetArray addObject:imageAsset]; + + if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:didCheckImageAtIndex:)]) { + [self.imagePickerViewControllerDelegate imagePickerViewController:self didCheckImageAtIndex:indexPath.item]; + } + + // 根据选择图片数控制预览和发送按钮的 enable,以及修改已选中的图片数 + [self updateImageCountAndCheckLimited]; + + // 发出请求获取大图,如果图片在 iCloud,则会发出网络请求下载图片。这里同时保存请求 id,供取消请求使用 + [self requestImageWithIndexPath:indexPath]; + } +} + +- (void)updateImageCountAndCheckLimited { + NSInteger selectedImageCount = [self.selectedImageAssetArray count]; + if (selectedImageCount > 0 && selectedImageCount >= _minimumSelectImageCount) { + self.previewButton.enabled = YES; + self.sendButton.enabled = YES; + self.imageCountLabel.text = [NSString stringWithFormat:@"%@", @(selectedImageCount)]; + self.imageCountLabel.hidden = NO; + [QMUIImagePickerHelper springAnimationOfImageSelectedCountChangeWithCountLabel:self.imageCountLabel]; + } else { + self.previewButton.enabled = NO; + self.sendButton.enabled = NO; + self.imageCountLabel.hidden = YES; + } +} + +#pragma mark - Request Image + +- (void)requestImageWithIndexPath:(NSIndexPath *)indexPath { + // 发出请求获取大图,如果图片在 iCloud,则会发出网络请求下载图片。这里同时保存请求 id,供取消请求使用 + QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item]; + 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]); + + if (downloadSucceed) { + // 资源资源已经在本地或下载成功 + [imageAsset updateDownloadStatusWithDownloadResult:YES]; + cell.downloadStatus = QMUIAssetDownloadStatusSucceed; + + } else if ([info objectForKey:PHImageErrorKey] ) { + // 下载错误 + [imageAsset updateDownloadStatusWithDownloadResult:NO]; + cell.downloadStatus = QMUIAssetDownloadStatusFailed; + } + + } withProgressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { + imageAsset.downloadProgress = progress; + + dispatch_async(dispatch_get_main_queue(), ^{ + if ([self.collectionView qmui_itemVisibleAtIndexPath:indexPath]) { + + QMUILogInfo(@"QMUIImagePickerLibrary", @"Download iCloud image, current progress is : %f", progress); + + if (cell.downloadStatus != QMUIAssetDownloadStatusDownloading) { + cell.downloadStatus = QMUIAssetDownloadStatusDownloading; + // 预先设置预览界面的下载状态 + self.imagePickerPreviewViewController.downloadStatus = QMUIAssetDownloadStatusDownloading; + } + if (error) { + QMUILog(@"QMUIImagePickerLibrary", @"Download iCloud image Failed, current progress is: %f", progress); + cell.downloadStatus = QMUIAssetDownloadStatusFailed; + } + } + }); + }]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h new file mode 100644 index 00000000..a0b3b796 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h @@ -0,0 +1,35 @@ +/** + * 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+Transition.h +// qmui +// +// Created by QMUI Team on 11/25/16. +// + +#import + +@interface UINavigationBar (Transition) + +/// 用来模仿真的navBar,配合 UINavigationController+NavigationBarTransition 在转场过程中存在的一条假navBar +@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 new file mode 100644 index 00000000..3e1c5ea7 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m @@ -0,0 +1,281 @@ +/** + * 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+Transition.m +// qmui +// +// Created by QMUI Team on 11/25/16. +// + +#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) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + 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; + } + }; + }); + + 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]; + } + }; + }); + + 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 new file mode 100644 index 00000000..c42103e8 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.h @@ -0,0 +1,24 @@ +/** + * 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. + */ + +// +// UINavigationController+NavigationBarTransition.h +// qmui +// +// Created by QMUI Team on 16/2/22. +// + +#import +#import + +/** + * 因为系统的UINavigationController只有一个navBar,所以会导致在切换controller的时候,如果两个controller的navBar状态不一致(包括backgroundImgae、shadowImage、barTintColor等等),就会导致在刚要切换的瞬间,navBar的状态都立马变成下一个controller所设置的样式了,为了解决这种情况,QMUI给出了一个方案,有四个方法可以决定你在转场的时候要不要使用自定义的navBar来模仿真实的navBar。 + */ +@interface UINavigationController (NavigationBarTransition) + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m new file mode 100644 index 00000000..21982331 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m @@ -0,0 +1,605 @@ +/** + * 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. + */ + +// +// UINavigationController+NavigationBarTransition.m +// qmui +// +// Created by QMUI Team on 16/2/22. +// + +#import "UINavigationController+NavigationBarTransition.h" +#import "QMUINavigationController.h" +#import "QMUICore.h" +#import "UINavigationController+QMUI.h" +#import "UIImage+QMUI.h" +#import "UIViewController+QMUI.h" +#import "UINavigationBar+Transition.h" +#import "QMUINavigationTitleView.h" +#import "UINavigationBar+QMUI.h" +#import "UINavigationBar+QMUIBarProtocol.h" +#import "UIView+QMUI.h" +#import "QMUILog.h" + +/** + * 为了响应NavigationBarTransition分类的功能,UIViewController需要做一些相应的支持。 + * @see UINavigationController+NavigationBarTransition.h + */ +@interface UIViewController (NavigationBarTransition) + +@property(nonatomic, assign) BOOL qmuinb_shouldShowTransitionBar; + +/// 用来模仿真的navBar的,在转场过程中存在的一条假navBar +@property(nonatomic, strong) _QMUITransitionNavigationBar *transitionNavigationBar; + +/// 是否要把真的navBar隐藏 +@property(nonatomic, assign) BOOL prefersNavigationBarBackgroundViewHidden; + +/// 原始containerView的背景色 +@property(nonatomic, strong) UIColor *originContainerViewBackgroundColor; + +@end + +@interface UILabel (NavigationBarTransition) +@property(nonatomic, strong) UIColor *qmui_specifiedTextColor; +@end + +@implementation UILabel (NavigationBarTransition) + +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 + + +@implementation UIViewController (NavigationBarTransition) + +#pragma mark - 主流程 + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + 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) { + + // 左右两个界面都必须存在 + UIViewController *disappearingViewController = disappearingViewControllers.lastObject; + if (!appearingViewController || !disappearingViewController) { + return; + } + + 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; + } + } + 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, 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(viewWillAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject, BOOL firstArgv) { + + // 放在最前面,留一个时机给业务可以覆盖 + [selfObject renderNavigationBarStyleAnimated:firstArgv]; + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + + OverrideImplementation([UIViewController class], @selector(viewWillLayoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject) { + [selfObject.transitionNavigationBar updateLayout]; + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + 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)addTransitionNavigationBarAndBindNavigationBar:(BOOL)shouldBind { + // add 时虽然过滤了 navigationBarHidden 的条件,但可能在 push/pop 时,新界面暂时还没刷新导航栏的显隐状态,所以还是需要在 viewWillLayoutSubviews 那边再重新根据 navigationBarHidden 的值来决定是否隐藏假 bar + if (!self.qmuinb_shouldShowTransitionBar || self.transitionNavigationBar || !self.navigationController.navigationBar || self.navigationController.navigationBarHidden) { + return; + } + + _QMUITransitionNavigationBar *customBar = [[_QMUITransitionNavigationBar alloc] init]; + customBar.parentViewController = self; + self.transitionNavigationBar = customBar; + + // 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; + } + } + [self.view addSubview:customBar]; + customBar.originalNavigationBar = self.navigationController.navigationBar;// 注意这里内部不会保留真 bar 和假 bar 的 copy 关系 + if (shouldBind) { + self.navigationController.navigationBar.qmuinb_copyStylesToBar = customBar; + } +} + +- (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 { + + // 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController + if (![self.navigationController.viewControllers containsObject:self]) { + return; + } + + if (![self conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) { + return; + } + + // 以下用于控制 vc 的外观样式,如果某个方法有实现则用方法的返回值,否则再看配置表对应的值是否有配置,有配置就使用配置表,没配置则什么都不做,维持系统原生样式 + UIViewController *vc = (UIViewController *)self; + UINavigationController *navigationController = vc.navigationController; + + // 显示/隐藏 导航栏 + if ([vc canCustomNavigationBarTransitionIfBarHiddenable]) { + if ([vc hideNavigationBarWhenTransitioning]) { + if (!navigationController.isNavigationBarHidden) { + [navigationController setNavigationBarHidden:YES animated:animated]; + } + } else { + if (navigationController.isNavigationBarHidden) { + [navigationController setNavigationBarHidden:NO animated:animated]; + } + } + } + + // 仅当导航栏被添加到 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)renderNavigationBarAppearanceAnimated:(BOOL)animated { + + // 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController + if (![self.navigationController.viewControllers containsObject:self]) { + return; + } + + if (![self conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) { + return; + } + + // 以下用于控制 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]; +} + +// 仅处理导航栏标题 +- (void)renderNavigationBarTitleAppearanceAnimated:(BOOL)animated { + + // 屏蔽不处于 UINavigationController 里的 viewController,以及 custom containerViewController 里的 childViewController + if (![self.navigationController.viewControllers containsObject:self]) { + return; + } + + if (![self conformsToProtocol:@protocol(QMUINavigationControllerAppearanceDelegate)]) { + return; + } + + // 以下用于控制 vc 的外观样式,如果某个方法有实现则用方法的返回值,否则再看配置表对应的值是否有配置,有配置就使用配置表,没配置则什么都不做,维持系统原生样式 + 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 { + // 设置了自定义的 navigationItem.titleView,则不处理 + } + } 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 { + // 设置了自定义的 navigationItem.titleView,则不处理 + } + } +} + +- (BOOL)respondCustomNavigationBarTransitionIfBarHiddenable { + BOOL respondIfBarHiddenable = NO; + + // 如果当前界面正在搜索,由于 UISearchController 会自动把 navigationBar 移上去,所以这种时候 QMUI 就不应该再去操作 bar 的显隐了 + if ([self.presentedViewController isKindOfClass:[UISearchController class]] && ((UISearchController *)self.presentedViewController).hidesNavigationBarDuringPresentation) { + return NO; + } + + if ([self conformsToProtocol:@protocol(QMUICustomNavigationBarTransitionDelegate)]) { + UIViewController *vc = (UIViewController *)self; + if ([vc respondsToSelector:@selector(shouldCustomizeNavigationBarTransitionIfHideable)]) { + respondIfBarHiddenable = YES; + } + } + return respondIfBarHiddenable; +} + +- (BOOL)respondCustomNavigationBarTransitionWithBarHiddenState { + BOOL respondWithBarHidden = NO; + if ([self conformsToProtocol:@protocol(QMUICustomNavigationBarTransitionDelegate)]) { + UIViewController *vc = (UIViewController *)self; + if ([vc respondsToSelector:@selector(preferredNavigationBarHidden)]) { + respondWithBarHidden = YES; + } + } + return respondWithBarHidden; +} + +- (BOOL)canCustomNavigationBarTransitionIfBarHiddenable { + if ([self respondCustomNavigationBarTransitionIfBarHiddenable]) { + UIViewController *vc = (UIViewController *)self; + return [vc shouldCustomizeNavigationBarTransitionIfHideable]; + } + return NO; +} + +- (BOOL)hideNavigationBarWhenTransitioning { + if ([self respondCustomNavigationBarTransitionWithBarHiddenState]) { + UIViewController *vc = (UIViewController *)self; + BOOL hidden = [vc preferredNavigationBarHidden]; + return hidden; + } + return NO; +} + +// 对于有一个界面隐藏了导航栏的情况,我们也要做自定义的动画去干预,因为如果左右两个界面导航栏样式不同,你不去干预的话,push/pop 瞬间导航栏会变成即将显示的那个界面的样式,这不符合预期 +- (BOOL)shouldCustomTransitionAutomaticallyForOperation:(UINavigationControllerOperation)operation firstViewController:(UIViewController *)viewController1 secondViewController:(UIViewController *)viewController2 { + + UIViewController *vc1 = (UIViewController *)viewController1; + UIViewController *vc2 = (UIViewController *)viewController2; + + if (![vc1 conformsToProtocol:@protocol(QMUINavigationControllerDelegate)] || ![vc2 conformsToProtocol:@protocol(QMUINavigationControllerDelegate)]) { + 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; + BOOL result = (key1 || key2) && ![key1 isEqualToString:key2]; + return result; + } + + if (!AutomaticCustomNavigationBarTransitionStyle) { + return NO; + } + + + + 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;// 一个有一个没有,则需要自定义 + } + if (![bg1.qmui_averageColor isEqual:bg2.qmui_averageColor]) { + return YES;// 目前只能判断图片颜色是否相等了 + } + } + + // 如果存在 backgroundImage,则 barTintColor、barStyle 就算存在也不会被显示出来,所以这里只判断两个 backgroundImage 都不存在的时候 + if (!bg1 && !bg2) { + 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; + } + if (![barTintColor1 isEqual:barTintColor2]) { + 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(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; + } + if (![shadowImage1.qmui_averageColor isEqual:shadowImage2.qmui_averageColor]) { + return YES; + } + } + + return NO; +} + +- (UIColor *)containerViewBackgroundColor { + if ([self conformsToProtocol:@protocol(QMUICustomNavigationBarTransitionDelegate)]) { + UIViewController *vc = (UIViewController *)self; + if ([vc respondsToSelector:@selector(containerViewBackgroundColorWhenTransitioning)]) { + return [vc containerViewBackgroundColorWhenTransitioning]; + } + } + return self.isViewLoaded && self.view.backgroundColor ? self.view.backgroundColor : UIColorWhite; +} + +#pragma mark - Setter / Getter + +QMUISynthesizeIdStrongProperty(transitionNavigationBar, setTransitionNavigationBar) +QMUISynthesizeIdStrongProperty(originContainerViewBackgroundColor, setOriginContainerViewBackgroundColor) + +static char kAssociatedObjectKey_backgroundViewHidden; +- (void)setPrefersNavigationBarBackgroundViewHidden:(BOOL)prefersNavigationBarBackgroundViewHidden { + // 从某个版本开始,发现从有 navBar 的界面返回无 navBar 的界面,backgroundView 会跑出来,发现是被系统重新设置了显示,所以改用其他的方法来隐藏 backgroundView,就是 mask。 + if (prefersNavigationBarBackgroundViewHidden) { + self.navigationController.navigationBar.qmui_backgroundView.layer.mask = [CALayer layer]; + } else { + self.navigationController.navigationBar.qmui_backgroundView.layer.mask = nil; + } + objc_setAssociatedObject(self, &kAssociatedObjectKey_backgroundViewHidden, @(prefersNavigationBarBackgroundViewHidden), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (BOOL)prefersNavigationBarBackgroundViewHidden { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_backgroundViewHidden)) boolValue]; +} + +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; + } + } +} + +- (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 new file mode 100644 index 00000000..ca1a82be --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.h @@ -0,0 +1,305 @@ +/** + * 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. + */ + +// +// QMUIAlertController.h +// qmui +// +// Created by QMUI Team on 15/7/20. +// + +#import +#import "QMUIModalPresentationViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIButton; +@class QMUITextField; +@class QMUIAlertController; + + +typedef NS_ENUM(NSInteger, QMUIAlertActionStyle) { + QMUIAlertActionStyleDefault = 0, + QMUIAlertActionStyleCancel, + QMUIAlertActionStyleDestructive +}; + +typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) { + QMUIAlertControllerStyleActionSheet = 0, + QMUIAlertControllerStyleAlert +}; + + +@protocol QMUIAlertControllerDelegate + +@optional + +- (void)willShowAlertController:(QMUIAlertController *)alertController; +- (void)willHideAlertController:(QMUIAlertController *)alertController; +- (void)didShowAlertController:(QMUIAlertController *)alertController; +- (void)didHideAlertController:(QMUIAlertController *)alertController; +- (BOOL)shouldHideAlertController:(QMUIAlertController *)alertController; + +@end + + +/** + * QMUIAlertController的按钮,初始化完通过`QMUIAlertController`的`addAction:`方法添加到 AlertController 上即可。 + */ +@interface QMUIAlertAction : NSObject + +/** + * 初始化`QMUIAlertController`的按钮 + * + * @param title 按钮标题 + * @param style 按钮style,跟系统一样,有 Default、Cancel、Destructive 三种类型 + * @param handler 处理点击事件的block,注意 QMUIAlertAction 点击后必定会隐藏 alertController,不需要手动在 handler 里 hide + * + * @return QMUIAlertController按钮的实例 + */ ++ (instancetype)actionWithTitle:(nullable NSString *)title style:(QMUIAlertActionStyle)style handler:(nullable void (^)(__kindof QMUIAlertController *aAlertController, QMUIAlertAction *action))handler; + +/// `QMUIAlertAction`对应的 button 对象 +@property(nonatomic, strong, readonly) QMUIButton *button; + +/// `QMUIAlertAction`对应的标题 +@property(nullable, nonatomic, copy, readonly) NSString *title; + +/// `QMUIAlertAction`对应的样式 +@property(nonatomic, assign, readonly) QMUIAlertActionStyle style; + +/// `QMUIAlertAction`是否允许操作 +@property(nonatomic, assign, getter=isEnabled) BOOL enabled; + +/// `QMUIAlertAction`按钮样式,默认nil。当此值为nil的时候,则使用`QMUIAlertController`的`alertButtonAttributes`或者`sheetButtonAttributes`的值。 +@property(nullable, nonatomic, strong) NSDictionary *buttonAttributes; + +/// 原理同上`buttonAttributes` +@property(nullable, nonatomic, strong) NSDictionary *buttonDisabledAttributes; + +@end + + +/** + * `QMUIAlertController`是模仿系统`UIAlertController`的控件,所以系统有的功能在QMUIAlertController里面基本都有。同时`QMUIAlertController`还提供了一些扩展功能,例如:它的每个 button 都是开放出来的,可以对默认的按钮进行二次处理(比如加一个图片);可以通过 appearance 在 app 启动的时候修改整个`QMUIAlertController`的主题样式。 + */ +@interface QMUIAlertController : UIViewController { + UIView *_containerView; // 弹窗的主体容器 + UIView *_scrollWrapView; // 包含上下两个 scrollView 的容器 + UIScrollView *_headerScrollView; // 上半部分的内容的 scrollView,例如 title、message + UIScrollView *_buttonScrollView; // 所有按钮的容器,特别的,actionSheet 下的取消按钮不放在这里面,因为它不参与滚动 + UIControl *_dimmingView; // 背后占满整个屏幕的半透明黑色遮罩 +} + +/// alert距离屏幕四边的间距,默认UIEdgeInsetsMake(0, 0, 0, 0)。alert的宽度最终是通过屏幕宽度减去水平的 alertContentMargin 和 alertContentMaximumWidth 决定的。 +@property(nonatomic, assign) UIEdgeInsets alertContentMargin UI_APPEARANCE_SELECTOR; + +/// alert的最大宽度,默认270。 +@property(nonatomic, assign) CGFloat alertContentMaximumWidth UI_APPEARANCE_SELECTOR; + +/// alert上分隔线颜色,默认UIColorMake(211, 211, 219)。 +@property(nullable, nonatomic, strong) UIColor *alertSeparatorColor UI_APPEARANCE_SELECTOR; + +/// alert标题样式,默认@{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontBoldMake(17),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]} +@property(nullable, nonatomic, strong) NSDictionary *alertTitleAttributes UI_APPEARANCE_SELECTOR; + +/// alert信息样式,默认@{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]} +@property(nullable, nonatomic, strong) NSDictionary *alertMessageAttributes UI_APPEARANCE_SELECTOR; + +/// alert按钮样式,默认@{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)} +@property(nullable, nonatomic, strong) NSDictionary *alertButtonAttributes UI_APPEARANCE_SELECTOR; + +/// alert按钮disabled时的样式,默认@{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)} +@property(nullable, nonatomic, strong) NSDictionary *alertButtonDisabledAttributes UI_APPEARANCE_SELECTOR; + +/// alert cancel 按钮样式,默认@{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(17),NSKernAttributeName:@(0)} +@property(nullable, nonatomic, strong) NSDictionary *alertCancelButtonAttributes UI_APPEARANCE_SELECTOR; + +/// alert destructive 按钮样式,默认@{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)} +@property(nullable, nonatomic, strong) NSDictionary *alertDestructiveButtonAttributes UI_APPEARANCE_SELECTOR; + +/// alert圆角大小,默认值是 13,以保持与系统默认样式一致 +@property(nonatomic, assign) CGFloat alertContentCornerRadius UI_APPEARANCE_SELECTOR; + +/// alert按钮高度,默认44pt +@property(nonatomic, assign) CGFloat alertButtonHeight UI_APPEARANCE_SELECTOR; + +/// alert头部(非按钮部分)背景色,默认值是:UIColorMakeWithRGBA(247, 247, 247, 1) +@property(nullable, nonatomic, strong) UIColor *alertHeaderBackgroundColor UI_APPEARANCE_SELECTOR; + +/// alert按钮背景色,默认值同`alertHeaderBackgroundColor` +@property(nullable, nonatomic, strong) UIColor *alertButtonBackgroundColor UI_APPEARANCE_SELECTOR; + +/// alert按钮高亮背景色,默认UIColorMake(232, 232, 232) +@property(nullable, nonatomic, strong) UIColor *alertButtonHighlightBackgroundColor UI_APPEARANCE_SELECTOR; + +/// alert头部四边insets间距 +@property(nonatomic, assign) UIEdgeInsets alertHeaderInsets UI_APPEARANCE_SELECTOR; + +/// alert头部title和message之间的间距,默认3pt +@property(nonatomic, assign) CGFloat alertTitleMessageSpacing UI_APPEARANCE_SELECTOR; + +/// alert 内部 textField 的字体 +@property(nullable, nonatomic, strong) UIFont *alertTextFieldFont UI_APPEARANCE_SELECTOR; + +/// alert 内部 textField 的文字颜色 +@property(nullable, nonatomic, strong) UIColor *alertTextFieldTextColor UI_APPEARANCE_SELECTOR; + +/// 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; + +/// sheet的最大宽度,默认值是5.5英寸的屏幕的宽度减去水平的 sheetContentMargin +@property(nonatomic, assign) CGFloat sheetContentMaximumWidth UI_APPEARANCE_SELECTOR; + +/// sheet分隔线颜色,默认UIColorMake(211, 211, 219) +@property(nullable, nonatomic, strong) UIColor *sheetSeparatorColor UI_APPEARANCE_SELECTOR; + +/// sheet标题样式,默认@{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontBoldMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]} +@property(nullable, nonatomic, copy) NSDictionary *sheetTitleAttributes UI_APPEARANCE_SELECTOR; + +/// sheet信息样式,默认@{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]} +@property(nullable, nonatomic, copy) NSDictionary *sheetMessageAttributes UI_APPEARANCE_SELECTOR; + +/// sheet按钮样式,默认@{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)} +@property(nullable, nonatomic, copy) NSDictionary *sheetButtonAttributes UI_APPEARANCE_SELECTOR; + +/// sheet按钮disabled时的样式,默认@{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)} +@property(nullable, nonatomic, copy) NSDictionary *sheetButtonDisabledAttributes UI_APPEARANCE_SELECTOR; + +/// sheet cancel 按钮样式,默认@{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(20),NSKernAttributeName:@(0)} +@property(nullable, nonatomic, copy) NSDictionary *sheetCancelButtonAttributes UI_APPEARANCE_SELECTOR; + +/// sheet destructive 按钮样式,默认@{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)} +@property(nullable, nonatomic, copy) NSDictionary *sheetDestructiveButtonAttributes UI_APPEARANCE_SELECTOR; + +/// sheet cancel 按钮距离其上面元素(按钮或者header)的间距,默认8pt +@property(nonatomic, assign) CGFloat sheetCancelButtonMarginTop UI_APPEARANCE_SELECTOR; + +/// sheet内容的圆角,默认值是 13,以保持与系统默认样式一致 +@property(nonatomic, assign) CGFloat sheetContentCornerRadius UI_APPEARANCE_SELECTOR; + +/// sheet按钮高度,默认值是 57,以保持与系统默认样式一致 +@property(nonatomic, assign) CGFloat sheetButtonHeight UI_APPEARANCE_SELECTOR; + +/// sheet头部(非按钮部分)背景色,默认值是:UIColorMakeWithRGBA(247, 247, 247, 1) +@property(nullable, nonatomic, strong) UIColor *sheetHeaderBackgroundColor UI_APPEARANCE_SELECTOR; + +/// sheet按钮背景色,默认值同`sheetHeaderBackgroundColor` +@property(nullable, nonatomic, strong) UIColor *sheetButtonBackgroundColor UI_APPEARANCE_SELECTOR; + +/// sheet按钮高亮背景色,默认UIColorMake(232, 232, 232) +@property(nullable, nonatomic, strong) UIColor *sheetButtonHighlightBackgroundColor UI_APPEARANCE_SELECTOR; + +/// sheet头部四边insets间距 +@property(nonatomic, assign) UIEdgeInsets sheetHeaderInsets UI_APPEARANCE_SELECTOR; + +/// 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; + +/// 通过类方法初始化实例 ++ (nonnull instancetype)alertControllerWithTitle:(nullable NSString *)title message:(nullable NSString *)message preferredStyle:(QMUIAlertControllerStyle)preferredStyle; + +/// @see `QMUIAlertControllerDelegate` +@property(nullable, nonatomic,weak) iddelegate; + +/// 增加一个按钮 +- (void)addAction:(nonnull QMUIAlertAction *)action; + +// 增加一个“取消”按钮,点击后 alertController 会被 hide +- (void)addCancelAction; + +/// 增加一个输入框 +- (void)addTextFieldWithConfigurationHandler:(void (^_Nullable)(QMUITextField *textField))configurationHandler; + +/// 是否应该自动管理输入框的键盘 Return 事件(切换多个输入框的焦点、自动响应某个按钮等),默认为 YES。你也可以通过 UITextFieldDelegate 自己管理,此时请将此属性置为 NO。 +@property(nonatomic, assign) BOOL shouldManageTextFieldsReturnEventAutomatically; + +/// 增加一个自定义的view作为`QMUIAlertController`的customView +- (void)addCustomView:(UIView *_Nullable)view; + +/// 显示`QMUIAlertController` +- (void)showWithAnimated:(BOOL)animated; + +/// 隐藏`QMUIAlertController` +- (void)hideWithAnimated:(BOOL)animated; + +/// 所有`QMUIAlertAction`对象 +@property(nullable, nonatomic, copy, readonly) NSArray *actions; + +/// 当前所有通过`addTextFieldWithConfigurationHandler:`接口添加的输入框 +@property(nullable, nonatomic, copy, readonly) NSArray *textFields; + +/// 设置自定义view。通过`addCustomView:`方法添加一个自定义的view,`QMUIAlertController`会在布局的时候去调用这个view的`sizeThatFits:`方法来获取size,至于x和y坐标则由控件自己控制。 +@property(nullable, nonatomic, strong, readonly) UIView *customView; + +/// 当前标题title +@property(nullable, nonatomic, copy) NSString *title; + +/// 当前信息message +@property(nullable, nonatomic, copy) NSString *message; + +/// 当前样式style +@property(nonatomic, assign, readonly) QMUIAlertControllerStyle preferredStyle; + +/// 将`QMUIAlertController`弹出来的`QMUIModalPresentationViewController`对象 +@property(nullable, nonatomic, strong, readonly) QMUIModalPresentationViewController *modalPresentationViewController; + +/// 主体内容(alert 下指整个弹窗,actionSheet 下指取消按钮上方的那些 header 和 按钮)背后用来做背景样式的 view,默认为空白的 UIView,当你需要做磨砂效果时可以将一个 UIVisualEffectView 赋值给它。当赋值为 nil 时,内部会自动创建一个空白的 UIView 代替,以保证这个属性不为空。 +@property(null_resettable, nonatomic, strong) UIView *mainVisualEffectView; + +/// actionSheet 下的取消按钮背后用来做背景样式的 view,默认为空白的 UIView,当你需要做磨砂效果时可以将一个 UIVisualEffectView 赋值给它。alert 情况下不会出现。当赋值为 nil 时,内部会自动创建一个空白的 UIView 代替,以保证这个属性不为空。 +@property(null_resettable, nonatomic, strong) UIView *cancelButtonVisualEffectView; + +/** + * 设置按钮的排序是否要由用户添加的顺序来决定,默认为NO,也即与系统原生`UIAlertController`一致,QMUIAlertActionStyleDestructive 类型的action必定在最后面。 + * + * @warning 注意 QMUIAlertActionStyleCancel 按钮不受这个属性的影响 + */ +@property(nonatomic, assign) BOOL orderActionsByAddedOrdered; + +/// 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; + +@end + + +@interface QMUIAlertController (UIAppearance) + ++ (instancetype)appearance; + +@end + + +@interface QMUIAlertController (Manager) + +/// 可方便地判断是否有 alertController 正在显示,全局生效 ++ (BOOL)isAnyAlertControllerVisible; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.m b/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.m new file mode 100644 index 00000000..32a201a2 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIAlertController.m @@ -0,0 +1,1297 @@ +/** + * 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. + */ + +// +// QMUIAlertController.m +// qmui +// +// Created by QMUI Team on 15/7/20. +// + +#import "QMUIAlertController.h" +#import "QMUICore.h" +#import "QMUIButton.h" +#import "QMUITextField.h" +#import "UIView+QMUI.h" +#import "UIControl+QMUI.h" +#import "NSParagraphStyle+QMUI.h" +#import "UIImage+QMUI.h" +#import "CALayer+QMUI.h" +#import "QMUIKeyboardManager.h" +#import "QMUIAppearance.h" +#import "QMUILabel.h" + +static NSUInteger alertControllerCount = 0; + +#pragma mark - QMUIBUttonWrapView + +@interface QMUIAlertButtonWrapView : UIView + +@property(nonatomic, strong) QMUIButton *button; + +@end + +@implementation QMUIAlertButtonWrapView + +- (instancetype)init { + self = [super init]; + if (self) { + self.button = [[QMUIButton alloc] init]; + self.button.adjustsButtonWhenDisabled = NO; + self.button.adjustsButtonWhenHighlighted = NO; + [self addSubview:self.button]; + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.button.frame = self.bounds; +} + +@end + + +#pragma mark - QMUIAlertAction + +@protocol QMUIAlertActionDelegate + +- (void)didClickAlertAction:(QMUIAlertAction *)alertAction; + +@end + +@interface QMUIAlertAction () + +@property(nonatomic, copy, readwrite) NSString *title; +@property(nonatomic, assign, readwrite) QMUIAlertActionStyle style; +@property(nonatomic, copy) void (^handler)(QMUIAlertController *aAlertController, QMUIAlertAction *action); +@property(nonatomic, weak) id delegate; + +@end + +@implementation QMUIAlertAction + ++ (nonnull instancetype)actionWithTitle:(nullable NSString *)title style:(QMUIAlertActionStyle)style handler:(void (^)(__kindof QMUIAlertController *, QMUIAlertAction *))handler { + QMUIAlertAction *alertAction = [[self alloc] init]; + alertAction.title = title; + alertAction.style = style; + alertAction.handler = handler; + return alertAction; +} + +- (nonnull instancetype)init { + self = [super init]; + if (self) { + _button = [[QMUIButton alloc] init]; + self.button.adjustsButtonWhenDisabled = NO; + self.button.adjustsButtonWhenHighlighted = NO; + self.button.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + [self.button addTarget:self action:@selector(handleAlertActionEvent:) forControlEvents:UIControlEventTouchUpInside]; + } + return self; +} + +- (void)setEnabled:(BOOL)enabled { + _enabled = enabled; + self.button.enabled = enabled; +} + +- (void)handleAlertActionEvent:(id)sender { + // 需要先调delegate,里面会先恢复keywindow + if (self.delegate && [self.delegate respondsToSelector:@selector(didClickAlertAction:)]) { + [self.delegate didClickAlertAction:self]; + } +} + +@end + + +@implementation QMUIAlertController (UIAppearance) + ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self initAppearance]; + }); +} + ++ (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 + + +#pragma mark - QMUIAlertController + +@interface QMUIAlertController () + +@property(nonatomic, assign, readwrite) QMUIAlertControllerStyle preferredStyle; +@property(nonatomic, strong, readwrite) QMUIModalPresentationViewController *modalPresentationViewController; + +@property(nonatomic, strong) UIView *containerView; + +@property(nonatomic, strong) UIControl *dimmingView; + +@property(nonatomic, strong) UIView *scrollWrapView; +@property(nonatomic, strong) UIScrollView *headerScrollView; +@property(nonatomic, strong) UIScrollView *buttonScrollView; + +@property(nonatomic, strong) CALayer *extendLayer; + +@property(nonatomic, strong) QMUILabel *titleLabel; +@property(nonatomic, strong) QMUILabel *messageLabel; +@property(nonatomic, strong) QMUIAlertAction *cancelAction; + +@property(nonatomic, strong) NSMutableArray *alertActions; +@property(nonatomic, strong) NSMutableArray *destructiveActions; +@property(nonatomic, strong) NSMutableArray *alertTextFields; + +@property(nonatomic, assign) CGFloat keyboardHeight; + +/// 调用 showWithAnimated 时置为 YES,在 show 动画结束时置为 NO +@property(nonatomic, assign) BOOL willShow; + +/// 在 show 动画结束时置为 YES,在 hide 动画结束时置为 NO +@property(nonatomic, assign) BOOL showing; + +// 保护 showing 的过程中调用 hide 无效 +@property(nonatomic, assign) BOOL isNeedsHideAfterAlertShowed; +@property(nonatomic, assign) BOOL isAnimatedForHideAfterAlertShowed; + +@end + +@implementation QMUIAlertController { + NSString *_title; + BOOL _needsUpdateAction; + BOOL _needsUpdateTitle; + BOOL _needsUpdateMessage; +} + +- (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 { + [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; +} + +- (void)setAlertButtonAttributes:(NSDictionary *)alertButtonAttributes { + _alertButtonAttributes = alertButtonAttributes; + _needsUpdateAction = YES; +} + +- (void)setSheetButtonAttributes:(NSDictionary *)sheetButtonAttributes { + _sheetButtonAttributes = sheetButtonAttributes; + _needsUpdateAction = YES; +} + +- (void)setAlertButtonDisabledAttributes:(NSDictionary *)alertButtonDisabledAttributes { + _alertButtonDisabledAttributes = alertButtonDisabledAttributes; + _needsUpdateAction = YES; +} + +- (void)setSheetButtonDisabledAttributes:(NSDictionary *)sheetButtonDisabledAttributes { + _sheetButtonDisabledAttributes = sheetButtonDisabledAttributes; + _needsUpdateAction = YES; +} + +- (void)setAlertCancelButtonAttributes:(NSDictionary *)alertCancelButtonAttributes { + _alertCancelButtonAttributes = alertCancelButtonAttributes; + _needsUpdateAction = YES; +} + +- (void)setSheetCancelButtonAttributes:(NSDictionary *)sheetCancelButtonAttributes { + _sheetCancelButtonAttributes = sheetCancelButtonAttributes; + _needsUpdateAction = YES; +} + +- (void)setAlertDestructiveButtonAttributes:(NSDictionary *)alertDestructiveButtonAttributes { + _alertDestructiveButtonAttributes = alertDestructiveButtonAttributes; + _needsUpdateAction = YES; +} + +- (void)setSheetDestructiveButtonAttributes:(NSDictionary *)sheetDestructiveButtonAttributes { + _sheetDestructiveButtonAttributes = sheetDestructiveButtonAttributes; + _needsUpdateAction = YES; +} + +- (void)setAlertButtonBackgroundColor:(UIColor *)alertButtonBackgroundColor { + _alertButtonBackgroundColor = alertButtonBackgroundColor; + _needsUpdateAction = YES; +} + +- (void)setSheetButtonBackgroundColor:(UIColor *)sheetButtonBackgroundColor { + _sheetButtonBackgroundColor = sheetButtonBackgroundColor; + [self updateExtendLayerAppearance]; + _needsUpdateAction = YES; +} + +- (void)setAlertButtonHighlightBackgroundColor:(UIColor *)alertButtonHighlightBackgroundColor { + _alertButtonHighlightBackgroundColor = alertButtonHighlightBackgroundColor; + _needsUpdateAction = YES; +} + +- (void)setSheetButtonHighlightBackgroundColor:(UIColor *)sheetButtonHighlightBackgroundColor { + _sheetButtonHighlightBackgroundColor = sheetButtonHighlightBackgroundColor; + _needsUpdateAction = YES; +} + +- (void)setAlertTitleAttributes:(NSDictionary *)alertTitleAttributes { + _alertTitleAttributes = alertTitleAttributes; + _needsUpdateTitle = YES; +} + +- (void)setAlertMessageAttributes:(NSDictionary *)alertMessageAttributes { + _alertMessageAttributes = alertMessageAttributes; + _needsUpdateMessage = YES; +} + +- (void)setSheetTitleAttributes:(NSDictionary *)sheetTitleAttributes { + _sheetTitleAttributes = sheetTitleAttributes; + _needsUpdateTitle = YES; +} + +- (void)setSheetMessageAttributes:(NSDictionary *)sheetMessageAttributes { + _sheetMessageAttributes = sheetMessageAttributes; + _needsUpdateMessage = YES; +} + +- (void)setAlertHeaderBackgroundColor:(UIColor *)alertHeaderBackgroundColor { + _alertHeaderBackgroundColor = alertHeaderBackgroundColor; + [self updateHeaderBackgrondColor]; +} + +- (void)setSheetHeaderBackgroundColor:(UIColor *)sheetHeaderBackgroundColor { + _sheetHeaderBackgroundColor = sheetHeaderBackgroundColor; + [self updateHeaderBackgrondColor]; +} + +- (void)updateHeaderBackgrondColor { + if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { + if (_headerScrollView) { _headerScrollView.backgroundColor = self.sheetHeaderBackgroundColor; } + } else if (self.preferredStyle == QMUIAlertControllerStyleAlert) { + if (_headerScrollView) { _headerScrollView.backgroundColor = self.alertHeaderBackgroundColor; } + } +} + +- (void)setAlertSeparatorColor:(UIColor *)alertSeparatorColor { + _alertSeparatorColor = alertSeparatorColor; + [self updateSeparatorColor]; +} + +- (void)setSheetSeparatorColor:(UIColor *)sheetSeparatorColor { + _sheetSeparatorColor = sheetSeparatorColor; + [self updateSeparatorColor]; +} + +- (void)updateSeparatorColor { + UIColor *separatorColor = self.preferredStyle == QMUIAlertControllerStyleAlert ? self.alertSeparatorColor : self.sheetSeparatorColor; + [self.alertActions enumerateObjectsUsingBlock:^(QMUIAlertAction * _Nonnull alertAction, NSUInteger idx, BOOL * _Nonnull stop) { + alertAction.button.qmui_borderColor = separatorColor; + }]; +} + +- (void)setAlertContentCornerRadius:(CGFloat)alertContentCornerRadius { + _alertContentCornerRadius = alertContentCornerRadius; + [self updateCornerRadius]; +} + +- (void)setSheetContentCornerRadius:(CGFloat)sheetContentCornerRadius { + _sheetContentCornerRadius = sheetContentCornerRadius; + [self updateCornerRadius]; +} + +- (void)setIsExtendBottomLayout:(BOOL)isExtendBottomLayout { + _isExtendBottomLayout = isExtendBottomLayout; + if (isExtendBottomLayout) { + self.extendLayer.hidden = NO; + [self updateExtendLayerAppearance]; + } else { + self.extendLayer.hidden = YES; + } +} + +- (void)updateExtendLayerAppearance { + 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 = 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; } + if (self.cancelButtonVisualEffectView) { self.cancelButtonVisualEffectView.layer.cornerRadius = self.sheetContentCornerRadius; self.cancelButtonVisualEffectView.clipsToBounds = YES; } + if (self.scrollWrapView) { self.scrollWrapView.layer.cornerRadius = self.sheetContentCornerRadius; self.scrollWrapView.clipsToBounds = YES; } + } +} + +- (void)setAlertTextFieldFont:(UIFont *)alertTextFieldFont { + _alertTextFieldFont = alertTextFieldFont; + [self.textFields enumerateObjectsUsingBlock:^(QMUITextField * _Nonnull textField, NSUInteger idx, BOOL * _Nonnull stop) { + textField.font = alertTextFieldFont; + }]; +} + +- (void)setAlertTextFieldBorderColor:(UIColor *)alertTextFieldBorderColor { + _alertTextFieldBorderColor = alertTextFieldBorderColor; + [self.textFields enumerateObjectsUsingBlock:^(QMUITextField * _Nonnull textField, NSUInteger idx, BOOL * _Nonnull stop) { + textField.layer.borderColor = alertTextFieldBorderColor.CGColor; + }]; +} + +- (void)setAlertTextFieldTextColor:(UIColor *)alertTextFieldTextColor { + _alertTextFieldTextColor = alertTextFieldTextColor; + [self.textFields enumerateObjectsUsingBlock:^(QMUITextField * _Nonnull textField, NSUInteger idx, BOOL * _Nonnull stop) { + textField.textColor = 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) { + // 不允许为空 + mainVisualEffectView = [[UIView alloc] init]; + } + BOOL isValueChanged = _mainVisualEffectView != mainVisualEffectView; + if (isValueChanged) { + if ([_mainVisualEffectView isKindOfClass:[UIVisualEffectView class]]) { + [((UIVisualEffectView *)_mainVisualEffectView).contentView qmui_removeAllSubviews]; + } else { + [_mainVisualEffectView qmui_removeAllSubviews]; + } + [_mainVisualEffectView removeFromSuperview]; + _mainVisualEffectView = nil; + } + _mainVisualEffectView = mainVisualEffectView; + if (isValueChanged) { + [self.scrollWrapView insertSubview:_mainVisualEffectView atIndex:0]; + [self updateCornerRadius]; + } +} + +- (void)setCancelButtonVisualEffectView:(UIView *)cancelButtonVisualEffectView { + if (!cancelButtonVisualEffectView) { + // 不允许为空 + cancelButtonVisualEffectView = [[UIView alloc] init]; + } + BOOL isValueChanged = _cancelButtonVisualEffectView != cancelButtonVisualEffectView; + if (isValueChanged) { + if ([_cancelButtonVisualEffectView isKindOfClass:[UIVisualEffectView class]]) { + [((UIVisualEffectView *)_cancelButtonVisualEffectView).contentView qmui_removeAllSubviews]; + } else { + [_cancelButtonVisualEffectView qmui_removeAllSubviews]; + } + [_cancelButtonVisualEffectView removeFromSuperview]; + _cancelButtonVisualEffectView = nil; + } + _cancelButtonVisualEffectView = cancelButtonVisualEffectView; + if (isValueChanged) { + [self.containerView addSubview:_cancelButtonVisualEffectView]; + if (self.preferredStyle == QMUIAlertControllerStyleActionSheet && self.cancelAction && !self.cancelAction.button.superview) { + if ([_cancelButtonVisualEffectView isKindOfClass:[UIVisualEffectView class]]) { + UIVisualEffectView *effectView = (UIVisualEffectView *)_cancelButtonVisualEffectView; + [effectView.contentView addSubview:self.cancelAction.button]; + } else { + [_cancelButtonVisualEffectView addSubview:self.cancelAction.button]; + } + } + + [self updateCornerRadius]; + } +} + ++ (nonnull instancetype)alertControllerWithTitle:(nullable NSString *)title message:(nullable NSString *)message preferredStyle:(QMUIAlertControllerStyle)preferredStyle { + QMUIAlertController *alertController = [[self alloc] initWithTitle:title message:message preferredStyle:preferredStyle]; + if (alertController) { + return alertController; + } + return nil; +} + +- (nonnull instancetype)initWithTitle:(nullable NSString *)title message:(nullable NSString *)message preferredStyle:(QMUIAlertControllerStyle)preferredStyle { + self = [self init]; + if (self) { + + self.preferredStyle = preferredStyle; + + self.shouldRespondDimmingViewTouch = preferredStyle == QMUIAlertControllerStyleActionSheet; + + self.alertActions = [[NSMutableArray alloc] init]; + self.alertTextFields = [[NSMutableArray alloc] init]; + self.destructiveActions = [[NSMutableArray alloc] init]; + + self.title = title; + self.message = message; + + self.mainVisualEffectView = [[UIView alloc] init]; + self.cancelButtonVisualEffectView = [[UIView alloc] init]; + } + return self; +} + +- (QMUIAlertControllerStyle)preferredStyle { + return PreferredValueForDeviceIncludingiPad(1, 0, 0, 0, 0) > 0 ? QMUIAlertControllerStyleAlert : _preferredStyle; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + [self.view addSubview:self.dimmingView]; + [self.view addSubview:self.containerView]; + [self.containerView addSubview:self.scrollWrapView]; + [self.scrollWrapView addSubview:self.headerScrollView]; + [self.scrollWrapView addSubview:self.buttonScrollView]; + [self.containerView.layer addSublayer:self.extendLayer]; +} + +- (void)viewDidLayoutSubviews { + + [super viewDidLayoutSubviews]; + + BOOL hasTitle = (self.titleLabel.text.length > 0 && !self.titleLabel.hidden); + 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.dimmingView.frame = self.view.bounds; + + if (self.preferredStyle == QMUIAlertControllerStyleAlert) { + + CGFloat contentPaddingLeft = self.alertHeaderInsets.left; + CGFloat contentPaddingRight = self.alertHeaderInsets.right; + + CGFloat contentPaddingTop = (hasTitle || hasMessage || hasTextField || hasCustomView) ? self.alertHeaderInsets.top : 0; + CGFloat contentPaddingBottom = (hasTitle || hasMessage || hasTextField || hasCustomView) ? self.alertHeaderInsets.bottom : 0; + self.containerView.qmui_width = fmin(self.alertContentMaximumWidth, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(self.alertContentMargin)); + self.scrollWrapView.qmui_width = CGRectGetWidth(self.containerView.bounds); + self.headerScrollView.frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollWrapView.bounds), 0); + contentOriginY = contentPaddingTop; + // 标题和副标题布局 + if (hasTitle) { + self.titleLabel.frame = CGRectFlatted(CGRectMake(contentPaddingLeft, contentOriginY, CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight, QMUIViewSelfSizingHeight)); + contentOriginY = CGRectGetMaxY(self.titleLabel.frame) + (hasMessage ? self.alertTitleMessageSpacing : contentPaddingBottom); + } + if (hasMessage) { + self.messageLabel.frame = CGRectFlatted(CGRectMake(contentPaddingLeft, contentOriginY, CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight, QMUIViewSelfSizingHeight)); + contentOriginY = CGRectGetMaxY(self.messageLabel.frame) + contentPaddingBottom; + } + // 输入框布局 + if (hasTextField) { + for (int i = 0; i < self.alertTextFields.count; i++) { + UITextField *textField = self.alertTextFields[i]; + 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; + } + } + // 自定义view的布局 - 自动居中 + if (hasCustomView) { + CGSize customViewSize = [_customView sizeThatFits:CGSizeMake(CGRectGetWidth(self.headerScrollView.bounds), CGFLOAT_MAX)]; + _customView.frame = CGRectFlatted(CGRectMake((CGRectGetWidth(self.headerScrollView.bounds) - customViewSize.width) / 2, contentOriginY, customViewSize.width, customViewSize.height)); + contentOriginY = CGRectGetMaxY(_customView.frame) + contentPaddingBottom; + } + // 内容scrollView的布局 + self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, contentOriginY); + self.headerScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.headerScrollView.bounds), contentOriginY); + 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 (newOrderActions.count > 0) { + BOOL verticalLayout = YES; + if (self.alertActions.count == 2) { + CGFloat halfWidth = CGRectGetWidth(self.buttonScrollView.bounds) / 2; + QMUIAlertAction *action1 = newOrderActions[0]; + QMUIAlertAction *action2 = newOrderActions[1]; + CGSize actionSize1 = [action1.button sizeThatFits:CGSizeMax]; + CGSize actionSize2 = [action2.button sizeThatFits:CGSizeMax]; + if (actionSize1.width < halfWidth && actionSize2.width < halfWidth) { + verticalLayout = NO; + } + } + if (!verticalLayout) { + // 对齐系统,先 add 的在右边,后 add 的在左边 + QMUIAlertAction *leftAction = newOrderActions[1]; + leftAction.button.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.buttonScrollView.bounds) / 2, self.alertButtonHeight); + 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); + 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); + if (i > 0 || shouldShowSeparatorAtTopOfButtonAtFirstLine) { + action.button.qmui_borderPosition = QMUIViewBorderPositionTop; + } + contentOriginY = CGRectGetMaxY(action.button.frame); + } + } + } + // 按钮scrollView的布局 + self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, contentOriginY); + 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) - UIEdgeInsetsGetVerticalValue(SafeAreaInsetsConstantForDeviceWithNotch) - self.keyboardHeight; + if (contentHeight > screenSpaceHeight - 20) { + screenSpaceHeight -= 20; + CGFloat contentH = fmin(CGRectGetHeight(self.headerScrollView.bounds), screenSpaceHeight / 2); + CGFloat buttonH = fmin(CGRectGetHeight(self.buttonScrollView.bounds), screenSpaceHeight / 2); + if (contentH >= screenSpaceHeight / 2 && buttonH >= screenSpaceHeight / 2) { + self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, screenSpaceHeight / 2); + self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); + self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, screenSpaceHeight / 2); + } else if (contentH < screenSpaceHeight / 2) { + self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, contentH); + self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); + self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, screenSpaceHeight - contentH); + } else if (buttonH < screenSpaceHeight / 2) { + self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, screenSpaceHeight - buttonH); + self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); + self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, buttonH); + } + contentHeight = CGRectGetHeight(self.headerScrollView.bounds) + CGRectGetHeight(self.buttonScrollView.bounds); + screenSpaceHeight += 20; + } + self.scrollWrapView.frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollWrapView.bounds), contentHeight); + self.mainVisualEffectView.frame = self.scrollWrapView.bounds; + + 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) { + + CGFloat contentPaddingLeft = self.alertHeaderInsets.left; + CGFloat contentPaddingRight = self.alertHeaderInsets.right; + + CGFloat contentPaddingTop = (hasTitle || hasMessage || hasTextField) ? self.sheetHeaderInsets.top : 0; + CGFloat contentPaddingBottom = (hasTitle || hasMessage || hasTextField) ? self.sheetHeaderInsets.bottom : 0; + self.containerView.qmui_width = fmin(self.sheetContentMaximumWidth, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(self.sheetContentMargin)); + self.scrollWrapView.qmui_width = CGRectGetWidth(self.containerView.bounds); + self.headerScrollView.frame = CGRectMake(0, 0, CGRectGetWidth(self.containerView.bounds), 0); + contentOriginY = contentPaddingTop; + // 标题和副标题布局 + if (hasTitle) { + self.titleLabel.frame = CGRectFlatted(CGRectMake(contentPaddingLeft, contentOriginY, CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight, QMUIViewSelfSizingHeight)); + contentOriginY = CGRectGetMaxY(self.titleLabel.frame) + (hasMessage ? self.sheetTitleMessageSpacing : contentPaddingBottom); + } + if (hasMessage) { + self.messageLabel.frame = CGRectFlatted(CGRectMake(contentPaddingLeft, contentOriginY, CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight, QMUIViewSelfSizingHeight)); + contentOriginY = CGRectGetMaxY(self.messageLabel.frame) + contentPaddingBottom; + } + // 自定义view的布局 - 自动居中 + if (hasCustomView) { + CGSize customViewSize = [_customView sizeThatFits:CGSizeMake(CGRectGetWidth(self.headerScrollView.bounds), CGFLOAT_MAX)]; + _customView.frame = CGRectFlatted(CGRectMake((CGRectGetWidth(self.headerScrollView.bounds) - customViewSize.width) / 2, contentOriginY, customViewSize.width, customViewSize.height)); + contentOriginY = CGRectGetMaxY(_customView.frame) + contentPaddingBottom; + } + // 内容scrollView布局 + self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, contentOriginY); + self.headerScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.headerScrollView.bounds), contentOriginY); + contentOriginY = CGRectGetMaxY(self.headerScrollView.frame); + // 按钮的布局 + self.buttonScrollView.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.containerView.bounds), 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) { + for (int i = 0; i < newOrderActions.count; i++) { + QMUIAlertAction *action = newOrderActions[i]; + if (action.style == QMUIAlertActionStyleCancel && i == newOrderActions.count - 1) { + continue; + } else { + 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; + } + } + } + } + // 按钮scrollView布局 + self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, contentOriginY); + self.buttonScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.buttonScrollView.bounds), contentOriginY); + // 容器最终布局 + self.scrollWrapView.frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollWrapView.bounds), CGRectGetMaxY(self.buttonScrollView.frame)); + self.mainVisualEffectView.frame = self.scrollWrapView.bounds; + contentOriginY = CGRectGetMaxY(self.scrollWrapView.frame) + self.sheetCancelButtonMarginTop; + if (self.cancelAction) { + self.cancelButtonVisualEffectView.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.containerView.bounds), self.sheetButtonHeight); + self.cancelAction.button.frame = self.cancelButtonVisualEffectView.bounds; + contentOriginY = CGRectGetMaxY(self.cancelButtonVisualEffectView.frame); + } + // 把上下的margin都加上用于跟整个屏幕的高度做比较 + CGFloat contentHeight = contentOriginY + UIEdgeInsetsGetVerticalValue(self.sheetContentMargin); + 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); + CGFloat contentH = MIN(CGRectGetHeight(self.headerScrollView.bounds), screenSpaceHeight / 2); + CGFloat buttonH = MIN(CGRectGetHeight(self.buttonScrollView.bounds), screenSpaceHeight / 2); + if (contentH >= screenSpaceHeight / 2 && buttonH >= screenSpaceHeight / 2) { + self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, screenSpaceHeight / 2); + self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); + self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, screenSpaceHeight / 2); + } else if (contentH < screenSpaceHeight / 2) { + self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, contentH); + self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); + self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, screenSpaceHeight - contentH); + } else if (buttonH < screenSpaceHeight / 2) { + self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, screenSpaceHeight - buttonH); + self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); + self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, buttonH); + } + self.scrollWrapView.frame = CGRectSetHeight(self.scrollWrapView.frame, CGRectGetHeight(self.headerScrollView.bounds) + CGRectGetHeight(self.buttonScrollView.bounds)); + if (self.cancelAction) { + self.cancelButtonVisualEffectView.frame = CGRectSetY(self.cancelButtonVisualEffectView.frame, CGRectGetMaxY(self.scrollWrapView.frame) + self.sheetCancelButtonMarginTop); + } + contentHeight = CGRectGetHeight(self.headerScrollView.bounds) + CGRectGetHeight(self.buttonScrollView.bounds) + cancelButtonAreaHeight + self.sheetContentMargin.bottom; + screenSpaceHeight += (cancelButtonAreaHeight + UIEdgeInsetsGetVerticalValue(self.sheetContentMargin)); + } else { + // 如果小于屏幕高度,则把顶部的top减掉 + contentHeight -= self.sheetContentMargin.top; + } + + 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); + } +} + +- (NSArray *)orderedAlertActions:(NSArray *)actions { + NSMutableArray *newActions = [[NSMutableArray alloc] init]; + // 按照用户addAction的先后顺序来排序 + if (self.orderActionsByAddedOrdered) { + [newActions addObjectsFromArray:self.alertActions]; + // 取消按钮不参与排序,所以先移除,在最后再重新添加 + if (self.cancelAction) { + [newActions removeObject:self.cancelAction]; + } + } else { + for (QMUIAlertAction *action in self.alertActions) { + if (action.style != QMUIAlertActionStyleCancel && action.style != QMUIAlertActionStyleDestructive) { + [newActions addObject:action]; + } + } + for (QMUIAlertAction *action in self.destructiveActions) { + [newActions addObject:action]; + } + } + if (self.cancelAction) { + [newActions addObject:self.cancelAction]; + } + return newActions; +} + +- (void)initModalPresentationController { + _modalPresentationViewController = [[QMUIModalPresentationViewController alloc] init]; + self.modalPresentationViewController.delegate = self; + self.modalPresentationViewController.maximumContentViewWidth = CGFLOAT_MAX; + self.modalPresentationViewController.contentViewMargins = UIEdgeInsetsZero; + self.modalPresentationViewController.dimmingView = nil; + self.modalPresentationViewController.contentViewController = self; + [self customModalPresentationControllerAnimation]; +} + +- (void)customModalPresentationControllerAnimation { + + __weak __typeof(self)weakSelf = self; + + self.modalPresentationViewController.layoutBlock = ^(CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewDefaultFrame) { + weakSelf.view.frame = CGRectMake(0, 0, CGRectGetWidth(containerBounds), CGRectGetHeight(containerBounds)); + weakSelf.keyboardHeight = keyboardHeight; + [weakSelf.view setNeedsLayout]; + }; + + self.modalPresentationViewController.showingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewFrame, void(^completion)(BOOL finished)) { + if (self.preferredStyle == QMUIAlertControllerStyleAlert) { + 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.dimmingView.alpha = 1; + weakSelf.containerView.alpha = 1; + weakSelf.containerView.layer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0); + } completion:^(BOOL finished) { + if (completion) { + completion(finished); + } + }]; + } 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.dimmingView.alpha = 1; + weakSelf.containerView.layer.transform = CATransform3DIdentity; + } completion:^(BOOL finished) { + if (completion) { + completion(finished); + } + }]; + } + }; + + 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.dimmingView.alpha = 0; + weakSelf.containerView.alpha = 0; + } completion:^(BOOL finished) { + weakSelf.containerView.alpha = 1; + if (completion) { + completion(finished); + } + }]; + } else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { + [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + weakSelf.dimmingView.alpha = 0; + weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, CGRectGetHeight(weakSelf.view.bounds) - CGRectGetMinY(weakSelf.containerView.frame), 0); + } completion:^(BOOL finished) { + if (completion) { + completion(finished); + } + }]; + } + }; +} + +- (void)showWithAnimated:(BOOL)animated { + if (self.willShow || self.showing) { + return; + } + self.willShow = YES; + + if (self.alertTextFields.count > 0) { + [self.alertTextFields.firstObject becomeFirstResponder]; + } + + if (_needsUpdateAction) { + [self updateAction]; + } + if (_needsUpdateTitle) { + [self updateTitleLabel]; + } + if (_needsUpdateMessage) { + [self updateMessageLabel]; + } + + [self initModalPresentationController]; + + if ([self.delegate respondsToSelector:@selector(willShowAlertController:)]) { + [self.delegate willShowAlertController:self]; + } + + __weak __typeof(self)weakSelf = self; + + [self.modalPresentationViewController showWithAnimated:animated completion:^(BOOL finished) { + weakSelf.dimmingView.alpha = 1; + weakSelf.willShow = NO; + weakSelf.showing = YES; + if (weakSelf.isNeedsHideAfterAlertShowed) { + [weakSelf hideWithAnimated:weakSelf.isAnimatedForHideAfterAlertShowed]; + weakSelf.isNeedsHideAfterAlertShowed = NO; + weakSelf.isAnimatedForHideAfterAlertShowed = NO; + } + if ([weakSelf.delegate respondsToSelector:@selector(didShowAlertController:)]) { + [weakSelf.delegate didShowAlertController:weakSelf]; + } + }]; + + // 增加alertController计数 + alertControllerCount++; +} + +- (void)hideWithAnimated:(BOOL)animated { + [self hideWithAnimated:animated completion:NULL]; +} + +- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(void))completion { + if ([self.delegate respondsToSelector:@selector(shouldHideAlertController:)] && ![self.delegate shouldHideAlertController:self]) { + return; + } + + if (!self.showing) { + if (self.willShow) { + self.isNeedsHideAfterAlertShowed = YES; + self.isAnimatedForHideAfterAlertShowed = animated; + } + return; + } + + if ([self.delegate respondsToSelector:@selector(willHideAlertController:)]) { + [self.delegate willHideAlertController:self]; + } + + __weak __typeof(self)weakSelf = self; + + [self.modalPresentationViewController hideWithAnimated:animated completion:^(BOOL finished) { + weakSelf.modalPresentationViewController = nil; + weakSelf.willShow = NO; + weakSelf.showing = NO; + weakSelf.dimmingView.alpha = 0; + if (self.preferredStyle == QMUIAlertControllerStyleAlert) { + weakSelf.containerView.alpha = 0; + } else { + weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, CGRectGetHeight(weakSelf.view.bounds) - CGRectGetMinY(weakSelf.containerView.frame), 0); + } + if ([weakSelf.delegate respondsToSelector:@selector(didHideAlertController:)]) { + [weakSelf.delegate didHideAlertController:weakSelf]; + } + if (completion) completion(); + }]; + + // 减少alertController计数 + alertControllerCount--; +} + +- (void)addAction:(nonnull QMUIAlertAction *)action { + if (action.style == QMUIAlertActionStyleCancel && self.cancelAction) { + [NSException raise:@"QMUIAlertController使用错误" format:@"同一个alertController不可以同时添加两个cancel按钮"]; + } + if (action.style == QMUIAlertActionStyleCancel) { + self.cancelAction = action; + } + if (action.style == QMUIAlertActionStyleDestructive) { + [self.destructiveActions addObject:action]; + } + // 只有ActionSheet的取消按钮不参与滚动 + if (self.preferredStyle == QMUIAlertControllerStyleActionSheet && action.style == QMUIAlertActionStyleCancel) { + if (!self.cancelButtonVisualEffectView.superview) { + [self.containerView addSubview:self.cancelButtonVisualEffectView]; + } + if ([self.cancelButtonVisualEffectView isKindOfClass:[UIVisualEffectView class]]) { + [((UIVisualEffectView *)self.cancelButtonVisualEffectView).contentView addSubview:action.button]; + } else { + [self.cancelButtonVisualEffectView addSubview:action.button]; + } + } else { + [self.buttonScrollView addSubview:action.button]; + } + action.delegate = self; + [self.alertActions addObject:action]; +} + +- (void)addCancelAction { + QMUIAlertAction *action = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:nil]; + [self addAction:action]; +} + +- (void)addTextFieldWithConfigurationHandler:(void (^)(QMUITextField *textField))configurationHandler { + if (_customView) { + [NSException raise:@"QMUIAlertController使用错误" format:@"UITextField和CustomView不能共存"]; + } + if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { + [NSException raise:@"QMUIAlertController使用错误" format:@"Sheet类型不运行添加UITextField"]; + } + QMUITextField *textField = [[QMUITextField alloc] init]; + textField.delegate = self; + textField.borderStyle = UITextBorderStyleNone; + textField.backgroundColor = UIColorWhite; + textField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter; + textField.font = self.alertTextFieldFont; + 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]; + [self.alertTextFields addObject:textField]; + if (configurationHandler) { + configurationHandler(textField); + } +} + +- (void)addCustomView:(UIView *)view { + if (view && self.alertTextFields.count > 0) { + [NSException raise:@"QMUIAlertController使用错误" format:@"UITextField 和 customView 不能共存"]; + } + if (_customView && _customView != view) { + [_customView removeFromSuperview]; + } + _customView = view; + if (_customView) { + [self.headerScrollView addSubview:_customView]; + } +} + +- (void)setTitle:(NSString *)title { + _title = title; + if (!self.titleLabel) { + self.titleLabel = [[QMUILabel alloc] init]; + self.titleLabel.numberOfLines = 0; + [self.headerScrollView addSubview:self.titleLabel]; + } + if (!_title || [_title isEqualToString:@""]) { + self.titleLabel.hidden = YES; + } else { + self.titleLabel.hidden = NO; + [self updateTitleLabel]; + } +} + +- (NSString *)title { + return _title; +} + +- (void)updateTitleLabel { + if (self.titleLabel && !self.titleLabel.hidden) { + NSAttributedString *attributeString = [[NSAttributedString alloc] initWithString:self.title attributes:self.preferredStyle == QMUIAlertControllerStyleAlert ? self.alertTitleAttributes : self.sheetTitleAttributes]; + self.titleLabel.attributedText = attributeString; + } +} + +- (void)setMessage:(NSString *)message { + _message = message; + if (!self.messageLabel) { + self.messageLabel = [[QMUILabel alloc] init]; + self.messageLabel.numberOfLines = 0; + [self.headerScrollView addSubview:self.messageLabel]; + } + if (!_message || [_message isEqualToString:@""]) { + self.messageLabel.hidden = YES; + } else { + self.messageLabel.hidden = NO; + [self updateMessageLabel]; + } +} + +- (void)updateMessageLabel { + if (self.messageLabel && !self.messageLabel.hidden) { + NSAttributedString *attributeString = [[NSAttributedString alloc] initWithString:self.message attributes:self.preferredStyle == QMUIAlertControllerStyleAlert ? self.alertMessageAttributes : self.sheetMessageAttributes]; + self.messageLabel.attributedText = attributeString; + } +} + +- (NSArray *)actions { + return [self.alertActions copy]; +} + +- (void)updateAction { + + for (QMUIAlertAction *alertAction in self.alertActions) { + + UIColor *backgroundColor = self.preferredStyle == QMUIAlertControllerStyleAlert ? self.alertButtonBackgroundColor : self.sheetButtonBackgroundColor; + UIColor *highlightBackgroundColor = self.preferredStyle == QMUIAlertControllerStyleAlert ? self.alertButtonHighlightBackgroundColor : self.sheetButtonHighlightBackgroundColor; + UIColor *borderColor = self.preferredStyle == QMUIAlertControllerStyleAlert ? self.alertSeparatorColor : self.sheetSeparatorColor; + + alertAction.button.clipsToBounds = alertAction.style == QMUIAlertActionStyleCancel; + alertAction.button.backgroundColor = backgroundColor; + alertAction.button.highlightedBackgroundColor = highlightBackgroundColor; + alertAction.button.qmui_borderColor = borderColor; + + NSAttributedString *attributeString = nil; + if (alertAction.style == QMUIAlertActionStyleCancel) { + + NSDictionary *attributes = (self.preferredStyle == QMUIAlertControllerStyleAlert) ? self.alertCancelButtonAttributes : self.sheetCancelButtonAttributes; + if (alertAction.buttonAttributes) { + attributes = alertAction.buttonAttributes; + } + + attributeString = [[NSAttributedString alloc] initWithString:alertAction.title attributes:attributes]; + + } else if (alertAction.style == QMUIAlertActionStyleDestructive) { + + NSDictionary *attributes = (self.preferredStyle == QMUIAlertControllerStyleAlert) ? self.alertDestructiveButtonAttributes : self.sheetDestructiveButtonAttributes; + if (alertAction.buttonAttributes) { + attributes = alertAction.buttonAttributes; + } + + attributeString = [[NSAttributedString alloc] initWithString:alertAction.title attributes:attributes]; + + } else { + + NSDictionary *attributes = (self.preferredStyle == QMUIAlertControllerStyleAlert) ? self.alertButtonAttributes : self.sheetButtonAttributes; + if (alertAction.buttonAttributes) { + attributes = alertAction.buttonAttributes; + } + + attributeString = [[NSAttributedString alloc] initWithString:alertAction.title attributes:attributes]; + } + + [alertAction.button setAttributedTitle:attributeString forState:UIControlStateNormal]; + + NSDictionary *attributes = (self.preferredStyle == QMUIAlertControllerStyleAlert) ? self.alertButtonDisabledAttributes : self.sheetButtonDisabledAttributes; + if (alertAction.buttonDisabledAttributes) { + attributes = alertAction.buttonDisabledAttributes; + } + + attributeString = [[NSAttributedString alloc] initWithString:alertAction.title attributes:attributes]; + [alertAction.button setAttributedTitle:attributeString forState:UIControlStateDisabled]; + + if ([alertAction.button imageForState:UIControlStateNormal]) { + NSRange range = NSMakeRange(0, attributeString.length); + UIColor *disabledColor = [attributeString attribute:NSForegroundColorAttributeName atIndex:0 effectiveRange:&range]; + [alertAction.button setImage:[[alertAction.button imageForState:UIControlStateNormal] qmui_imageWithTintColor:disabledColor] forState:UIControlStateDisabled]; + } + } +} + +- (NSArray *)textFields { + return [self.alertTextFields copy]; +} + +- (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 { + [self hideWithAnimated:YES completion:^{ + if (alertAction.handler) { + alertAction.handler(self, alertAction); + } + }]; +} + +#pragma mark - + +- (void)hideModalPresentationComponent { + [self hideWithAnimated:NO completion:NULL]; +} + +#pragma mark - + +- (BOOL)shouldHideModalPresentationViewController:(QMUIModalPresentationViewController *)controller { + if ([self.delegate respondsToSelector:@selector(shouldHideAlertController:)]) { + return [self.delegate shouldHideAlertController:self]; + } + return YES; +} + +#pragma mark - + +- (BOOL)textFieldShouldReturn:(QMUITextField *)textField { + if (!self.shouldManageTextFieldsReturnEventAutomatically) { + return NO; + } + + if (![self.textFields containsObject:textField]) { + return NO; + } + + // 最后一个输入框,默认的 return 行为与 iOS 9-11 保持一致,也即: + // 如果 action = 1,则自动响应这个 action 的事件 + // 如果 action = 2,并且其中有一个是 Cancel,则响应另一个 action 的事件,如果其中不存在 Cancel,则降下键盘,不响应任何 action + // 如果 action > 2,则降下键盘,不响应任何 action + if (textField == self.textFields.lastObject) { + if (self.actions.count == 1) { + [self.actions.firstObject.button sendActionsForControlEvents:UIControlEventTouchUpInside]; + } else if (self.actions.count == 2) { + if (self.cancelAction) { + QMUIAlertAction *targetAction = self.actions.firstObject == self.cancelAction ? self.actions.lastObject : self.actions.firstObject; + [targetAction.button sendActionsForControlEvents:UIControlEventTouchUpInside]; + } + } + [self.view endEditing:YES]; + return NO; + } + // 非最后一个输入框,则默认的 return 行为是聚焦到下一个输入框 + NSUInteger index = [self.textFields indexOfObject:textField]; + [self.textFields[index + 1] becomeFirstResponder]; + return NO; +} + +@end + +@implementation QMUIAlertController (Manager) + ++ (BOOL)isAnyAlertControllerVisible { + return alertControllerCount > 0; +} + +@end + 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 new file mode 100644 index 00000000..a5e31442 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.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. + */ + +// +// UIBarItem+QMUIBadge.h +// QMUIKit +// +// Created by QMUI Team on 2018/6/2. +// + +#import +#import +#import "QMUIBadgeProtocol.h" + +/** + * 用于在 UIBarButtonItem(通常用于 UINavigationBar 和 UIToolbar)和 UITabBarItem 上显示未读红点或者未读数,对设置的时机没有要求。 + * 提供的属性请查看 @c QMUIBadgeProtocol ,属性的默认值在 QMUIConfigurationTemplate 配置表里设置,如果不使用配置表,则所有属性的默认值均为 0 或 nil。 + * + * @note 系统对 UIBarButtonItem 和 UITabBarItem 在横竖屏下均会有不同的布局,当你使用本控件时建议分别检查横竖屏下的表现是否正确。 + */ +@interface UIBarItem (QMUIBadge) + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m new file mode 100644 index 00000000..9dbb0490 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m @@ -0,0 +1,271 @@ +/** + * 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. + */ + +// +// UIBarItem+QMUIBadge.m +// QMUIKit +// +// Created by QMUI Team on 2018/6/2. +// + +#import "UIBarItem+QMUIBadge.h" +#import "QMUICore.h" +#import "UIView+QMUIBadge.h" +#import "UIBarItem+QMUI.h" + +@implementation UIBarItem (QMUIBadge) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // 保证配置表里的默认值正确被设置 + ExtendImplementationOfNonVoidMethodWithoutArguments([UIBarItem class], @selector(init), __kindof UIBarItem *, ^__kindof UIBarItem *(UIBarItem *selfObject, __kindof UIBarItem *originReturnValue) { + [selfObject qmuibaritem_didInitialize]; + return originReturnValue; + }); + + ExtendImplementationOfNonVoidMethodWithSingleArgument([UIBarItem class], @selector(initWithCoder:), NSCoder *, __kindof UIBarItem *, ^__kindof UIBarItem *(UIBarItem *selfObject, NSCoder *firstArgv, __kindof UIBarItem *originReturnValue) { + [selfObject qmuibaritem_didInitialize]; + return originReturnValue; + }); + + // UITabBarButton 在 layoutSubviews 时每次都重新让 imageView 和 label addSubview:,这会导致我们用 qmui_layoutSubviewsBlock 时产生持续的重复调用(但又不死循环,因为每次都在下一次 runloop 执行,而且奇怪的是如果不放到下一次 runloop,反而不会重复调用),所以这里 hack 地屏蔽 addSubview: 操作 + OverrideImplementation(NSClassFromString([NSString stringWithFormat:@"%@%@", @"UITab", @"BarButton"]), @selector(addSubview:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, UIView *firstArgv) { + + if (firstArgv.superview == selfObject) { + return; + } + + // call super + IMP originalIMP = originalIMPProvider(); + void (*originSelectorIMP)(id, SEL, UIView *); + originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMP; + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + }); +} + +- (void)qmuibaritem_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) { + [self updateViewDidSetBlockIfNeeded]; + } + self.qmui_view.qmui_badgeString = qmui_badgeString; +} + +- (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_view.qmui_badgeBackgroundColor = 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); + self.qmui_view.qmui_badgeTextColor = 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); + self.qmui_view.qmui_badgeFont = qmui_badgeFont; +} + +- (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); + self.qmui_view.qmui_badgeContentEdgeInsets = qmui_badgeContentEdgeInsets; +} + +- (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.qmui_view.qmui_badgeOffset = qmui_badgeOffset; +} + +- (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.qmui_view.qmui_badgeOffsetLandscape = qmui_badgeOffsetLandscape; +} + +- (CGPoint)qmui_badgeOffsetLandscape { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape)) CGPointValue]; +} + +- (void)setQmui_badgeView:(__kindof UIView *)qmui_badgeView { + self.qmui_view.qmui_badgeView = qmui_badgeView; +} + +- (__kindof UIView *)qmui_badgeView { + return self.qmui_view.qmui_badgeView; +} + +- (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 + +static char kAssociatedObjectKey_shouldShowUpdatesIndicator; +- (void)setQmui_shouldShowUpdatesIndicator:(BOOL)qmui_shouldShowUpdatesIndicator { + objc_setAssociatedObject(self, &kAssociatedObjectKey_shouldShowUpdatesIndicator, @(qmui_shouldShowUpdatesIndicator), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_shouldShowUpdatesIndicator) { + [self updateViewDidSetBlockIfNeeded]; + } + self.qmui_view.qmui_shouldShowUpdatesIndicator = qmui_shouldShowUpdatesIndicator; +} + +- (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_view.qmui_updatesIndicatorColor = 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); + self.qmui_view.qmui_updatesIndicatorSize = qmui_updatesIndicatorSize; +} + +- (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); + self.qmui_view.qmui_updatesIndicatorOffset = qmui_updatesIndicatorOffset; +} + +- (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); + self.qmui_view.qmui_updatesIndicatorOffsetLandscape = qmui_updatesIndicatorOffsetLandscape; +} + +- (CGPoint)qmui_updatesIndicatorOffsetLandscape { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape)) CGPointValue]; +} + +- (void)setQmui_updatesIndicatorView:(__kindof UIView *)qmui_updatesIndicatorView { + self.qmui_view.qmui_updatesIndicatorView = qmui_updatesIndicatorView; +} + +- (UIView *)qmui_updatesIndicatorView { + return self.qmui_view.qmui_updatesIndicatorView; +} + +- (void)setQmui_updatesIndicatorViewDidLayoutBlock:(void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_updatesIndicatorViewDidLayoutBlock { + self.qmui_view.qmui_updatesIndicatorViewDidLayoutBlock = qmui_updatesIndicatorViewDidLayoutBlock; +} + +- (void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_updatesIndicatorViewDidLayoutBlock { + return self.qmui_view.qmui_updatesIndicatorViewDidLayoutBlock; +} + +#pragma mark - Common + +- (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; + + 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; + }; + + // 为 qmui_viewDidSetBlock 赋值前 item 已经 set 完 view,则手动触发一次 + if (self.qmui_view) { + self.qmui_viewDidSetBlock(self, self.qmui_view); + } + } +} + +@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 new file mode 100644 index 00000000..88f9ed47 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h @@ -0,0 +1,117 @@ +/** + * 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. + */ + +// +// QMUIButton.h +// qmui +// +// Created by QMUI Team on 14-7-7. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// 控制图片在UIButton里的位置,默认为QMUIButtonImagePositionLeft +typedef NS_ENUM(NSUInteger, QMUIButtonImagePosition) { + QMUIButtonImagePositionTop, // imageView在titleLabel上面 + QMUIButtonImagePositionLeft, // imageView在titleLabel左边 + QMUIButtonImagePositionBottom, // imageView在titleLabel下面 + QMUIButtonImagePositionRight, // imageView在titleLabel右边 +}; + +/** + * 用于 `QMUIButton.cornerRadius` 属性,当 `cornerRadius` 为 `QMUIButtonCornerRadiusAdjustsBounds` 时,`QMUIButton` 会在高度变化时自动调整 `cornerRadius`,使其始终保持为高度的 1/2。 + */ +extern const CGFloat QMUIButtonCornerRadiusAdjustsBounds; + +/** + * 提供以下功能: + * 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 + +/** + * 子类继承时重写的方法,一般不建议重写 initWithXxx + */ +- (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 + */ +@property(nonatomic, assign) IBInspectable BOOL adjustsTitleTintColorAutomatically; + +/** + * 让按钮的图片颜色自动跟随tintColor调整(系统默认image是需要更改renderingMode才可以达到这种效果)
+ * 默认为NO + */ +@property(nonatomic, assign) IBInspectable BOOL adjustsImageTintColorAutomatically; + +/** + * 等价于 adjustsTitleTintColorAutomatically = YES & adjustsImageTintColorAutomatically = YES & tintColor = xxx + * @warning 不支持传 nil + */ +@property(nonatomic, strong) IBInspectable UIColor *tintColorAdjustsTitleAndImage; + +/** + * 是否自动调整highlighted时的按钮样式,默认为YES。
+ * 当值为YES时,按钮highlighted时会改变自身的alpha属性为ButtonHighlightedAlpha + */ +@property(nonatomic, assign) IBInspectable BOOL adjustsButtonWhenHighlighted; + +/** + * 是否自动调整disabled时的按钮样式,默认为YES。
+ * 当值为YES时,按钮disabled时会改变自身的alpha属性为ButtonDisabledAlpha + */ +@property(nonatomic, assign) IBInspectable BOOL adjustsButtonWhenDisabled; + +/** + * 设置按钮点击时的背景色,默认为nil。 + * @warning 不支持带透明度的背景颜色。当设置highlightedBackgroundColor时,会强制把adjustsButtonWhenHighlighted设为NO,避免两者效果冲突。 + * @see adjustsButtonWhenHighlighted + */ +@property(nonatomic, strong, nullable) IBInspectable UIColor *highlightedBackgroundColor; + +/** + * 设置按钮点击时的边框颜色,默认为nil。 + * @warning 当设置highlightedBorderColor时,会强制把adjustsButtonWhenHighlighted设为NO,避免两者效果冲突。 + * @see adjustsButtonWhenHighlighted + */ +@property(nonatomic, strong, nullable) IBInspectable UIColor *highlightedBorderColor; + +/** + * 设置按钮里图标和文字的相对位置,默认为QMUIButtonImagePositionLeft
+ * 可配合imageEdgeInsets、titleEdgeInsets、contentHorizontalAlignment、contentVerticalAlignment使用 + */ +@property(nonatomic, assign) QMUIButtonImagePosition imagePosition; + +/** + * 设置按钮里图标和文字之间的间隔,会自动响应 imagePosition 的变化而变化,默认为0。
+ * 系统默认实现需要同时设置 titleEdgeInsets 和 imageEdgeInsets,同时还需考虑 contentEdgeInsets 的增加(否则不会影响布局,可能会让图标或文字溢出或挤压),使用该属性可以避免以上情况。
+ * @warning 会与 imageEdgeInsets、 titleEdgeInsets、 contentEdgeInsets 共同作用。 + */ +@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 new file mode 100644 index 00000000..28944c00 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m @@ -0,0 +1,416 @@ +/** + * 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. + */ + +// +// QMUIButton.m +// qmui +// +// Created by QMUI Team on 14-7-7. +// + +#import "QMUIButton.h" +#import "QMUICore.h" +#import "CALayer+QMUI.h" +#import "UIButton+QMUI.h" +#import "QMUILayouter.h" + +const CGFloat QMUIButtonCornerRadiusAdjustsBounds = -1; + +@interface QMUIButton () + +@property(nonatomic, strong) CALayer *highlightedBackgroundLayer; +@property(nonatomic, strong) UIColor *originBorderColor; +@end + +@implementation QMUIButton + +@synthesize subtitleLabel = _qmuisubtitleLabel; + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.tintColor = ButtonTintColor; + [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; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self didInitialize]; + } + return self; +} + +- (void)didInitialize { + // 默认接管highlighted和disabled的表现,去掉系统默认的表现 + self.adjustsImageWhenHighlighted = NO; + self.adjustsImageWhenDisabled = NO; + self.adjustsButtonWhenHighlighted = YES; + self.adjustsButtonWhenDisabled = YES; + + // 图片默认在按钮左边,与系统UIButton保持一致 + self.imagePosition = QMUIButtonImagePositionLeft; + + _qmuisubtitleLabel = [[UILabel alloc] init]; + _qmuisubtitleLabel.textColor = self.subtitleColor; + _qmuisubtitleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; + + self.subtitleEdgeInsets = UIEdgeInsetsMake(4, 0, 0, 0); +} + +- (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; + 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; + + 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; + } + } + } else if (self.imagePosition == QMUIButtonImagePositionLeft || self.imagePosition == QMUIButtonImagePositionRight) { + if (horizontal == QMUILayouterAlignmentFill) { + if (image.visible && (title.visible || subtitle.visible)) { + titles.grow = QMUILayouterGrowMost; + } + } + if (vertical == QMUILayouterAlignmentFill) { + if (title.visible) { + title.grow = QMUILayouterGrowMost; + } else if (subtitle.visible) { + subtitle.grow = QMUILayouterGrowMost; + } + } + } + + 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 { + return [self sizeThatFits:CGSizeMax]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + if (CGRectIsEmpty(self.bounds)) { + return; + } + + if (self.cornerRadius == QMUIButtonCornerRadiusAdjustsBounds) { + self.layer.cornerRadius = CGRectGetHeight(self.bounds) / 2; + } + + 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 (CGRectGetHeight(rect) > CGRectGetHeight(limitRect)) { + rect = CGRectSetHeight(rect, CGRectGetHeight(limitRect)); + rect = CGRectSetY(rect, self.contentEdgeInsets.top + self.imageEdgeInsets.top); + } + imageView.frame = rect; + } +} + +- (void)setSpacingBetweenImageAndTitle:(CGFloat)spacingBetweenImageAndTitle { + _spacingBetweenImageAndTitle = spacingBetweenImageAndTitle; + + [self setNeedsLayout]; +} + +- (void)setImagePosition:(QMUIButtonImagePosition)imagePosition { + _imagePosition = imagePosition; + + [self setNeedsLayout]; +} + +- (void)setHighlightedBackgroundColor:(UIColor *)highlightedBackgroundColor { + _highlightedBackgroundColor = highlightedBackgroundColor; + if (_highlightedBackgroundColor) { + // 只要开启了highlightedBackgroundColor,就默认不需要alpha的高亮 + self.adjustsButtonWhenHighlighted = NO; + } +} + +- (void)setHighlightedBorderColor:(UIColor *)highlightedBorderColor { + _highlightedBorderColor = highlightedBorderColor; + if (_highlightedBorderColor) { + // 只要开启了highlightedBorderColor,就默认不需要alpha的高亮 + self.adjustsButtonWhenHighlighted = NO; + } +} + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + + if (highlighted && !self.originBorderColor) { + // 手指按在按钮上会不断触发setHighlighted:,所以这里做了保护,设置过一次就不用再设置了 + self.originBorderColor = [UIColor colorWithCGColor:self.layer.borderColor]; + } + + // 渲染背景色 + if (self.highlightedBackgroundColor || self.highlightedBorderColor) { + [self adjustsButtonHighlighted]; + } + // 如果此时是disabled,则disabled的样式优先 + if (!self.enabled) { + return; + } + // 自定义highlighted样式 + if (self.adjustsButtonWhenHighlighted) { + if (highlighted) { + self.alpha = ButtonHighlightedAlpha; + } else { + self.alpha = 1; + } + } +} + +- (void)setEnabled:(BOOL)enabled { + [super setEnabled:enabled]; + if (self.adjustsButtonWhenDisabled) { + self.alpha = enabled ? 1 : ButtonDisabledAlpha; + } +} + +- (void)adjustsButtonHighlighted { + if (self.highlightedBackgroundColor) { + if (!self.highlightedBackgroundLayer) { + self.highlightedBackgroundLayer = [CALayer layer]; + [self.highlightedBackgroundLayer qmui_removeDefaultAnimations]; + [self.layer insertSublayer:self.highlightedBackgroundLayer atIndex:0]; + } + 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; + } + + if (self.highlightedBorderColor) { + self.layer.borderColor = self.highlighted ? self.highlightedBorderColor.CGColor : self.originBorderColor.CGColor; + } +} + +- (void)setAdjustsTitleTintColorAutomatically:(BOOL)adjustsTitleTintColorAutomatically { + _adjustsTitleTintColorAutomatically = adjustsTitleTintColorAutomatically; + [self updateTitleColorIfNeeded]; +} + +- (void)updateTitleColorIfNeeded { + if (!self.adjustsTitleTintColorAutomatically) return; + if (self.currentTitleColor) { + [self setTitleColor:self.tintColor forState:UIControlStateNormal]; + } + 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 { + 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) { + continue; + } + if (number.unsignedIntegerValue != UIControlStateNormal && image == [self imageForState:UIControlStateNormal]) { + continue; + } + + 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]]; + } + } + } +} + +- (void)setImage:(UIImage *)image forState:(UIControlState)state { + if (self.adjustsImageTintColorAutomatically && image.renderingMode != UIImageRenderingModeAlwaysOriginal) { + image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + } + + [super setImage:image forState:state]; +} + +- (void)tintColorDidChange { + [super tintColorDidChange]; + + [self updateTitleColorIfNeeded]; + + if (self.adjustsImageTintColorAutomatically) { + [self updateImageRenderingModeIfNeeded]; + } +} + +- (void)setTintColorAdjustsTitleAndImage:(UIColor *)tintColorAdjustsTitleAndImage { + _tintColorAdjustsTitleAndImage = tintColorAdjustsTitleAndImage; + if (tintColorAdjustsTitleAndImage) { + self.tintColor = tintColorAdjustsTitleAndImage; + self.adjustsTitleTintColorAutomatically = YES; + self.adjustsImageTintColorAutomatically = YES; + } +} + +- (void)setCornerRadius:(CGFloat)cornerRadius { + _cornerRadius = cornerRadius; + if (cornerRadius != QMUIButtonCornerRadiusAdjustsBounds) { + self.layer.cornerRadius = cornerRadius; + } + [self setNeedsLayout]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h new file mode 100644 index 00000000..e46a483f --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h @@ -0,0 +1,85 @@ +/** + * 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. + */ + +// +// QMUINavigationButton.h +// QMUIKit +// +// Created by QMUI Team on 2018/4/9. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, QMUINavigationButtonType) { + QMUINavigationButtonTypeNormal, // 普通导航栏文字按钮 + QMUINavigationButtonTypeBold, // 导航栏加粗按钮 + QMUINavigationButtonTypeImage, // 图标按钮 + QMUINavigationButtonTypeBack // 自定义返回按钮(可以同时带有title) +}; + +/** + * QMUINavigationButton 有两部分组成: + * 一部分是 UIBarButtonItem (QMUINavigationButton),提供比系统更便捷的类方法来快速初始化一个 UIBarButtonItem,推荐首选这种方式(原则是能用系统的尽量用系统的,不满足才用自定义的)。 + * 另一部分就是 QMUINavigationButton,会提供一个按钮,作为 customView 给 UIBarButtonItem 使用,这种常用于自定义的返回按钮。 + * 对于第二种按钮,会尽量保证样式、布局看起来都和系统的 UIBarButtonItem 一致,所以内部做了许多 iOS 版本兼容的微调。 + */ +@interface QMUINavigationButton : UIButton + +/** + * 获取当前按钮的`QMUINavigationButtonType` + */ +@property(nonatomic, assign, readonly) QMUINavigationButtonType type; + +/** + * UIBarButtonItem 默认都是跟随 tintColor 的,所以这里声明是否让图片也是用 AlwaysTemplate 模式 + * 默认为 YES + */ +@property(nonatomic, assign) BOOL adjustsImageTintColorAutomatically; + +/** + * 导航栏按钮的初始化函数,指定的初始化方法 + * @param type 按钮类型 + * @param title 按钮的title + */ +- (instancetype)initWithType:(QMUINavigationButtonType)type title:(nullable NSString *)title; + +/** + * 导航栏按钮的初始化函数 + * @param type 按钮类型 + */ +- (instancetype)initWithType:(QMUINavigationButtonType)type; + +/** + * 导航栏按钮的初始化函数 + * @param image 按钮的image + */ +- (instancetype)initWithImage:(nullable UIImage *)image; + +@end + +@interface UIBarButtonItem (QMUINavigationButton) + ++ (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 + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m new file mode 100644 index 00000000..b79bdebb --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m @@ -0,0 +1,537 @@ +/** + * 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. + */ + +// +// QMUINavigationButton.m +// QMUIKit +// +// Created by QMUI Team on 2018/4/9. +// + +#import "QMUINavigationButton.h" +#import "QMUICore.h" +#import "UIImage+QMUI.h" +#import "UIColor+QMUI.h" +#import "UIViewController+QMUI.h" +#import "QMUINavigationController.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里调整位置 + QMUINavigationButtonPositionLeft, // 用于leftBarButtonItem,如果用于leftBarButtonItems,则只对最左边的item使用,其他item使用QMUINavigationButtonPositionNone + QMUINavigationButtonPositionRight, // 用于rightBarButtonItem,如果用于rightBarButtonItems,则只对最右边的item使用,其他item使用QMUINavigationButtonPositionNone +}; + +@interface QMUINavigationButton() + +@property(nonatomic, assign) QMUINavigationButtonPosition buttonPosition; +@property(nonatomic, strong) UIImage *defaultHighlightedImage;// 在 set normal image 时自动拿 normal image 加 alpha 作为 highlighted image +@property(nonatomic, strong) UIImage *defaultDisabledImage;// 在 set normal image 时自动拿 normal image 加 alpha 作为 disabled image +@end + + +@implementation QMUINavigationButton + +- (instancetype)init { + return [self initWithType:QMUINavigationButtonTypeNormal]; +} + +- (instancetype)initWithType:(QMUINavigationButtonType)type { + return [self initWithType:type title:nil]; +} + +- (instancetype)initWithType:(QMUINavigationButtonType)type title:(NSString *)title { + if (self = [super initWithFrame:CGRectZero]) { + _type = type; + self.buttonPosition = QMUINavigationButtonPositionNone; + [self setTitle:title forState:UIControlStateNormal]; + [self renderButtonStyle]; + [self sizeToFit]; + } + return self; +} + +- (instancetype)initWithImage:(UIImage *)image { + if (self = [self initWithType:QMUINavigationButtonTypeImage]) { + [self setImage:image forState:UIControlStateNormal]; + [self sizeToFit]; + } + return self; +} + +- (void)renderButtonStyle { + UIFont *font = NavBarButtonFont; + if (font) { + self.titleLabel.font = font; + } + self.titleLabel.backgroundColor = UIColorClear; + self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; + self.contentMode = UIViewContentModeCenter; + self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter; + self.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter; + self.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + + // UIBarButtonItem 默认都是跟随 tintColor 的,所以这里让图片也是用 alwaysTemplate 模式 + self.adjustsImageTintColorAutomatically = YES; + + if (self.type == QMUINavigationButtonTypeImage) { + // 让 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 + self.adjustsImageWhenHighlighted = NO; + self.adjustsImageWhenDisabled = NO; + + switch (self.type) { + case QMUINavigationButtonTypeNormal: + break; + case QMUINavigationButtonTypeImage: + // 拓展宽度,以保证用 leftBarButtonItems/rightBarButtonItems 时,按钮与按钮之间间距与系统的保持一致 + self.contentEdgeInsets = UIEdgeInsetsMake(0, 11, 0, 11); + break; + case QMUINavigationButtonTypeBold: { + font = NavBarButtonFontBold; + if (font) { + self.titleLabel.font = font; + } + } + break; + case QMUINavigationButtonTypeBack: { + self.qmui_outsideEdge = UIEdgeInsetsMake(-12, -12, -24, -24); + UIImage *backIndicatorImage = UINavigationBar.qmui_appearanceConfigured.backIndicatorImage; + if (!backIndicatorImage) { + // 配置表没有自定义的图片,则按照系统的返回按钮图片样式创建一张,颜色按照 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]; + [self setImage:[backIndicatorImage qmui_imageWithAlpha:NavBarHighlightedAlpha] forState:UIControlStateHighlighted]; + [self setImage:[backIndicatorImage qmui_imageWithAlpha:NavBarDisabledAlpha] forState:UIControlStateDisabled]; + + self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; + + // @warning 这些数值都是每个iOS版本核对过没问题的,如果修改则要检查要每个版本里与系统UIBarButtonItem的布局是否一致 + 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(0, + 0, + 0, + self.titleEdgeInsets.left); + } + break; + + default: + break; + } +} + +- (void)setImage:(UIImage *)image forState:(UIControlState)state { + if (image && self.adjustsImageTintColorAutomatically) { + image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + } + + if (image && [self imageForState:state] != image) { + if (state == UIControlStateNormal) { + // 将 normal image 处理成对应的 highlighted image 和 disabled image + self.defaultHighlightedImage = [[image qmui_imageWithAlpha:NavBarHighlightedAlpha] imageWithRenderingMode:image.renderingMode]; + [self setImage:self.defaultHighlightedImage forState:UIControlStateHighlighted]; + + self.defaultDisabledImage = [[image qmui_imageWithAlpha:NavBarDisabledAlpha] imageWithRenderingMode:image.renderingMode]; + [self setImage:self.defaultDisabledImage forState:UIControlStateDisabled]; + } else { + // 如果业务主动设置了非 normal 状态的 image,则把之前 QMUI 自动加上的两个 image 去掉,相当于认为业务希望完全控制这个按钮在所有 state 下的图片 + if (image != self.defaultHighlightedImage && image != self.defaultDisabledImage) { + if ([self imageForState:UIControlStateHighlighted] == self.defaultHighlightedImage && state != UIControlStateHighlighted) { + [self setImage:nil forState:UIControlStateHighlighted]; + } + if ([self imageForState:UIControlStateDisabled] == self.defaultDisabledImage && state != UIControlStateDisabled) { + [self setImage:nil forState:UIControlStateDisabled]; + } + } + } + } + + [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]; + [self setTitleColor:self.tintColor forState:UIControlStateNormal]; + [self setTitleColor:[self.tintColor colorWithAlphaComponent:NavBarHighlightedAlpha] forState:UIControlStateHighlighted]; + [self setTitleColor:[self.tintColor colorWithAlphaComponent:NavBarDisabledAlpha] forState:UIControlStateDisabled]; +} + +// 对按钮内容添加偏移,让UIBarButtonItem适配最新设备的系统行为,统一位置。注意 iOS 11 及以后,只有 image 类型的才会走进来 +- (UIEdgeInsets)alignmentRectInsets { + + UIEdgeInsets insets = [super alignmentRectInsets]; + + if (self.type == QMUINavigationButtonTypeNormal || self.type == QMUINavigationButtonTypeBold) { + // 对于奇数大小的字号,不同 iOS 版本的偏移策略不同,统一一下 + if (self.titleLabel.font.pointSize / 2.0 > 0) { + insets.top = -PixelOne; + insets.bottom = PixelOne; + } + } else if (self.type == QMUINavigationButtonTypeImage) { + // 图片类型的按钮,分别对最左、最右那个按钮调整 inset(这里与 UINavigationItem(QMUINavigationButton) 里的 position 赋值配合使用) + if (self.buttonPosition == QMUINavigationButtonPositionLeft) { + insets.left = 11; + } else if (self.buttonPosition == QMUINavigationButtonPositionRight) { + insets.right = 11; + } + + insets.top = 1; + } else if (self.type == QMUINavigationButtonTypeBack) { + insets.top = PixelOne; + } + + return insets; +} + +@end + +@implementation UIBarButtonItem (QMUINavigationButton) + ++ (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 [[self alloc] initWithCustomView:button]; +} + ++ (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:(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:(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 = [[self alloc] initWithCustomView:button]; + return barButtonItem; +} + ++ (instancetype)qmui_backItemWithTarget:(nullable id)target action:(nullable SEL)action { + NSString *backTitle = nil; + if (NeedsBackBarButtonItemTitle) { + backTitle = @"返回"; // 默认文字用返回 + if ([target isKindOfClass:[UIViewController class]]) { + UIViewController *viewController = (UIViewController *)target; + UIViewController *previousViewController = viewController.qmui_previousViewController; + if (previousViewController.navigationItem.backBarButtonItem) { + // 如果前一个界面有主动设置返回按钮的文字,则取这个文字 + backTitle = previousViewController.navigationItem.backBarButtonItem.title; + } else if ([viewController respondsToSelector:@selector(qmui_backBarButtonItemTitleWithPreviousViewController:)]) { + // 否则看是否有通过 QMUI 提供的接口来设置返回按钮的文字,有就用它的值 + backTitle = [((UIViewController *)viewController) qmui_backBarButtonItemTitleWithPreviousViewController:previousViewController]; + } else if (previousViewController.title) { + // 否则取上一个界面的标题 + backTitle = previousViewController.title; + } + } + } else { + backTitle = @" "; + } + + return [self qmui_backItemWithTitle:backTitle target:target action:action]; +} + ++ (instancetype)qmui_closeItemWithTarget:(nullable id)target action:(nullable SEL)action { + UIBarButtonItem *closeItem = [[self alloc] initWithImage:NavBarCloseButtonImage style:UIBarButtonItemStylePlain target:target action:action]; + closeItem.accessibilityLabel = @"关闭"; + return closeItem; +} + ++ (instancetype)qmui_fixedSpaceItemWithWidth:(CGFloat)width { + UIBarButtonItem *item = [[self alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:NULL]; + item.width = width; + return item; +} + ++ (instancetype)qmui_flexibleSpaceItem { + return [[self alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:NULL]; +} + +@end + +@interface UIBarButtonItem (QMUINavigationButton_Private) + +/// 判断当前的 UIBarButtonItem 是否是 QMUINavigationButton +@property(nonatomic, assign, readonly) BOOL qmui_isCustomizedBarButtonItem; + +/// 判断当前的 UIBarButtonItem 是否是用 QMUINavigationButton 自定义返回按钮生成的 +@property(nonatomic, assign, readonly) BOOL qmui_isCustomizedBackBarButtonItem; + +/// 获取内部的 QMUINavigationButton(如果有的话) +@property(nonatomic, strong, readonly) QMUINavigationButton *qmui_navigationButton; +@end + +@interface UIViewController (QMUINavigationButton) + +@end + +@interface UINavigationBar (QMUINavigationButton) + +/// 判断当前的 UINavigationBar 的返回按钮是不是自定义的 +@property(nonatomic, readonly) BOOL qmui_customizingBackBarButtonItem; +@end + +@implementation UIBarButtonItem (QMUINavigationButton_Private) + +- (BOOL)qmui_isCustomizedBarButtonItem { + if (!self.customView) { + return NO; + } + return [self.customView isKindOfClass:[QMUINavigationButton class]]; +} + +- (BOOL)qmui_isCustomizedBackBarButtonItem { + return self.qmui_isCustomizedBarButtonItem && ((QMUINavigationButton *)self.customView).type == QMUINavigationButtonTypeBack; +} + +- (QMUINavigationButton *)qmui_navigationButton { + if ([self.customView isKindOfClass:[QMUINavigationButton class]]) { + return (QMUINavigationButton *)self.customView; + } + return nil; +} + +@end + +@implementation UINavigationItem (QMUINavigationButton) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + SEL selectors[] = { + @selector(setLeftBarButtonItem:animated:), + @selector(setLeftBarButtonItems:animated:), + @selector(setRightBarButtonItem:animated:), + @selector(setRightBarButtonItems:animated:), + }; + for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); index++) { + SEL originalSelector = selectors[index]; + SEL swizzledSelector = NSSelectorFromString([@"qmui_" stringByAppendingString:NSStringFromSelector(originalSelector)]); + ExchangeImplementations([UINavigationItem class], originalSelector, swizzledSelector); + } + }); +} + +- (void)qmui_setLeftBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated { + [self qmui_setLeftBarButtonItem:item animated:animated]; + + // 自动给 position 赋值 + item.qmui_navigationButton.buttonPosition = QMUINavigationButtonPositionLeft; +} + +- (void)qmui_setLeftBarButtonItems:(NSArray *)items animated:(BOOL)animated { + [self qmui_setLeftBarButtonItems:items animated:animated]; + + // 自动给 position 赋值 + for (NSInteger i = 0; i < items.count; i++) { + if (i == 0) { + items[i].qmui_navigationButton.buttonPosition = QMUINavigationButtonPositionLeft; + } else { + items[i].qmui_navigationButton.buttonPosition = QMUINavigationButtonPositionNone; + } + } +} + +- (void)qmui_setRightBarButtonItem:(UIBarButtonItem *)item animated:(BOOL)animated { + [self qmui_setRightBarButtonItem:item animated:animated]; + + // 自动给 position 赋值 + item.qmui_navigationButton.buttonPosition = QMUINavigationButtonPositionRight; +} + +- (void)qmui_setRightBarButtonItems:(NSArray *)items animated:(BOOL)animated { + [self qmui_setRightBarButtonItems:items animated:animated]; + + // 自动给 position 赋值 + for (NSInteger i = 0; i < items.count; i++) { + if (i == 0) { + items[i].qmui_navigationButton.buttonPosition = QMUINavigationButtonPositionRight; + } else { + items[i].qmui_navigationButton.buttonPosition = QMUINavigationButtonPositionNone; + } + } +} + +@end + +@implementation UIViewController (QMUINavigationButton) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // 当使用自定义返回按钮时,无法使用 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; + }; + }); + }); +} + +@end + +@implementation UINavigationBar (QMUINavigationButton) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // 强制修改 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) { + + // call super + NSDirectionalEdgeInsets (*originSelectorIMP)(id, SEL); + originSelectorIMP = (NSDirectionalEdgeInsets (*)(id, SEL))originalIMPProvider(); + NSDirectionalEdgeInsets originResult = originSelectorIMP(selfObject, originCMD); + + // get navbar + UINavigationBar *navBar = nil; + if ([NSStringFromClass([selfObject class]) isEqualToString:barContentViewString] && + [selfObject.superview isKindOfClass:[UINavigationBar class]]) { + navBar = (UINavigationBar *)selfObject.superview; + } + + // change insets + if (navBar) { + NSDirectionalEdgeInsets value = originResult; + value.leading -= (navBar.qmui_customizingBackBarButtonItem ? 8 : 0); + return value; + } + + return originResult; + }; + }); + + // 系统的 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; + + NSMutableArray *customViews = [[NSMutableArray alloc] init]; + if (selfObject.topItem.titleView) { + [customViews addObject:selfObject.topItem.titleView]; + } + [customViews addObjectsFromArray:[selfObject.topItem.leftBarButtonItems qmui_compactMapWithBlock:^id _Nullable(UIBarButtonItem * _Nonnull item) { + return item.customView ?: nil; + }]]; + [customViews addObjectsFromArray:[selfObject.topItem.rightBarButtonItems qmui_compactMapWithBlock:^id _Nullable(UIBarButtonItem * _Nonnull item) { + return item.customView ?: nil; + }]]; + UIView *hitTestingView = [customViews qmui_firstMatchWithBlock:^BOOL(UIView * _Nonnull item) { + if (!CGRectIsEmpty(item.frame) && !item.hidden && item.alpha > 0.01 && item.window) { + if ([item isKindOfClass:UIControl.class] && !((UIControl *)item).enabled) { + return NO; + } + CGRect rect = [selfObject convertRect:item.bounds fromView:item]; + rect = CGRectInsetEdges(rect, item.qmui_outsideEdge); + if (CGRectContainsPoint(rect, firstArgv)) { + return YES; + } + } + return NO; + }]; + if (hitTestingView) { + return hitTestingView; + } + return result; + }; + }); + }); +} + +- (BOOL)qmui_customizingBackBarButtonItem { + 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 new file mode 100644 index 00000000..50175867 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.h @@ -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. + */ + +// +// QMUIToolbarButton.h +// QMUIKit +// +// Created by QMUI Team on 2018/4/9. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, QMUIToolbarButtonType) { + QMUIToolbarButtonTypeNormal, // 普通工具栏按钮 + QMUIToolbarButtonTypeRed, // 工具栏红色按钮,用于删除等警告性操作 + QMUIToolbarButtonTypeImage, // 图标类型的按钮 +}; + +/** + * `QMUIToolbarButton`是用于底部工具栏的按钮 + */ +@interface QMUIToolbarButton : UIButton + +/// 获取当前按钮的type +@property(nonatomic, assign, readonly) QMUIToolbarButtonType type; + +/** + * 工具栏按钮的初始化函数 + * @param type 按钮类型 + */ +- (instancetype)initWithType:(QMUIToolbarButtonType)type; + +/** + * 工具栏按钮的初始化函数 + * @param type 按钮类型 + * @param title 按钮的title + */ +- (instancetype)initWithType:(QMUIToolbarButtonType)type title:(nullable NSString *)title; + +/** + * 工具栏按钮的初始化函数 + * @param image 按钮的image + */ +- (instancetype)initWithImage:(UIImage *)image; + +/// 在原有的QMUIToolbarButton上创建一个UIBarButtonItem ++ (nullable UIBarButtonItem *)barButtonItemWithToolbarButton:(QMUIToolbarButton *)button target:(nullable id)target action:(nullable SEL)selector; + +/// 创建一个特定type的UIBarButtonItem ++ (nullable UIBarButtonItem *)barButtonItemWithType:(QMUIToolbarButtonType)type title:(nullable NSString *)title target:(nullable id)target action:(nullable SEL)selector; + +/// 创建一个图标类型的UIBarButtonItem ++ (nullable UIBarButtonItem *)barButtonItemWithImage:(nullable UIImage *)image target:(nullable id)target action:(nullable SEL)selector; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m new file mode 100644 index 00000000..6a7b5652 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m @@ -0,0 +1,93 @@ +/** + * 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. + */ + +// +// QMUIToolbarButton.m +// QMUIKit +// +// Created by QMUI Team on 2018/4/9. +// + +#import "QMUIToolbarButton.h" +#import "QMUICore.h" +#import "UIImage+QMUI.h" + +@implementation QMUIToolbarButton + +- (instancetype)init { + return [self initWithType:QMUIToolbarButtonTypeNormal]; +} + +- (instancetype)initWithType:(QMUIToolbarButtonType)type { + return [self initWithType:type title:nil]; +} + +- (instancetype)initWithType:(QMUIToolbarButtonType)type title:(NSString *)title { + if (self = [super init]) { + _type = type; + [self setTitle:title forState:UIControlStateNormal]; + [self renderButtonStyle]; + [self sizeToFit]; + } + return self; +} + +- (instancetype)initWithImage:(UIImage *)image { + if (self = [self initWithType:QMUIToolbarButtonTypeImage]) { + [self setImage:image forState:UIControlStateNormal]; + [self setImage:[image qmui_imageWithAlpha:ToolBarHighlightedAlpha] forState:UIControlStateHighlighted]; + [self setImage:[image qmui_imageWithAlpha:ToolBarDisabledAlpha] forState:UIControlStateDisabled]; + [self sizeToFit]; + } + return self; +} + +- (void)renderButtonStyle { + self.imageView.contentMode = UIViewContentModeCenter; + self.imageView.tintColor = nil; // 重置默认值,nil表示跟随父元素 + self.titleLabel.font = ToolBarButtonFont; + switch (self.type) { + case QMUIToolbarButtonTypeNormal: + [self setTitleColor:ToolBarTintColor forState:UIControlStateNormal]; + [self setTitleColor:ToolBarTintColorHighlighted forState:UIControlStateHighlighted]; + [self setTitleColor:ToolBarTintColorDisabled forState:UIControlStateDisabled]; + break; + case QMUIToolbarButtonTypeRed: + [self setTitleColor:UIColorRed forState:UIControlStateNormal]; + [self setTitleColor:[UIColorRed colorWithAlphaComponent:ToolBarHighlightedAlpha] forState:UIControlStateHighlighted]; + [self setTitleColor:[UIColorRed colorWithAlphaComponent:ToolBarDisabledAlpha] forState:UIControlStateDisabled]; + self.imageView.tintColor = UIColorRed; // 修改为红色 + break; + case QMUIToolbarButtonTypeImage: + break; + default: + break; + } +} + ++ (UIBarButtonItem *)barButtonItemWithToolbarButton:(QMUIToolbarButton *)button target:(id)target action:(SEL)selector { + [button addTarget:target action:selector forControlEvents:UIControlEventTouchUpInside]; + UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithCustomView:button]; + return buttonItem; +} + ++ (UIBarButtonItem *)barButtonItemWithType:(QMUIToolbarButtonType)type title:(NSString *)title target:(id)target action:(SEL)selector { + UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:target action:selector]; + if (type == QMUIToolbarButtonTypeRed) { + // 默认继承toolBar的tintColor,红色需要重置 + buttonItem.tintColor = UIColorRed; + } + return buttonItem; +} + ++ (UIBarButtonItem *)barButtonItemWithImage:(UIImage *)image target:(id)target action:(SEL)selector { + UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStylePlain target:target action:selector]; + return buttonItem; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.h new file mode 100644 index 00000000..cd123fd7 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.h @@ -0,0 +1,182 @@ +/** + * 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. + */ + +// +// QMUICellHeightCache.h +// qmui +// +// Created by QMUI Team on 15/12/23. +// + +#import +#import + +@interface QMUICellHeightCache : NSObject + +- (BOOL)existsHeightForKey:(id)key; +- (void)cacheHeight:(CGFloat)height byKey:(id)key; +- (CGFloat)heightForKey:(id)key; +- (void)invalidateHeightForKey:(id)key; +- (void)invalidateAllHeightCache; + +@end + +@interface QMUICellHeightIndexPathCache : NSObject + +@property(nonatomic, assign) BOOL automaticallyInvalidateEnabled;// TODO: 这个要放在 tableView 那边 + +- (BOOL)existsHeightAtIndexPath:(NSIndexPath *)indexPath; +- (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath; +- (CGFloat)heightForIndexPath:(NSIndexPath *)indexPath; +- (void)invalidateHeightAtIndexPath:(NSIndexPath *)indexPath; +- (void)invalidateAllHeightCache; + +@end + +/// ====================== 动态计算 cell 高度相关 ======================= + +/** + * UITableView 定义了一套动态计算 cell 高度的方式: + * + * 其思路是参考开源代码:https://github.com/forkingdog/UITableView-FDTemplateLayoutCell。 + * + * 1. cell 必须实现 sizeThatFits: 方法,在里面计算自身的高度并返回 + * 2. 初始化一个 QMUITableView,并为其指定一个 QMUITableViewDataSource + * 3. 实现 qmui_tableView:cellWithIdentifier: 方法,在里面为不同的 identifier 创建不同的 cell 实例 + * 4. 在 tableView:cellForRowAtIndexPath: 里使用 qmui_tableView:cellWithIdentifier: 获取 cell + * 5. 在 tableView:heightForRowAtIndexPath: 里使用 UITableView (QMUILayoutCell) 提供的几种方法得到 cell 的高度 + * 6. 当某个 cell 的缓存需要主动刷新时,请调用 UITableView 的 qmui_invalidateXxx 系列方法。 + * + * 这套方式的好处是 tableView 能直接操作 cell 的实例,cell 无需增加额外的专门用于获取 cell 高度的方法。并且这套方式支持基本的高度缓存(可按 key 缓存或按 indexPath 缓存),若使用了缓存,请注意在适当的时机去更新缓存(例如某个 cell 的内容发生变化,可能 cell 的高度也会变化,则需要更新这个 cell 已被缓存起来的高度)。 + * + * 使用这套方式额外的消耗是每个 identifier 都会生成一个多余的 cell 实例(专用于高度计算),但大部分情况下一个生成一个 cell 实例并不会带来过多的负担,所以一般不用担心这个问题。 + + * @note 当 tableView 的宽度发生变化时,缓存会自动刷新,所以无需自己监听横竖屏旋转、viewWillTransitionToSize: 等事件。 + * + * @note 注意,如果你的 tableView 可以使用 estimatedRowHeight,则建议使用 UITableView (QMUICellHeightKeyCache) 代替本控件,可节省大量代码。 + * + * @see UITableView (QMUICellHeightKeyCache) + */ + +@interface UITableView (QMUILayoutCell) + +/** + * 通过 qmui_tableView:cellWithIdentifier: 得到 identifier 对应的 cell 实例,并在 configuration 里对 cell 进行渲染后,得到 cell 的高度。 + * @param identifier cell 的 identifier + * @param configuration 用于渲染 cell 的block,一般与 tableView:cellForRowAtIndexPath: 里渲染 cell 的代码一样 + */ +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(__kindof UITableViewCell *cell))configuration; + +/** + * 通过 qmui_tableView:cellWithIdentifier: 得到 identifier 对应的 cell 实例,并在 configuration 里对 cell 进行渲染后,得到 cell 的高度。 + * + * 以 indexPath 为单位进行缓存,相同的 indexPath 高度将不会重复计算,若需刷新高度,请参考 QMUICellHeightIndexPathCache + * + * @param identifier cell 的 identifier + * @param configuration 用于渲染 cell 的block,一般与 tableView:cellForRowAtIndexPath: 里渲染 cell 的代码一样 + */ +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UITableViewCell *cell))configuration; + +/** + * 通过 qmui_tableView:cellWithIdentifier: 得到 identifier 对应的 cell 实例,并在 configuration 里对 cell 进行渲染后,得到 cell 的高度。 + * + * 以自定义的 key 为单位进行缓存,相同的 key 高度将不会重复计算,若需刷新高度,请参考 QMUICellHeightCache + * + * @param identifier cell 的 identifier + * @param configuration 用于渲染 cell 的block,一般与 tableView:cellForRowAtIndexPath: 里渲染 cell 的代码一样 + */ +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id)key configuration:(void (^)(__kindof UITableViewCell *cell))configuration; + +/// 搭配 QMUICellHeightCache,清除整个列表的所有高度缓存(包括 key 和 indexPath),注意请不要直接使用 self.qmui_keyedHeightCache 或 self.qmui_indexPathHeightCache 的 invalidate 方法,因为一个 UITableView 在不同宽度下会有不同的 QMUICellHeightCache/QMUICellHeightIndexPathCache,直接使用那两个 cache 的 invalidate 方法只能刷新当前的 cache,无法刷新其他宽度下的 cache。 +- (void)qmui_invalidateAllHeight; + +@end + +@interface UITableView (QMUIKeyedHeightCache) + +/// 在 UITableView 不同的宽度下会得到不一样的 QMUICellHeightCache 实例,从而保证宽度变化时缓存自动刷新 +@property(nonatomic, strong, readonly) QMUICellHeightCache *qmui_keyedHeightCache; + +/// 搭配 QMUICellHeightCache,清除指定 key 的高度缓存,注意请不要直接使用 [self.qmui_keyedHeightCache invalidateHeightForKey:],因为一个 UITableView 在不同宽度下会有不同的 QMUICellHeightCache,直接使用那个 cache 的 invalidate 方法只能刷新当前的 cache,无法刷新其他宽度下的 cache。 +- (void)qmui_invalidateHeightForKey:(id)key; + +@end + +@interface UITableView (QMUICellHeightIndexPathCache) + +/// YES 表示在 reloadData、reloadIndexPath: 等方法被调用时,对应的缓存也会被自动更新,默认为 YES。仅对 indexPath 方式的缓存有效。 +@property(nonatomic, assign) BOOL qmui_invalidateIndexPathHeightCachedAutomatically; + +/// 在 UICollectionView 不同的大小下会得到不一样的 QMUICellHeightIndexPathCache 实例,从而保证大小变化时缓存自动刷新 +@property(nonatomic, strong, readonly) QMUICellHeightIndexPathCache *qmui_indexPathHeightCache; + +/// 搭配 QMUICellHeightIndexPathCache,清除指定 indexPath 的高度缓存,注意请不要直接使用 [self.qmui_indexPathHeightCache invalidateHeightAtIndexPath:],因为一个 UITableView 在不同宽度下会有不同的 QMUICellHeightIndexPathCache,直接使用那个 cache 的 invalidate 方法只能刷新当前的 cache,无法刷新其他宽度下的 cache。 +- (void)qmui_invalidateHeightAtIndexPath:(NSIndexPath *)indexPath; + +@end + +@interface UITableView (QMUIIndexPathHeightCacheInvalidation) + +/// 当需要 reloadData 的时候,又不想使缓存失效,可以调用下面这个方法。注意,仅在 qmui_invalidateIndexPathHeightCachedAutomatically 为 YES 时才有意义。 +- (void)qmui_reloadDataWithoutInvalidateIndexPathHeightCache; + +@end + +/// ====================== 计算动态cell高度相关 ======================= + +/** + * UICollectionView 定义了一套动态计算 cell 高度的方式。 + * 原理类似 UITableView,具体请参考 UITableView (QMUILayoutCell)。 + */ + +@interface UICollectionView (QMUIKeyedHeightCache) + +/// 在 UICollectionView 不同的大小下会得到不一样的 QMUICellHeightCache 实例,从而保证大小变化时缓存自动刷新 +@property(nonatomic, strong, readonly) QMUICellHeightCache *qmui_keyedHeightCache; + +/// 搭配 QMUICellHeightCache,清除指定 key 的高度缓存,注意请不要直接使用 [self.qmui_keyedHeightCache invalidateHeightForKey:],因为一个 UICollectionView 在不同宽度下会有不同的 QMUICellHeightCache,直接使用那个 cache 的 invalidate 方法只能刷新当前的 cache,无法刷新其他宽度下的 cache。 +- (void)qmui_invalidateHeightForKey:(id)key; +@end + +@interface UICollectionView (QMUICellHeightIndexPathCache) + +/// YES 表示在 reloadData、reloadIndexPath: 等方法被调用时,对应的缓存也会被自动更新,默认为 YES。仅对 indexPath 方式的缓存有效。 +@property(nonatomic, assign) BOOL qmui_invalidateIndexPathHeightCachedAutomatically; + +/// 在 UICollectionView 不同的大小下会得到不一样的 QMUICellHeightIndexPathCache 实例,从而保证大小变化时缓存自动刷新 +@property(nonatomic, strong, readonly) QMUICellHeightIndexPathCache *qmui_indexPathHeightCache; + +/// 搭配 QMUICellHeightIndexPathCache,清除指定 indexPath 的高度缓存,注意请不要直接使用 [self.qmui_indexPathHeightCache invalidateHeightAtIndexPath:],因为一个 UICollectionView 在不同宽度下会有不同的 QMUICellHeightIndexPathCache,直接使用那个 cache 的 invalidate 方法只能刷新当前的 cache,无法刷新其他宽度下的 cache。 +- (void)qmui_invalidateHeightAtIndexPath:(NSIndexPath *)indexPath; + +@end + +@interface UICollectionView (QMUIIndexPathHeightCacheInvalidation) + +/// 当需要 reloadData 的时候,又不想使缓存失效,可以调用下面这个方法。注意,仅在 qmui_invalidateIndexPathHeightCachedAutomatically 为 YES 时才有意义。 +- (void)qmui_reloadDataWithoutInvalidateIndexPathHeightCache; + +@end + +/// 以下接口可在“sizeForItemAtIndexPath”里面调用来计算高度 +/// 通过构建一个cell模拟真正显示的cell,给cell设置真实的数据,然后再调用cell的sizeThatFits:来计算高度 +/// 也就是说我们自定义的cell里面需要重写sizeThatFits:并返回正确的值 +@interface UICollectionView (QMUILayoutCell) + +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration; + +// 通过indexPath缓存高度 +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration; + +// 通过key缓存高度 +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByKey:(id)key configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration; + +/// 搭配 QMUICellHeightCache,清除整个列表的所有高度缓存(包括 key 和 indexPath),注意请不要直接使用 self.qmui_keyedHeightCache 或 self.qmui_indexPathHeightCache 的 invalidate 方法,因为一个 UICollectionView 在不同宽度下会有不同的 QMUICellHeightCache/QMUICellHeightIndexPathCache,直接使用那两个 cache 的 invalidate 方法只能刷新当前的 cache,无法刷新其他宽度下的 cache。 +- (void)qmui_invalidateAllHeight; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.m new file mode 100644 index 00000000..75875590 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightCache.m @@ -0,0 +1,738 @@ +/** + * 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. + */ + +// +// QMUICellHeightCache.m +// qmui +// +// Created by QMUI Team on 15/12/23. +// + +#import "QMUICellHeightCache.h" +#import "QMUITableViewProtocols.h" +#import "QMUICore.h" +#import "UIScrollView+QMUI.h" +#import "UITableView+QMUI.h" +#import "UIView+QMUI.h" +#import "NSNumber+QMUI.h" + +const CGFloat kQMUICellHeightInvalidCache = -1; + +@interface QMUICellHeightCache () + +@property(nonatomic, strong) NSMutableDictionary, NSNumber *> *cachedHeights; +@end + +@implementation QMUICellHeightCache + +- (instancetype)init { + self = [super init]; + if (self) { + self.cachedHeights = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (BOOL)existsHeightForKey:(id)key { + NSNumber *number = self.cachedHeights[key]; + return number && ![number isEqualToNumber:@(kQMUICellHeightInvalidCache)]; +} + +- (void)cacheHeight:(CGFloat)height byKey:(id)key { + self.cachedHeights[key] = @(height); +} + +- (CGFloat)heightForKey:(id)key { + return self.cachedHeights[key].qmui_CGFloatValue; +} + +- (void)invalidateHeightForKey:(id)key { + [self.cachedHeights removeObjectForKey:key]; +} + +- (void)invalidateAllHeightCache { + [self.cachedHeights removeAllObjects]; +} + +@end + +@interface QMUICellHeightIndexPathCache () + +@property(nonatomic, strong) NSMutableArray *> *cachedHeights; +@end + +@implementation QMUICellHeightIndexPathCache + +- (instancetype)init { + self = [super init]; + if (self) { + self.automaticallyInvalidateEnabled = YES; + self.cachedHeights = [[NSMutableArray alloc] init]; + } + return self; +} + +- (BOOL)existsHeightAtIndexPath:(NSIndexPath *)indexPath { + [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; + NSNumber *number = self.cachedHeights[indexPath.section][indexPath.row]; + return number && ![number isEqualToNumber:@(kQMUICellHeightInvalidCache)]; +} + +- (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath { + [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; + self.cachedHeights[indexPath.section][indexPath.row] = @(height); +} + +- (CGFloat)heightForIndexPath:(NSIndexPath *)indexPath { + [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; + return self.cachedHeights[indexPath.section][indexPath.row].qmui_CGFloatValue; +} + +- (void)invalidateHeightInSection:(NSInteger)section { + [self buildSectionsIfNeeded:section]; + [self.cachedHeights[section] removeAllObjects]; +} + +- (void)invalidateHeightAtIndexPath:(NSIndexPath *)indexPath { + [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; + self.cachedHeights[indexPath.section][indexPath.row] = @(kQMUICellHeightInvalidCache); +} + +- (void)invalidateAllHeightCache { + [self.cachedHeights enumerateObjectsUsingBlock:^(NSMutableArray * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [obj removeAllObjects]; + }]; +} + +- (void)buildCachesAtIndexPathsIfNeeded:(NSArray *)indexPaths { + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + [self buildSectionsIfNeeded:indexPath.section]; + [self buildRowsIfNeeded:indexPath.row inExistSection:indexPath.section]; + }]; +} + +- (void)buildSectionsIfNeeded:(NSInteger)targetSection { + for (NSInteger section = 0; section <= targetSection; ++section) { + if (section >= self.cachedHeights.count) { + [self.cachedHeights addObject:[[NSMutableArray alloc] init]]; + } + } +} + +- (void)buildRowsIfNeeded:(NSInteger)targetRow inExistSection:(NSInteger)section { + NSMutableArray *heightsInSection = self.cachedHeights[section]; + for (NSInteger row = 0; row <= targetRow; ++row) { + if (row >= heightsInSection.count) { + [heightsInSection addObject:@(kQMUICellHeightInvalidCache)]; + } + } +} + +@end + +#pragma mark - UITableView Height Cache + +/// ====================== 计算动态cell高度相关 ======================= + +@interface UITableView () + +/// key 为 tableView 的内容宽度,value 为该宽度下对应的缓存容器,从而保证 tableView 宽度变化时缓存也会跟着刷新 +@property(nonatomic, strong) NSMutableDictionary *qmuiTableCache_allKeyedHeightCaches; +@property(nonatomic, strong) NSMutableDictionary *qmuiTableCache_allIndexPathHeightCaches; +@end + +@implementation UITableView (QMUIKeyedHeightCache) + +QMUISynthesizeIdStrongProperty(qmuiTableCache_allKeyedHeightCaches, setQmuiTableCache_allKeyedHeightCaches) + +- (QMUICellHeightCache *)qmui_keyedHeightCache { + if (!self.qmuiTableCache_allKeyedHeightCaches) { + self.qmuiTableCache_allKeyedHeightCaches = [[NSMutableDictionary alloc] init]; + } + CGFloat contentWidth = self.qmui_validContentWidth; + QMUICellHeightCache *cache = self.qmuiTableCache_allKeyedHeightCaches[@(contentWidth)]; + if (!cache) { + cache = [[QMUICellHeightCache alloc] init]; + self.qmuiTableCache_allKeyedHeightCaches[@(contentWidth)] = cache; + } + return cache; +} + +- (void)qmui_invalidateHeightForKey:(id)aKey { + [self.qmuiTableCache_allKeyedHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj invalidateHeightForKey:aKey]; + }]; +} + +@end + +@implementation UITableView (QMUICellHeightIndexPathCache) + +QMUISynthesizeIdStrongProperty(qmuiTableCache_allIndexPathHeightCaches, setQmuiTableCache_allIndexPathHeightCaches) +QMUISynthesizeBOOLProperty(qmui_invalidateIndexPathHeightCachedAutomatically, setQmui_invalidateIndexPathHeightCachedAutomatically) + +- (QMUICellHeightIndexPathCache *)qmui_indexPathHeightCache { + if (!self.qmuiTableCache_allIndexPathHeightCaches) { + self.qmuiTableCache_allIndexPathHeightCaches = [[NSMutableDictionary alloc] init]; + } + CGFloat contentWidth = self.qmui_validContentWidth; + QMUICellHeightIndexPathCache *cache = self.qmuiTableCache_allIndexPathHeightCaches[@(contentWidth)]; + if (!cache) { + cache = [[QMUICellHeightIndexPathCache alloc] init]; + self.qmuiTableCache_allIndexPathHeightCaches[@(contentWidth)] = cache; + } + return cache; +} + +- (void)qmui_invalidateHeightAtIndexPath:(NSIndexPath *)indexPath { + [self.qmuiTableCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj invalidateHeightAtIndexPath:indexPath]; + }]; +} + +@end + +@implementation UITableView (QMUIIndexPathHeightCacheInvalidation) + +- (void)qmui_reloadDataWithoutInvalidateIndexPathHeightCache { + [self qmuiTableCache_reloadData]; +} + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + SEL selectors[] = { + @selector(initWithFrame:style:), + @selector(initWithCoder:), + @selector(reloadData), + @selector(insertSections:withRowAnimation:), + @selector(deleteSections:withRowAnimation:), + @selector(reloadSections:withRowAnimation:), + @selector(moveSection:toSection:), + @selector(insertRowsAtIndexPaths:withRowAnimation:), + @selector(deleteRowsAtIndexPaths:withRowAnimation:), + @selector(reloadRowsAtIndexPaths:withRowAnimation:), + @selector(moveRowAtIndexPath:toIndexPath:) + }; + for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) { + SEL originalSelector = selectors[index]; + SEL swizzledSelector = NSSelectorFromString([@"qmuiTableCache_" stringByAppendingString:NSStringFromSelector(originalSelector)]); + ExchangeImplementations([UITableView class], originalSelector, swizzledSelector); + } + }); +} + +- (instancetype)qmuiTableCache_initWithFrame:(CGRect)frame style:(UITableViewStyle)style { + [self qmuiTableCache_initWithFrame:frame style:style]; + [self qmuiTableCache_didInitialize]; + return self; +} + +- (instancetype)qmuiTableCache_initWithCoder:(NSCoder *)aDecoder { + [self qmuiTableCache_initWithCoder:aDecoder]; + [self qmuiTableCache_didInitialize]; + return self; +} + +- (void)qmuiTableCache_didInitialize { + self.qmui_invalidateIndexPathHeightCachedAutomatically = YES; +} + +- (void)qmuiTableCache_reloadData { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiTableCache_allIndexPathHeightCaches removeAllObjects]; + } + [self qmuiTableCache_reloadData]; +} + +- (void)qmuiTableCache_insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) { + [self.qmuiTableCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildSectionsIfNeeded:section]; + [obj.cachedHeights insertObject:[[NSMutableArray alloc] init] atIndex:section]; + }]; + }]; + } + [self qmuiTableCache_insertSections:sections withRowAnimation:animation]; +} + +- (void)qmuiTableCache_deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) { + [self.qmuiTableCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildSectionsIfNeeded:section]; + [obj.cachedHeights removeObjectAtIndex:section]; + }]; + }]; + } + [self qmuiTableCache_deleteSections:sections withRowAnimation:animation]; +} + +- (void)qmuiTableCache_reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [sections enumerateIndexesUsingBlock: ^(NSUInteger section, BOOL *stop) { + [self.qmuiTableCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildSectionsIfNeeded:section]; + [obj invalidateHeightInSection:section]; + }]; + }]; + } + [self qmuiTableCache_reloadSections:sections withRowAnimation:animation]; +} + +- (void)qmuiTableCache_moveSection:(NSInteger)section toSection:(NSInteger)newSection { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiTableCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildSectionsIfNeeded:section]; + [obj buildSectionsIfNeeded:newSection]; + [obj.cachedHeights exchangeObjectAtIndex:section withObjectAtIndex:newSection]; + }]; + } + [self qmuiTableCache_moveSection:section toSection:newSection]; +} + +- (void)qmuiTableCache_insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiTableCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildCachesAtIndexPathsIfNeeded:indexPaths]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull indexPath, NSUInteger idx, BOOL * _Nonnull stop) { + NSMutableArray *heightsInSection = obj.cachedHeights[indexPath.section]; + [heightsInSection insertObject:@(kQMUICellHeightInvalidCache) atIndex:indexPath.row]; + }]; + }]; + } + [self qmuiTableCache_insertRowsAtIndexPaths:indexPaths withRowAnimation:animation]; +} + +- (void)qmuiTableCache_deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiTableCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildCachesAtIndexPathsIfNeeded:indexPaths]; + NSMutableDictionary *mutableIndexSetsToRemove = [NSMutableDictionary dictionary]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + NSMutableIndexSet *mutableIndexSet = mutableIndexSetsToRemove[@(indexPath.section)]; + if (!mutableIndexSet) { + mutableIndexSet = [NSMutableIndexSet indexSet]; + mutableIndexSetsToRemove[@(indexPath.section)] = mutableIndexSet; + } + [mutableIndexSet addIndex:indexPath.row]; + }]; + [mutableIndexSetsToRemove enumerateKeysAndObjectsUsingBlock:^(NSNumber *aKey, NSIndexSet *indexSet, BOOL *stop) { + NSMutableArray *heightsInSection = obj.cachedHeights[aKey.integerValue]; + [heightsInSection removeObjectsAtIndexes:indexSet]; + }]; + }]; + } + [self qmuiTableCache_deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation]; +} + +- (void)qmuiTableCache_reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiTableCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildCachesAtIndexPathsIfNeeded:indexPaths]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + NSMutableArray *heightsInSection = obj.cachedHeights[indexPath.section]; + heightsInSection[indexPath.row] = @(kQMUICellHeightInvalidCache); + }]; + }]; + } + [self qmuiTableCache_reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation]; +} + +- (void)qmuiTableCache_moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiTableCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildCachesAtIndexPathsIfNeeded:@[sourceIndexPath, destinationIndexPath]]; + if (obj.cachedHeights.count > 0 && obj.cachedHeights.count > sourceIndexPath.section && obj.cachedHeights.count > destinationIndexPath.section) { + NSMutableArray *sourceHeightsInSection = obj.cachedHeights[sourceIndexPath.section]; + NSMutableArray *destinationHeightsInSection = obj.cachedHeights[destinationIndexPath.section]; + NSNumber *sourceHeight = sourceHeightsInSection[sourceIndexPath.row]; + NSNumber *destinationHeight = destinationHeightsInSection[destinationIndexPath.row]; + sourceHeightsInSection[sourceIndexPath.row] = destinationHeight; + destinationHeightsInSection[destinationIndexPath.row] = sourceHeight; + } + }]; + } + [self qmuiTableCache_moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; +} + +@end + +@implementation UITableView (QMUILayoutCell) + +- (__kindof UITableViewCell *)templateCellForReuseIdentifier:(NSString *)identifier { + QMUIAssert(identifier.length > 0, @"QMUICellHeightCache", @"%s 需要一个合法的 identifier", __func__); + NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd); + if (!templateCellsByIdentifiers) { + templateCellsByIdentifiers = @{}.mutableCopy; + objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + UITableViewCell *templateCell = templateCellsByIdentifiers[identifier]; + if (!templateCell) { + // 是否有通过dataSource返回的cell + if ([self.dataSource respondsToSelector:@selector(qmui_tableView:cellWithIdentifier:)] ) { + id dataSource = (id)self.dataSource; + templateCell = [dataSource qmui_tableView:self cellWithIdentifier:identifier]; + } + // 没有的话,则需要通过register来注册一个cell,否则会crash + if (!templateCell) { + templateCell = [self dequeueReusableCellWithIdentifier:identifier]; + } + QMUIAssert(templateCell != nil, @"QMUICellHeightCache", @"通过 %s %@ 无法得到一个 cell 对象", __func__, identifier); + templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO; + templateCellsByIdentifiers[identifier] = templateCell; + } + return templateCell; +} + +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(__kindof UITableViewCell *))configuration { + CGFloat contentWidth = self.qmui_validContentWidth; + if (!identifier || contentWidth <= 0) { + return 0; + } + UITableViewCell *cell = [self templateCellForReuseIdentifier:identifier]; + [cell prepareForReuse]; + if (configuration) configuration(cell); + CGSize fitSize = CGSizeZero; + if (cell && contentWidth > 0) { + fitSize = [cell sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)]; + } + return flat(fitSize.height); +} + +// 通过indexPath缓存高度 +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UITableViewCell *))configuration { + if (!identifier || !indexPath || self.qmui_validContentWidth <= 0) { + return 0; + } + if ([self.qmui_indexPathHeightCache existsHeightAtIndexPath:indexPath]) { + return [self.qmui_indexPathHeightCache heightForIndexPath:indexPath]; + } + CGFloat height = [self qmui_heightForCellWithIdentifier:identifier configuration:configuration]; + [self.qmui_indexPathHeightCache cacheHeight:height byIndexPath:indexPath]; + return height; +} + +// 通过key缓存高度 +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id)key configuration:(void (^)(__kindof UITableViewCell *))configuration { + if (!identifier || !key || self.qmui_validContentWidth <= 0) { + return 0; + } + if ([self.qmui_keyedHeightCache existsHeightForKey:key]) { + return [self.qmui_keyedHeightCache heightForKey:key]; + } + CGFloat height = [self qmui_heightForCellWithIdentifier:identifier configuration:configuration]; + [self.qmui_keyedHeightCache cacheHeight:height byKey:key]; + return height; +} + +- (void)qmui_invalidateAllHeight { + [self.qmuiTableCache_allKeyedHeightCaches removeAllObjects]; + [self.qmuiTableCache_allIndexPathHeightCaches removeAllObjects]; +} + +@end + +#pragma mark - UICollectionView Height Cache + +/// ====================== 计算动态cell高度相关 ======================= + +@interface UICollectionView () + +/// key 为 UICollectionView 的内容大小(包裹着 CGSize),value 为该大小下对应的缓存容器,从而保证 UICollectionView 大小变化时缓存也会跟着刷新 +@property(nonatomic, strong) NSMutableDictionary *qmuiCollectionCache_allKeyedHeightCaches; +@property(nonatomic, strong) NSMutableDictionary *qmuiCollectionCache_allIndexPathHeightCaches; +@end + +@implementation UICollectionView (QMUIKeyedHeightCache) + +QMUISynthesizeIdStrongProperty(qmuiCollectionCache_allKeyedHeightCaches, setQmuiCollectionCache_allKeyedHeightCaches) + +- (QMUICellHeightCache *)qmui_keyedHeightCache { + if (!self.qmuiCollectionCache_allKeyedHeightCaches) { + self.qmuiCollectionCache_allKeyedHeightCaches = [[NSMutableDictionary alloc] init]; + } + 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]; + self.qmuiCollectionCache_allKeyedHeightCaches[[NSValue valueWithCGSize:collectionViewSize]] = cache; + } + return cache; +} + +- (void)qmui_invalidateHeightForKey:(id)aKey { + [self.qmuiCollectionCache_allKeyedHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj invalidateHeightForKey:aKey]; + }]; +} + +@end + +@implementation UICollectionView (QMUICellHeightIndexPathCache) + +QMUISynthesizeBOOLProperty(qmui_invalidateIndexPathHeightCachedAutomatically, setQmui_invalidateIndexPathHeightCachedAutomatically) +QMUISynthesizeIdStrongProperty(qmuiCollectionCache_allIndexPathHeightCaches, setQmuiCollectionCache_allIndexPathHeightCaches) + +- (QMUICellHeightIndexPathCache *)qmui_indexPathHeightCache { + if (!self.qmuiCollectionCache_allIndexPathHeightCaches) { + self.qmuiCollectionCache_allIndexPathHeightCaches = [[NSMutableDictionary alloc] init]; + } + 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]; + self.qmuiCollectionCache_allIndexPathHeightCaches[[NSValue valueWithCGSize:collectionViewSize]] = cache; + } + return cache; +} + +- (void)qmui_invalidateHeightAtIndexPath:(NSIndexPath *)indexPath { + [self.qmuiCollectionCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj invalidateHeightAtIndexPath:indexPath]; + }]; +} + +@end + +@implementation UICollectionView (QMUIIndexPathHeightCacheInvalidation) + +- (void)qmui_reloadDataWithoutInvalidateIndexPathHeightCache { + [self qmuiCollectionCache_reloadData]; +} + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + SEL selectors[] = { + @selector(initWithFrame:collectionViewLayout:), + @selector(initWithCoder:), + @selector(reloadData), + @selector(insertSections:), + @selector(deleteSections:), + @selector(reloadSections:), + @selector(moveSection:toSection:), + @selector(insertItemsAtIndexPaths:), + @selector(deleteItemsAtIndexPaths:), + @selector(reloadItemsAtIndexPaths:), + @selector(moveItemAtIndexPath:toIndexPath:) + }; + for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); index++) { + SEL originalSelector = selectors[index]; + SEL swizzledSelector = NSSelectorFromString([@"qmuiCollectionCache_" stringByAppendingString:NSStringFromSelector(originalSelector)]); + ExchangeImplementations([UICollectionView class], originalSelector, swizzledSelector); + } + }); +} + +- (instancetype)qmuiCollectionCache_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout { + [self qmuiCollectionCache_initWithFrame:frame collectionViewLayout:layout]; + [self qmuiCollectionCache_didInitialize]; + return self; +} + +- (instancetype)qmuiCollectionCache_initWithCoder:(NSCoder *)aDecoder { + [self qmuiCollectionCache_initWithCoder:aDecoder]; + [self qmuiCollectionCache_didInitialize]; + return self; +} + +- (void)qmuiCollectionCache_didInitialize { + self.qmui_invalidateIndexPathHeightCachedAutomatically = YES; +} + +- (void)qmuiCollectionCache_reloadData { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiCollectionCache_allIndexPathHeightCaches removeAllObjects]; + } + [self qmuiCollectionCache_reloadData]; +} + +- (void)qmuiCollectionCache_insertSections:(NSIndexSet *)sections { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiCollectionCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL * _Nonnull stop) { + [obj buildSectionsIfNeeded:section]; + [obj.cachedHeights insertObject:[[NSMutableArray alloc] init] atIndex:section]; + }]; + }]; + } + [self qmuiCollectionCache_insertSections:sections]; +} + +- (void)qmuiCollectionCache_deleteSections:(NSIndexSet *)sections { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiCollectionCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL * _Nonnull stop) { + [obj buildSectionsIfNeeded:section]; + [obj.cachedHeights removeObjectAtIndex:section]; + }]; + }]; + } + [self qmuiCollectionCache_deleteSections:sections]; +} + +- (void)qmuiCollectionCache_reloadSections:(NSIndexSet *)sections { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiCollectionCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL * _Nonnull stop) { + [obj buildSectionsIfNeeded:section]; + [obj.cachedHeights[section] removeAllObjects]; + }]; + }]; + } + [self qmuiCollectionCache_reloadSections:sections]; +} + +- (void)qmuiCollectionCache_moveSection:(NSInteger)section toSection:(NSInteger)newSection { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiCollectionCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildSectionsIfNeeded:section]; + [obj buildSectionsIfNeeded:newSection]; + [obj.cachedHeights exchangeObjectAtIndex:section withObjectAtIndex:newSection]; + }]; + } + [self qmuiCollectionCache_moveSection:section toSection:newSection]; +} + +- (void)qmuiCollectionCache_insertItemsAtIndexPaths:(NSArray *)indexPaths { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiCollectionCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildCachesAtIndexPathsIfNeeded:indexPaths]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + NSMutableArray *heightsInSection = obj.cachedHeights[indexPath.section]; + [heightsInSection insertObject:@(kQMUICellHeightInvalidCache) atIndex:indexPath.item]; + }]; + }]; + } + [self qmuiCollectionCache_insertItemsAtIndexPaths:indexPaths]; +} + +- (void)qmuiCollectionCache_deleteItemsAtIndexPaths:(NSArray *)indexPaths { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiCollectionCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildCachesAtIndexPathsIfNeeded:indexPaths]; + NSMutableDictionary *mutableIndexSetsToRemove = [NSMutableDictionary dictionary]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + NSMutableIndexSet *mutableIndexSet = mutableIndexSetsToRemove[@(indexPath.section)]; + if (!mutableIndexSet) { + mutableIndexSet = [NSMutableIndexSet indexSet]; + mutableIndexSetsToRemove[@(indexPath.section)] = mutableIndexSet; + } + [mutableIndexSet addIndex:indexPath.item]; + }]; + [mutableIndexSetsToRemove enumerateKeysAndObjectsUsingBlock:^(NSNumber *aKey, NSIndexSet *indexSet, BOOL *stop) { + NSMutableArray *heightsInSection = obj.cachedHeights[aKey.integerValue]; + [heightsInSection removeObjectsAtIndexes:indexSet]; + }]; + }]; + } + [self qmuiCollectionCache_deleteItemsAtIndexPaths:indexPaths]; +} + +- (void)qmuiCollectionCache_reloadItemsAtIndexPaths:(NSArray *)indexPaths { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiCollectionCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildCachesAtIndexPathsIfNeeded:indexPaths]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + NSMutableArray *heightsInSection = obj.cachedHeights[indexPath.section]; + heightsInSection[indexPath.item] = @(kQMUICellHeightInvalidCache); + }]; + }]; + } + [self qmuiCollectionCache_reloadItemsAtIndexPaths:indexPaths]; +} + +- (void)qmuiCollectionCache_moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { + if (self.qmui_invalidateIndexPathHeightCachedAutomatically) { + [self.qmuiCollectionCache_allIndexPathHeightCaches enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, QMUICellHeightIndexPathCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj buildCachesAtIndexPathsIfNeeded:@[sourceIndexPath, destinationIndexPath]]; + if (obj.cachedHeights.count > 0 && obj.cachedHeights.count > sourceIndexPath.section && obj.cachedHeights.count > destinationIndexPath.section) { + NSMutableArray *sourceHeightsInSection = obj.cachedHeights[sourceIndexPath.section]; + NSMutableArray *destinationHeightsInSection = obj.cachedHeights[destinationIndexPath.section]; + NSNumber *sourceHeight = sourceHeightsInSection[sourceIndexPath.item]; + NSNumber *destinationHeight = destinationHeightsInSection[destinationIndexPath.item]; + sourceHeightsInSection[sourceIndexPath.item] = destinationHeight; + destinationHeightsInSection[destinationIndexPath.item] = sourceHeight; + } + }]; + } + [self qmuiCollectionCache_moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; +} + +@end + +@implementation UICollectionView (QMUILayoutCell) + +- (__kindof UICollectionViewCell *)templateCellForReuseIdentifier:(NSString *)identifier cellClass:(Class)cellClass { + 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; + objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + UICollectionViewCell *templateCell = templateCellsByIdentifiers[identifier]; + if (!templateCell) { + // 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]; + QMUIAssert(templateCell != nil, @"QMUICellHeightCache", @"通过 %s %@ 无法得到一个 cell 对象", __func__, identifier); + } + templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO; + templateCellsByIdentifiers[identifier] = templateCell; + return templateCell; +} + +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration { + if (!identifier || CGRectGetWidth(self.bounds) <= 0) { + return 0; + } + UICollectionViewCell *cell = [self templateCellForReuseIdentifier:identifier cellClass:cellClass]; + [cell prepareForReuse]; + if (configuration) configuration(cell); + CGSize fitSize = CGSizeZero; + if (cell && itemWidth > 0) { + fitSize = [cell sizeThatFits:CGSizeMake(itemWidth, CGFLOAT_MAX)]; + } + return ceil(fitSize.height); +} + +// 通过indexPath缓存高度 +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration { + if (!identifier || !indexPath || CGRectGetWidth(self.bounds) <= 0) { + return 0; + } + if ([self.qmui_indexPathHeightCache existsHeightAtIndexPath:indexPath]) { + return [self.qmui_indexPathHeightCache heightForIndexPath:indexPath]; + } + CGFloat height = [self qmui_heightForCellWithIdentifier:identifier cellClass:cellClass itemWidth:itemWidth configuration:configuration]; + [self.qmui_indexPathHeightCache cacheHeight:height byIndexPath:indexPath]; + return height; +} + +// 通过key缓存高度 +- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByKey:(id)key configuration:(void (^)(__kindof UICollectionViewCell *cell))configuration { + if (!identifier || !key || CGRectGetWidth(self.bounds) <= 0) { + return 0; + } + if ([self.qmui_keyedHeightCache existsHeightForKey:key]) { + return [self.qmui_keyedHeightCache heightForKey:key]; + } + CGFloat height = [self qmui_heightForCellWithIdentifier:identifier cellClass:cellClass itemWidth:itemWidth configuration:configuration]; + [self.qmui_keyedHeightCache cacheHeight:height byKey:key]; + return height; +} + +- (void)qmui_invalidateAllHeight { + [self.qmuiCollectionCache_allKeyedHeightCaches removeAllObjects]; + [self.qmuiCollectionCache_allIndexPathHeightCaches removeAllObjects]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h new file mode 100644 index 00000000..9c4be7c9 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h @@ -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. + */ + +// +// QMUICellHeightKeyCache.h +// QMUIKit +// +// Created by QMUI Team on 2018/3/14. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * 通过业务定义的一个 key 来缓存 cell 的高度,需搭配 UITableView 使用,一般不用你自己去 init。 + * 具体使用方式请看 UITableView (QMUICellHeightKeyCache) 的注释。 + */ +@interface QMUICellHeightKeyCache : NSObject + +/// 检查是否存在某个 key 的高度 +- (BOOL)existsHeightForKey:(id)key; + +/// 将某个高度缓存到指定的 key +- (void)cacheHeight:(CGFloat)height forKey:(id)key; + +/// 获取指定 key 对应的高度,如果该 key 不存在,则返回 0 +- (CGFloat)heightForKey:(id)key; + +/// 令指定 key 的缓存失效。注意如果在业务里,应该调用 [UITableView -qmui_invalidateCellHeightCachedForKey:],而不应该直接调用这个方法。 +- (void)invalidateHeightForKey:(id)key; + +/// 令所有的缓存失效。注意如果在业务里,应该调用 [UITableView -qmui_invalidateAllCellHeightKeyCache],而不应该直接调用这个方法。 +- (void)invalidateAllHeightCache; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m new file mode 100644 index 00000000..fa2e0838 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m @@ -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. + */ + +// +// QMUICellHeightKeyCache.m +// QMUIKit +// +// Created by QMUI Team on 2018/3/14. +// + +#import "QMUICellHeightKeyCache.h" +#import "NSNumber+QMUI.h" + +@interface QMUICellHeightKeyCache () + +@property(nonatomic, strong) NSMutableDictionary, NSNumber *> *cachedHeights; +@end + +@implementation QMUICellHeightKeyCache + +- (instancetype)init { + if (self = [super init]) { + self.cachedHeights = [NSMutableDictionary dictionary]; + } + return self; +} + +- (BOOL)existsHeightForKey:(id)key { + NSNumber *number = self.cachedHeights[key]; + return !!number;// 注意这里“拿 number 是否存在”作为条件,也即意味着高度为0也是合法的,因为 @(0) 也是一个不为 nil 的 NSNumber +} + +- (void)cacheHeight:(CGFloat)height forKey:(id)key { + self.cachedHeights[key] = @(height); +} + +- (CGFloat)heightForKey:(id)key { + return self.cachedHeights[key].qmui_CGFloatValue; +} + +- (void)invalidateHeightForKey:(id)key { + [self.cachedHeights removeObjectForKey:key]; +} + +- (void)invalidateAllHeightCache { + [self.cachedHeights removeAllObjects]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@, cachedHeights = %@", [super description], _cachedHeights]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h new file mode 100644 index 00000000..c7ff7dcd --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h @@ -0,0 +1,49 @@ +/** + * 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. + */ + +// +// UITableView+QMUICellHeightKeyCache.h +// QMUIKit +// +// Created by QMUI Team on 2018/3/14. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUICellHeightKeyCache; + +/** + * 自动缓存 self-sizing cell 的高度,避免重复计算。使用方法: + * 1. 将 tableView.qmui_cacheCellHeightByKeyAutomatically = YES + * 2. 实现 tableView 的 delegate 方法 qmui_tableView:cacheKeyForRowAtIndexPath: 返回一个 key。建议 key 由所有可能影响高度的字段拼起来,这样当数据发生变化时不需要手动更新缓存。 + * + * @note 注意这里的高度缓存仅适合于使用 self-sizing 机制的 tableView(也即 tableView.rowHeight = UITableViewAutomaticDimension),QMUICellHeightKeyCache 会自动在 willDisplayCell 里将 cell 的当前高度缓存起来,然后在 heightForRow 里从缓存中读取高度后使用。 + * @note 如果 tableView 开启了 qmui_cacheCellHeightByKeyAutomatically 并且 tableView.delegate 实现了 tableView:heightForRowAtIndexPath:,如果返回值 >= 0则使用这个返回值当成最终的高度,如果 < 0 则交给 QMUICellHeightKeyCache 自己处理。 + * @note 如果 tableView 开启了 qmui_cacheCellHeightByKeyAutomatically 并且 tableView.delegate 实现了 tableView:estimatedHeightForRowAtIndexPath:,则当该 indexPath 所在的 cell 的高度已经被计算过的情况下,业务自己的 tableView:estimatedHeightForRowAtIndexPath: 不会被调用,只有当高度缓存里找不到该 indexPath 对应的 key 的缓存时,才会调用业务的这个方法。 + * + * @note 在 UITableView 的宽度和 contentInset、safeAreaInsets 发生变化时(例如横竖屏旋转、iPad 分屏),高度缓存会自动刷新,所以无需为这种情况做保护。 + */ +@interface UITableView (QMUICellHeightKeyCache) + +/// 控制是否要自动缓存 cell 的高度,默认为 NO +@property(nonatomic, assign) BOOL qmui_cacheCellHeightByKeyAutomatically; + +/// 获取当前的缓存容器。tableView 的宽度和 contentInset 发生变化时,这个数组也会跟着变,但当 tableView 宽度小于 0 时会返回 nil。 +@property(nonatomic, weak, readonly, nullable) QMUICellHeightKeyCache *qmui_currentCellHeightKeyCache; + +/// 搭配 QMUICellHeightKeyCache,清除某个指定 key 的缓存,注意不要直接调用 self.qmui_currentCellHeightKeyCache.invalidateHeightForKey,因为一个 UITableView 里会包含多个 QMUICellHeightKeyCache,那样写只能刷新当前的 QMUICellHeightKeyCache,其他宽度下的 QMUICellHeightKeyCache 无法刷新。 +- (void)qmui_invalidateCellHeightCachedForKey:(id)key; + +/// 搭配 QMUICellHeightKeyCache,清除所有状态下的缓存 +- (void)qmui_invalidateAllCellHeightKeyCache; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m new file mode 100644 index 00000000..a6198636 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m @@ -0,0 +1,244 @@ +/** + * 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. + */ + +// +// UITableView+QMUICellHeightKeyCache.m +// QMUIKit +// +// Created by QMUI Team on 2018/3/14. +// + +#import "UITableView+QMUICellHeightKeyCache.h" +#import "QMUICore.h" +#import "QMUICellHeightKeyCache.h" +#import "UIView+QMUI.h" +#import "UIScrollView+QMUI.h" +#import "UITableView+QMUI.h" +#import "QMUITableViewProtocols.h" +#import "QMUIMultipleDelegates.h" + +@interface UITableView () + +@property(nonatomic, strong) NSMutableDictionary *qmui_allKeyCaches; +@end + +@implementation UITableView (QMUICellHeightKeyCache) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([UITableView class], @selector(setDelegate:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableView *selfObject, id firstArgv) { + + [selfObject replaceMethodForDelegateIfNeeded:firstArgv]; + + // call super + void (*originSelectorIMP)(id, SEL, id); + originSelectorIMP = (void (*)(id, SEL, id))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + }); +} + +static char kAssociatedObjectKey_qmuiCacheCellHeightByKeyAutomatically; +- (void)setQmui_cacheCellHeightByKeyAutomatically:(BOOL)qmui_cacheCellHeightByKeyAutomatically { + objc_setAssociatedObject(self, &kAssociatedObjectKey_qmuiCacheCellHeightByKeyAutomatically, @(qmui_cacheCellHeightByKeyAutomatically), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + if (qmui_cacheCellHeightByKeyAutomatically) { + + 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]; + + // 在上面那一句 replaceMethodForDelegateIfNeeded 里可能修改了 delegate 里的一些方法,所以需要通过重新设置 delegate 来触发 tableView 读取新的方法。 + self.delegate = self.delegate; + } +} + +- (BOOL)qmui_cacheCellHeightByKeyAutomatically { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_qmuiCacheCellHeightByKeyAutomatically)) boolValue]; +} + +static char kAssociatedObjectKey_qmuiAllKeyCaches; +- (void)setQmui_allKeyCaches:(NSMutableDictionary *)qmui_allKeyCaches { + objc_setAssociatedObject(self, &kAssociatedObjectKey_qmuiAllKeyCaches, qmui_allKeyCaches, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSMutableDictionary *)qmui_allKeyCaches { + if (!objc_getAssociatedObject(self, &kAssociatedObjectKey_qmuiAllKeyCaches)) { + self.qmui_allKeyCaches = [NSMutableDictionary dictionary]; + } + return (NSMutableDictionary *)objc_getAssociatedObject(self, &kAssociatedObjectKey_qmuiAllKeyCaches); +} + +- (QMUICellHeightKeyCache *)qmui_currentCellHeightKeyCache { + CGFloat width = self.qmui_validContentWidth; + if (width <= 0) { + return nil; + } + QMUICellHeightKeyCache *cache = self.qmui_allKeyCaches[@(width)]; + if (!cache) { + cache = [[QMUICellHeightKeyCache alloc] init]; + self.qmui_allKeyCaches[@(width)] = cache; + } + return cache; +} + +- (void)qmui_tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { + if (tableView.qmui_cacheCellHeightByKeyAutomatically) { + id cachedKey = [((id)tableView.delegate) qmui_tableView:tableView cacheKeyForRowAtIndexPath:indexPath]; + [tableView.qmui_currentCellHeightKeyCache cacheHeight:CGRectGetHeight(cell.frame) forKey:cachedKey]; + } +} + +- (CGFloat)qmui_tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + if (tableView.qmui_cacheCellHeightByKeyAutomatically) { + id cachedKey = [((id)tableView.delegate) qmui_tableView:tableView cacheKeyForRowAtIndexPath:indexPath]; + if ([tableView.qmui_currentCellHeightKeyCache existsHeightForKey:cachedKey]) { + return [tableView.qmui_currentCellHeightKeyCache heightForKey:cachedKey]; + } + // 由于 QMUICellHeightKeyCache 只对 self-sizing 的 cell 生效,所以这里返回这个值,以使用 self-sizing 效果 + return UITableViewAutomaticDimension; + } else { + // 对于开启过 qmui_cacheCellHeightByKeyAutomatically 然后又关闭的 class 就会走到这里,做个保护而已。理论上走到这个分支本身就是没有意义的。 + return tableView.rowHeight; + } +} + +- (CGFloat)qmui_tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { + if (tableView.qmui_cacheCellHeightByKeyAutomatically) { + id cachedKey = [((id)tableView.delegate) qmui_tableView:tableView cacheKeyForRowAtIndexPath:indexPath]; + if ([tableView.qmui_currentCellHeightKeyCache existsHeightForKey:cachedKey]) { + return [tableView.qmui_currentCellHeightKeyCache heightForKey:cachedKey]; + } + } + return UITableViewAutomaticDimension;// 表示 QMUICellHeightKeyCache 无法决定一个合适的高度,交给业务,或者交给系统默认值决定。 +} + +- (void)replaceMethodForDelegateIfNeeded:(id)delegate { + if (self.qmui_cacheCellHeightByKeyAutomatically && delegate) { + + void (^addSelectorBlock)(id) = ^void(id aDelegate) { + [QMUIHelper executeBlock:^{ + [self handleWillDisplayCellMethodForDelegate:aDelegate]; + [self handleHeightForRowMethodForDelegate:aDelegate]; + [self handleEstimatedHeightForRowMethodForDelegate:aDelegate]; + } oncePerIdentifier:[NSString stringWithFormat:@"QMUICellHeightKeyCache %@", NSStringFromClass(aDelegate.class)]]; + }; + + if ([delegate isKindOfClass:[QMUIMultipleDelegates class]]) { + NSPointerArray *delegates = [((QMUIMultipleDelegates *)delegate).delegates copy]; + for (id d in delegates) { + if ([d conformsToProtocol:@protocol(QMUITableViewDelegate)]) { + addSelectorBlock((id)d); + } + } + } else { + addSelectorBlock((id)delegate); + } + } +} + +- (void)handleWillDisplayCellMethodForDelegate:(id)delegate { + // 如果 delegate 本身没有实现 tableView:willDisplayCell:forRowAtIndexPath:,则为它添加一个。 + // 如果 delegate 已经有实现,则在调用完 delegate 自身的实现后,再调用我们自己的实现去存储计算后的 cell 高度 + SEL willDisplayCellSelector = @selector(tableView:willDisplayCell:forRowAtIndexPath:); + Method willDisplayCellMethod = class_getInstanceMethod([self class], @selector(qmui_tableView:willDisplayCell:forRowAtIndexPath:)); + IMP willDisplayCellIMP = method_getImplementation(willDisplayCellMethod); + void (*willDisplayCellFunction)(id, SEL, UITableView *, UITableViewCell *, NSIndexPath *); + willDisplayCellFunction = (void (*)(id, SEL, UITableView *, UITableViewCell *, NSIndexPath *))willDisplayCellIMP; + + BOOL addedSuccessfully = class_addMethod(delegate.class, willDisplayCellSelector, willDisplayCellIMP, method_getTypeEncoding(willDisplayCellMethod)); + if (!addedSuccessfully) { + OverrideImplementation([delegate class], willDisplayCellSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(id delegateSelf, UITableView *tableView, UITableViewCell *cell, NSIndexPath *indexPath) { + + // call super + void (*originSelectorIMP)(id, SEL, UITableView *, UITableViewCell *, NSIndexPath *); + originSelectorIMP = (void (*)(id, SEL, UITableView *, UITableViewCell *, NSIndexPath *))originalIMPProvider(); + originSelectorIMP(delegateSelf, originCMD, tableView, cell, indexPath); + + // call QMUI + willDisplayCellFunction(delegateSelf, willDisplayCellSelector, tableView, cell, indexPath); + }; + }); + } +} + +- (void)handleHeightForRowMethodForDelegate:(id)delegate { + // 如果 delegate 本身没有实现 tableView:heightForRowAtIndexPath:,则为它添加一个。 + // 如果 delegate 已经有实现,则优先拿它的实现的值来 return,如果它的值小于0(例如-1),则认为它想用 QMUICellHeightKeyCache 的计算,此时再 return 我们自己的计算结果 + SEL heightForRowSelector = @selector(tableView:heightForRowAtIndexPath:); + Method heightForRowMethod = class_getInstanceMethod([self class], @selector(qmui_tableView:heightForRowAtIndexPath:)); + IMP heightForRowIMP = method_getImplementation(heightForRowMethod); + CGFloat (*heightForRowFunction)(id, SEL, UITableView *, NSIndexPath *); + heightForRowFunction = (CGFloat (*)(id, SEL, UITableView *, NSIndexPath *))heightForRowIMP; + + BOOL addedSuccessfully = class_addMethod([delegate class], heightForRowSelector, heightForRowIMP, method_getTypeEncoding(heightForRowMethod)); + if (!addedSuccessfully) { + OverrideImplementation([delegate class], heightForRowSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGFloat(id delegateSelf, UITableView *tableView, NSIndexPath *indexPath) { + + // call super + CGFloat (*originSelectorIMP)(id, SEL, UITableView *, NSIndexPath *); + originSelectorIMP = (CGFloat (*)(id, SEL, UITableView *, NSIndexPath *))originalIMPProvider(); + CGFloat result = originSelectorIMP(delegateSelf, originCMD, tableView, indexPath); + + if (result >= 0) { + return result; + } + + // call QMUI + return heightForRowFunction(delegateSelf, heightForRowSelector, tableView, indexPath); + }; + }); + } +} + +- (void)handleEstimatedHeightForRowMethodForDelegate:(id)delegate { + // 如果 delegate 本身没有实现 tableView:estimatedHeightForRowAtIndexPath:,则为它添加一个。 + // 如果 delegate 已经有实现,会优先拿 QMUICellHeightKeyCache 的结果,如果 QMUICellHeightKeyCache 在 cache 里找不到值,才会返回业务在 tableView:estimatedHeightForRowAtIndexPath: 里的返回值 + SEL heightForRowSelector = @selector(tableView:estimatedHeightForRowAtIndexPath:); + Method heightForRowMethod = class_getInstanceMethod([self class], @selector(qmui_tableView:estimatedHeightForRowAtIndexPath:)); + IMP heightForRowIMP = method_getImplementation(heightForRowMethod); + CGFloat (*heightForRowFunction)(id, SEL, UITableView *, NSIndexPath *); + heightForRowFunction = (CGFloat (*)(id, SEL, UITableView *, NSIndexPath *))heightForRowIMP; + + BOOL addedSuccessfully = class_addMethod([delegate class], heightForRowSelector, heightForRowIMP, method_getTypeEncoding(heightForRowMethod)); + if (!addedSuccessfully) { + OverrideImplementation([delegate class], heightForRowSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGFloat(id delegateSelf, UITableView *tableView, NSIndexPath *indexPath) { + + CGFloat result = heightForRowFunction(delegateSelf, heightForRowSelector, tableView, indexPath); + if (result != UITableViewAutomaticDimension) { + return result; + } + + // call super + CGFloat (*originSelectorIMP)(id, SEL, UITableView *, NSIndexPath *); + originSelectorIMP = (CGFloat (*)(id, SEL, UITableView *, NSIndexPath *))originalIMPProvider(); + result = originSelectorIMP(delegateSelf, originCMD, tableView, indexPath); + return result; + }; + }); + } +} + +- (void)qmui_invalidateCellHeightCachedForKey:(id)key { + [self.qmui_allKeyCaches enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull widthKey, QMUICellHeightKeyCache * _Nonnull obj, BOOL * _Nonnull stop) { + [obj invalidateHeightForKey:key]; + }]; +} + +- (void)qmui_invalidateAllCellHeightKeyCache { + [self.qmui_allKeyCaches removeAllObjects]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h new file mode 100644 index 00000000..c97a7c87 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.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. + */ + +// +// QMUICellSizeKeyCache.h +// QMUIKit +// +// Created by QMUI Team on 2018/3/14. +// + +#import +#import + +/** + * 通过业务定义的一个 key 来缓存 cell 的 size,需搭配 UICollectionView 使用,一般不用你自己去 init。 + * 具体使用方式请看 UICollectionView (QMUICellSizeKeyCache) 的注释。 + */ +@interface QMUICellSizeKeyCache : NSObject + +/// 检查是否存在某个 key 的 size +- (BOOL)existsSizeForKey:(id)key; + +/// 将某个 size 缓存到指定的 key +- (void)cacheSize:(CGSize)size forKey:(id)key; + +/// 获取指定 key 对应的 size,如果该 key 不存在,则返回 0 +- (CGSize)sizeForKey:(id)key; + +// 使 cache 失效,多用在 data 更新之后或 UICollectionView 的 size 发生变化的时候,但在 QMUI 里,UICollectionView 的 size 发生变化会自动更新,所以不用处理 size 变化的场景。 +- (void)invalidateSizeForKey:(id)key; +- (void)invalidateAllSizeCache; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m new file mode 100644 index 00000000..8f083b4a --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m @@ -0,0 +1,57 @@ +/** + * 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. + */ + +// +// QMUICellSizeKeyCache.m +// QMUIKit +// +// Created by QMUI Team on 2018/3/14. +// + +#import "QMUICellSizeKeyCache.h" + +@interface QMUICellSizeKeyCache () + +@property(nonatomic, strong) NSMutableDictionary, NSValue *> *cachedSizes; +@end + +@implementation QMUICellSizeKeyCache + +- (instancetype)init { + if (self = [super init]) { + self.cachedSizes = [NSMutableDictionary dictionary]; + } + return self; +} + +- (BOOL)existsSizeForKey:(id)key { + NSValue *sizeValue = self.cachedSizes[key]; + return sizeValue && !CGSizeEqualToSize(sizeValue.CGSizeValue, CGSizeMake(-1, -1)); +} + +- (void)cacheSize:(CGSize)size forKey:(id)key { + self.cachedSizes[key] = @(size); +} + +- (CGSize)sizeForKey:(id)key { + return self.cachedSizes[key].CGSizeValue; +} + +- (void)invalidateSizeForKey:(id)key { + [self.cachedSizes removeObjectForKey:key]; +} + +- (void)invalidateAllSizeCache { + [self.cachedSizes removeAllObjects]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@, cachedSizes = %@", [super description], _cachedSizes]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h new file mode 100644 index 00000000..8ba4b741 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h @@ -0,0 +1,36 @@ +/** + * 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. + */ + +// +// UICollectionView+QMUICellSizeKeyCache.h +// QMUIKit +// +// Created by QMUI Team on 2018/3/14. +// + +#import +#import + +@class QMUICellSizeKeyCache; + +@protocol QMUICellSizeKeyCache_UICollectionViewDelegate + +@optional +- (nonnull id)qmui_collectionView:(nonnull UICollectionView *)collectionView cacheKeyForItemAtIndexPath:(nonnull NSIndexPath *)indexPath; +@end + +/// 注意,这个类的功能暂无法使用 +@interface UICollectionView (QMUICellSizeKeyCache) + +/// 控制是否要自动缓存 cell 的高度,默认为 NO +@property(nonatomic, assign) BOOL qmui_cacheCellSizeByKeyAutomatically; + +/// 获取当前的缓存容器。tableView 的宽度和 contentInset 发生变化时,这个数组也会跟着变,但当 tableView 宽度小于 0 时会返回 nil。 +@property(nonatomic, weak, readonly, nullable) QMUICellSizeKeyCache *qmui_currentCellSizeKeyCache; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m new file mode 100644 index 00000000..54b71dae --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m @@ -0,0 +1,201 @@ +/** + * 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. + */ + +// +// UICollectionView+QMUICellSizeKeyCache.m +// QMUIKit +// +// Created by QMUI Team on 2018/3/14. +// + +#import "UICollectionView+QMUICellSizeKeyCache.h" +#import "QMUICore.h" +#import "QMUICellSizeKeyCache.h" +#import "UIScrollView+QMUI.h" +#import "QMUIMultipleDelegates.h" + +//@interface UICollectionViewCell (QMUICellSizeKeyCache) +// +//@property(nonatomic, weak) UICollectionView *qmui_collectionView; +//@end +// +//@implementation UICollectionViewCell (QMUICellSizeKeyCache) +// +//+ (void)load { +// static dispatch_once_t onceToken; +// dispatch_once(&onceToken, ^{ +// ExchangeImplementations(self.class, @selector(preferredLayoutAttributesFittingAttributes:), @selector(qmui_preferredLayoutAttributesFittingAttributes:)); +// ExchangeImplementations(self.class, @selector(didMoveToSuperview), @selector(qmui_didMoveToSuperview)); +// }); +//} +// +//static char kAssociatedObjectKey_collectionView; +//- (void)setQmui_collectionView:(UICollectionView *)qmui_collectionView { +// objc_setAssociatedObject(self, &kAssociatedObjectKey_collectionView, qmui_collectionView, OBJC_ASSOCIATION_ASSIGN); +//} +// +//- (UICollectionView *)qmui_collectionView { +// return (UICollectionView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_collectionView); +//} +// +//- (void)qmui_didMoveToSuperview { +// [self qmui_didMoveToSuperview]; +// if ([self.superview isKindOfClass:[UICollectionView class]]) { +// __weak UICollectionView *weakCollectionView = (UICollectionView *)self.superview; +// self.qmui_collectionView = weakCollectionView; +// } else { +// self.qmui_collectionView = nil; +// } +//} +// +//- (UICollectionViewLayoutAttributes *)qmui_preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes { +// if (self.qmui_collectionView.qmui_cacheCellSizeByKeyAutomatically) { +// id key = [((id)self.qmui_collectionView.delegate) qmui_collectionView:self.qmui_collectionView cacheKeyForItemAtIndexPath:layoutAttributes.indexPath]; +// if ([self.qmui_collectionView.qmui_currentCellSizeKeyCache existsSizeForKey:key]) { +// CGSize cachedSize = [self.qmui_collectionView.qmui_currentCellSizeKeyCache sizeForKey:key]; +// layoutAttributes.size = cachedSize; +// return layoutAttributes; +// } +// } +// return [self qmui_preferredLayoutAttributesFittingAttributes:layoutAttributes]; +//} +// +//@end + +@interface UICollectionView () + +@property(nonatomic, strong) NSMutableDictionary *qmui_allKeyCaches; +@end + +@implementation UICollectionView (QMUICellSizeKeyCache) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([UICollectionView class], @selector(setDelegate:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UICollectionView *selfObject, id firstArgv) { + + [selfObject replaceMethodForDelegateIfNeeded:firstArgv]; + + // call super + void (*originSelectorIMP)(id, SEL, id); + originSelectorIMP = (void (*)(id, SEL, id))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv); + }; + }); + }); +} + +static char kAssociatedObjectKey_qmuiCacheCellSizeByKeyAutomatically; +- (void)setQmui_cacheCellSizeByKeyAutomatically:(BOOL)qmui_cacheCellSizeByKeyAutomatically { + objc_setAssociatedObject(self, &kAssociatedObjectKey_qmuiCacheCellSizeByKeyAutomatically, @(qmui_cacheCellSizeByKeyAutomatically), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + if (qmui_cacheCellSizeByKeyAutomatically) { + QMUIAssert([self.collectionViewLayout isKindOfClass:[UICollectionViewFlowLayout class]], @"QMUICellSizeKeyCache", @"只支持 %@", NSStringFromClass(UICollectionViewFlowLayout.class)); + + [self replaceMethodForDelegateIfNeeded:self.delegate]; + + // 在上面那一句 replaceMethodForDelegateIfNeeded 里可能修改了 delegate 里的一些方法,所以需要通过重新设置 delegate 来触发 tableView 读取新的方法。与 UITableView 不同,UICollectionView 不管哪个 iOS 版本都要先置为 nil 再重新设置才能让 delegate 方法替换立即生效 + id tempDelegate = self.delegate; + self.delegate = nil; + self.delegate = tempDelegate; + } +} + +- (BOOL)qmui_cacheCellSizeByKeyAutomatically { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_qmuiCacheCellSizeByKeyAutomatically)) boolValue]; +} + +static char kAssociatedObjectKey_qmuiAllKeyCaches; +- (void)setQmui_allKeyCaches:(NSMutableDictionary *)qmui_allKeyCaches { + objc_setAssociatedObject(self, &kAssociatedObjectKey_qmuiAllKeyCaches, qmui_allKeyCaches, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSMutableDictionary *)qmui_allKeyCaches { + if (!objc_getAssociatedObject(self, &kAssociatedObjectKey_qmuiAllKeyCaches)) { + self.qmui_allKeyCaches = [NSMutableDictionary dictionary]; + } + return (NSMutableDictionary *)objc_getAssociatedObject(self, &kAssociatedObjectKey_qmuiAllKeyCaches); +} + +- (QMUICellSizeKeyCache *)qmui_currentCellSizeKeyCache { + CGFloat width = [self widthForCacheKey]; + if (width <= 0) { + return nil; + } + QMUICellSizeKeyCache *cache = self.qmui_allKeyCaches[@(width)]; + if (!cache) { + cache = [[QMUICellSizeKeyCache alloc] init]; + self.qmui_allKeyCaches[@(width)] = cache; + } + return cache; +} + +// 当 collectionView 水平滚动时,则认为垂直方向的内容区域会影响 cell 的 size 计算。而当 collectionView 垂直滚动时,则认为水平方向的内容区域会影响 cell 的 size 计算。 +- (CGFloat)widthForCacheKey { + UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionViewLayout; + if (layout.scrollDirection == UICollectionViewScrollDirectionHorizontal) { + CGFloat height = CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.adjustedContentInset) - UIEdgeInsetsGetVerticalValue(layout.sectionInset); + return height; + } + CGFloat width = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.adjustedContentInset) - UIEdgeInsetsGetHorizontalValue(((UICollectionViewFlowLayout *)self.collectionViewLayout).sectionInset); + return width; +} + +- (void)qmui_collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { + [collectionView qmui_collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; + if (collectionView.qmui_cacheCellSizeByKeyAutomatically) { + if (![collectionView.delegate respondsToSelector:@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]; + } +} + +//- (CGSize)qmui_collectionView:(UICollectionView *)collectionView layout:(UICollectionViewFlowLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { +// if (collectionView.qmui_cacheCellSizeByKeyAutomatically) { +// if (![collectionView.delegate respondsToSelector:@selector(qmui_collectionView:cacheKeyForItemAtIndexPath:)]) { +// NSAssert(NO, @"%@ 需要实现 %@ 方法才能自动缓存 cell 高度", collectionView.delegate, NSStringFromSelector(@selector(qmui_collectionView:cacheKeyForItemAtIndexPath:))); +// } +// id cachedKey = [((id)self) qmui_collectionView:collectionView cacheKeyForItemAtIndexPath:indexPath]; +// if ([collectionView.qmui_currentCellSizeKeyCache existsSizeForKey:cachedKey]) { +// return [collectionView.qmui_currentCellSizeKeyCache sizeForKey:cachedKey]; +// } +// } else { +// // 对于开启过 qmui_cacheCellSizeByKeyAutomatically 然后又关闭的 class 就会走到这里,此时已经无法调用回之前被替换的方法的实现,所以直接使用 collecionView.itemSize +// // TODO: molice 最好应该在 replaceMethodForDelegateIfNeeded: 里判断在替换方法之前 delegate 是否已经有实现 sizeForItem,如果有,则在这里调用回它自己的实现,如果没有,再使用 collecionView.itemSize,不然现在的做法会导致 delegate 里关闭了自动缓存的情况下就算实现了 sizeForItem,也无法被调用。 +// return collectionViewLayout.estimatedItemSize; +// } +// +// // 由于 QMUICellSizeKeyCache 只对 self-sizing 的 cell 生效,所以这里返回这个值,以使用 self-sizing 效果 +// return collectionViewLayout.estimatedItemSize; +//} + +- (void)replaceMethodForDelegateIfNeeded:(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 new file mode 100644 index 00000000..b532188d --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.h @@ -0,0 +1,92 @@ +/** + * 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. + */ + +// +// QMUICollectionViewPagingLayout.h +// qmui +// +// Created by QMUI Team on 15/9/24. +// + +#import + +typedef NS_ENUM(NSInteger, QMUICollectionViewPagingLayoutStyle) { + QMUICollectionViewPagingLayoutStyleDefault, // 普通模式,水平滑动 + QMUICollectionViewPagingLayoutStyleScale, // 缩放模式,两边的item会小一点,逐渐向中间放大 + QMUICollectionViewPagingLayoutStyleRotation // 旋转模式,围绕底部某个点为中心旋转 +}; + +/** + * 支持按页横向滚动的 UICollectionViewLayout,可切换不同类型的滚动动画。 + * + * @warning item 的大小和布局仅支持通过 UICollectionViewFlowLayout 的 property 系列属性修改,也即每个 item 都应相等。对于通过 delegate 方式返回各不相同的 itemSize、sectionInset 的场景是不支持的。 + */ +@interface QMUICollectionViewPagingLayout : UICollectionViewFlowLayout + +- (instancetype)initWithStyle:(QMUICollectionViewPagingLayoutStyle)style NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, assign, readonly) QMUICollectionViewPagingLayoutStyle style; + +/** + * 规定超过这个滚动速度就强制翻页,从而使翻页更容易触发。默认为 0.4 + */ +@property(nonatomic, assign) CGFloat velocityForEnsurePageDown; + +/** + * 是否支持一次滑动可以滚动多个 item,默认为 YES + */ +@property(nonatomic, assign) BOOL allowsMultipleItemScroll; + +/** + * 规定了当支持一次滑动允许滚动多个 item 的时候,滑动速度要达到多少才会滚动多个 item,默认为 2.5 + * + * 仅当 allowsMultipleItemScroll 为 YES 时生效 + */ +@property(nonatomic, assign) CGFloat multipleItemScrollVelocityLimit; + +@end + +@interface QMUICollectionViewPagingLayout (DefaultStyle) + +/// 当前 cell 的百分之多少滚过临界点时就会触发滚到下一张的动作,默认为 .666,也即超过 2/3 即会滚到下一张。 +/// 对应地,触发滚到上一张的临界点将会被设置为 (1 - pagingThreshold) +@property(nonatomic, assign) CGFloat pagingThreshold; + +/// 打开时,会在 collectionView.backgroundView 上添加一条红线,用来标志分页的参考点位置。仅对 Default style 有效。 +@property(nonatomic, assign) BOOL debug; + +@end + + +@interface QMUICollectionViewPagingLayout (ScaleStyle) + +/** + * 中间那张卡片基于初始大小的缩放倍数,默认为 1.0 + */ +@property(nonatomic, assign) CGFloat maximumScale; + +/** + * 除了中间之外的其他卡片基于初始大小的缩放倍数,默认为 0.9 + */ +@property(nonatomic, assign) CGFloat minimumScale; +@end + + +extern const CGFloat QMUICollectionViewPagingLayoutRotationRadiusAutomatic; + +@interface QMUICollectionViewPagingLayout (RotationStyle) + +/** + * 旋转卡片相关 + * 左右两个卡片最终旋转的角度有 rotationRadius * 90 计算出来 + * rotationRadius表示旋转的半径 + * @warning 仅当 style 为 QMUICollectionViewPagingLayoutStyleRotation 时才生效 + */ +@property(nonatomic, assign) CGFloat rotationRatio; +@property(nonatomic, assign) CGFloat rotationRadius; +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m b/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m new file mode 100644 index 00000000..24f96227 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m @@ -0,0 +1,310 @@ +/** + * 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. + */ + +// +// QMUICollectionViewPagingLayout.m +// qmui +// +// Created by QMUI Team on 15/9/24. +// + +#import "QMUICollectionViewPagingLayout.h" +#import "QMUICore.h" +#import "UIScrollView+QMUI.h" +#import "CALayer+QMUI.h" + +@interface QMUICollectionViewPagingLayout () { + CGFloat _maximumScale; + CGFloat _minimumScale; + CGFloat _rotationRatio; + CGFloat _rotationRadius; + CGSize _finalItemSize; + CGFloat _pagingThreshold; + BOOL _debug; +} + +@property(nonatomic, strong) CALayer *debugLayer; + +@end + +@implementation QMUICollectionViewPagingLayout (DefaultStyle) + +- (CGFloat)pagingThreshold { + return _pagingThreshold; +} + +- (void)setPagingThreshold:(CGFloat)pagingThreshold { + _pagingThreshold = pagingThreshold; +} + +- (BOOL)debug { + return _debug; +} + +- (void)setDebug:(BOOL)debug { + _debug = debug; + if (self.style == QMUICollectionViewPagingLayoutStyleDefault && debug && !self.debugLayer) { + self.debugLayer = [CALayer layer]; + [self.debugLayer qmui_removeDefaultAnimations]; + self.debugLayer.backgroundColor = UIColorTestRed.CGColor; + UIView *backgroundView = self.collectionView.backgroundView; + if (!backgroundView) { + backgroundView = [[UIView alloc] init]; + backgroundView.tag = 1024; + self.collectionView.backgroundView = backgroundView; + } + [backgroundView.layer addSublayer:self.debugLayer]; + } else if (!debug) { + [self.debugLayer removeFromSuperlayer]; + self.debugLayer = nil; + if (self.collectionView.backgroundView.tag == 1024) { + self.collectionView.backgroundView = nil; + } + } +} + +@end + +@implementation QMUICollectionViewPagingLayout (ScaleStyle) + +- (CGFloat)maximumScale { + return _maximumScale; +} + +- (void)setMaximumScale:(CGFloat)maximumScale { + _maximumScale = maximumScale; +} + +- (CGFloat)minimumScale { + return _minimumScale; +} + +- (void)setMinimumScale:(CGFloat)minimumScale { + _minimumScale = minimumScale; +} + +@end + +const CGFloat QMUICollectionViewPagingLayoutRotationRadiusAutomatic = -1.0; + +@implementation QMUICollectionViewPagingLayout (RotationStyle) + +- (CGFloat)rotationRatio { + return _rotationRatio; +} + +- (void)setRotationRatio:(CGFloat)rotationRatio { + _rotationRatio = [self validatedRotationRatio:rotationRatio]; +} + +- (CGFloat)rotationRadius { + return _rotationRadius; +} + +- (void)setRotationRadius:(CGFloat)rotationRadius { + _rotationRadius = rotationRadius; +} + +- (CGFloat)validatedRotationRatio:(CGFloat)rotationRatio { + return MAX(0.0, MIN(1.0, rotationRatio)); +} + +@end + +@implementation QMUICollectionViewPagingLayout + +- (instancetype)initWithStyle:(QMUICollectionViewPagingLayoutStyle)style { + if (self = [super init]) { + _style = style; + self.velocityForEnsurePageDown = 0.4; + self.allowsMultipleItemScroll = YES; + self.multipleItemScrollVelocityLimit = 2.5; + self.pagingThreshold = 2.0 / 3.0; + self.maximumScale = 1.0; + self.minimumScale = 0.94; + self.rotationRatio = .5; + self.rotationRadius = QMUICollectionViewPagingLayoutRotationRadiusAutomatic; + + self.minimumInteritemSpacing = 0; + self.scrollDirection = UICollectionViewScrollDirectionHorizontal; + } + return self; +} + +- (instancetype)init { + return [self initWithStyle:QMUICollectionViewPagingLayoutStyleDefault]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self init]; +} + +- (void)prepareLayout { + [super prepareLayout]; + CGSize itemSize = self.itemSize; + id layoutDelegate = (id)self.collectionView.delegate; + if ([layoutDelegate respondsToSelector:@selector(collectionView:layout:sizeForItemAtIndexPath:)]) { + itemSize = [layoutDelegate collectionView:self.collectionView layout:self sizeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + } + _finalItemSize = itemSize; + + if (self.debugLayer) { + if (self.scrollDirection == UICollectionViewScrollDirectionVertical) { + 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.adjustedContentInset.left + self.sectionInset.left + _finalItemSize.width / 2, 0, PixelOne, CGRectGetHeight(self.collectionView.bounds)); + } + } +} + +- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { + if (self.style == QMUICollectionViewPagingLayoutStyleScale || self.style == QMUICollectionViewPagingLayoutStyleRotation) { + return YES; + } + return !CGSizeEqualToSize(self.collectionView.bounds.size, newBounds.size); +} + +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { + if (self.style == QMUICollectionViewPagingLayoutStyleDefault) { + return [super layoutAttributesForElementsInRect:rect]; + } + + NSArray *resultAttributes = [[NSArray alloc] initWithArray:[super layoutAttributesForElementsInRect:rect] copyItems:YES]; + CGFloat offset = CGRectGetMidX(self.collectionView.bounds);// 当前滚动位置的可视区域的中心点 + CGSize itemSize = _finalItemSize; + + if (self.style == QMUICollectionViewPagingLayoutStyleScale) { + + CGFloat distanceForMinimumScale = itemSize.width + self.minimumLineSpacing; + CGFloat distanceForMaximumScale = 0.0; + + for (UICollectionViewLayoutAttributes *attributes in resultAttributes) { + CGFloat scale = 0; + CGFloat distance = ABS(offset - attributes.center.x); + if (distance >= distanceForMinimumScale) { + scale = self.minimumScale; + } else if (distance == distanceForMaximumScale) { + scale = self.maximumScale; + } else { + scale = self.minimumScale + (distanceForMinimumScale - distance) * (self.maximumScale - self.minimumScale) / (distanceForMinimumScale - distanceForMaximumScale); + } + attributes.transform3D = CATransform3DMakeScale(scale, scale, 1); + attributes.zIndex = 1; + } + return resultAttributes; + } + + if (self.style == QMUICollectionViewPagingLayoutStyleRotation) { + if (self.rotationRadius == QMUICollectionViewPagingLayoutRotationRadiusAutomatic) { + self.rotationRadius = itemSize.height; + } + UICollectionViewLayoutAttributes *centerAttribute = nil; + CGFloat centerMin = 10000; + for (UICollectionViewLayoutAttributes *attributes in resultAttributes) { + CGFloat distance = self.collectionView.contentOffset.x + CGRectGetWidth(self.collectionView.bounds) / 2.0 - attributes.center.x; + CGFloat degress = - 90 * self.rotationRatio * (distance / CGRectGetWidth(self.collectionView.bounds)); + CGFloat cosValue = ABS(cosf(AngleWithDegrees(degress))); + CGFloat translateY = self.rotationRadius - self.rotationRadius * cosValue; + CGAffineTransform transform = CGAffineTransformMakeTranslation(0, translateY); + transform = CGAffineTransformRotate(transform, AngleWithDegrees(degress)); + attributes.transform = transform; + attributes.zIndex = 1; + if (ABS(distance) < centerMin) { + centerMin = ABS(distance); + centerAttribute = attributes; + } + } + centerAttribute.zIndex = 10; + return resultAttributes; + } + + return resultAttributes; +} + +- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity { + + CGFloat itemSpacing = (self.scrollDirection == UICollectionViewScrollDirectionHorizontal ? _finalItemSize.width : _finalItemSize.height) + self.minimumLineSpacing; + + CGSize contentSize = self.collectionViewContentSize; + CGSize frameSize = self.collectionView.bounds.size; + UIEdgeInsets contentInset = self.collectionView.adjustedContentInset; + + BOOL scrollingToRight = proposedContentOffset.x < self.collectionView.contentOffset.x;// 代表 collectionView 期望的实际滚动方向是向右,但不代表手指拖拽的方向是向右,因为有可能此时已经在左边的尽头,继续往右拖拽,松手的瞬间由于回弹,这里会判断为是想向左滚动,但其实你的手指是向右拖拽 + BOOL scrollingToBottom = proposedContentOffset.y < self.collectionView.contentOffset.y; + BOOL forcePaging = NO; + + CGPoint translation = [self.collectionView.panGestureRecognizer translationInView:self.collectionView]; + + if (self.scrollDirection == UICollectionViewScrollDirectionVertical) { + if (!self.allowsMultipleItemScroll || ABS(velocity.y) <= ABS(self.multipleItemScrollVelocityLimit)) { + proposedContentOffset = self.collectionView.contentOffset;// 一次性滚多次的本质是系统根据速度算出来的 proposedContentOffset 可能比当前 contentOffset 大很多,所以这里既然限制了一次只能滚一页,那就直接取瞬时 contentOffset 即可。 + + // 只支持滚动一页 或者 支持滚动多页但是速度不够滚动多页,时,允许强制滚动 + if (ABS(velocity.y) > self.velocityForEnsurePageDown) { + forcePaging = YES; + } + } + + // 最顶/最底 + if (proposedContentOffset.y < -contentInset.top || proposedContentOffset.y >= contentSize.height + contentInset.bottom - frameSize.height) { + // iOS 10 及以上的版本,直接返回当前的 contentOffset,系统会自动帮你调整到边界状态,而 iOS 9 及以下的版本需要自己计算 + return proposedContentOffset; + } + + CGFloat progress = ((contentInset.top + proposedContentOffset.y) + _finalItemSize.height / 2/*因为第一个 item 初始状态中心点离 contentOffset.y 有半个 item 的距离*/) / itemSpacing; + NSInteger currentIndex = (NSInteger)progress; + NSInteger targetIndex = currentIndex; + // 加上下面这两个额外的 if 判断是为了避免那种“从0滚到1的左边 1/3,松手后反而会滚回0”的 bug + if (translation.y < 0 && (ABS(translation.y) > _finalItemSize.height / 2 + self.minimumLineSpacing)) { + } else if (translation.y > 0 && ABS(translation.y > _finalItemSize.height / 2)) { + } else { + CGFloat remainder = progress - currentIndex; + CGFloat offset = remainder * itemSpacing; + 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; + } + else if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal) { + if (!self.allowsMultipleItemScroll || ABS(velocity.x) <= ABS(self.multipleItemScrollVelocityLimit)) { + proposedContentOffset = self.collectionView.contentOffset;// 一次性滚多次的本质是系统根据速度算出来的 proposedContentOffset 可能比当前 contentOffset 大很多,所以这里既然限制了一次只能滚一页,那就直接取瞬时 contentOffset 即可。 + + // 只支持滚动一页 或者 支持滚动多页但是速度不够滚动多页,时,允许强制滚动 + if (ABS(velocity.x) > self.velocityForEnsurePageDown) { + forcePaging = YES; + } + } + + // 最左/最右 + if (proposedContentOffset.x < -contentInset.left || proposedContentOffset.x >= contentSize.width + contentInset.right - frameSize.width) { + // iOS 10 及以上的版本,直接返回当前的 contentOffset,系统会自动帮你调整到边界状态,而 iOS 9 及以下的版本需要自己计算 + return proposedContentOffset; + } + + CGFloat progress = ((contentInset.left + proposedContentOffset.x) + _finalItemSize.width / 2/*因为第一个 item 初始状态中心点离 contentOffset.x 有半个 item 的距离*/) / itemSpacing; + NSInteger currentIndex = (NSInteger)progress; + NSInteger targetIndex = currentIndex; + // 加上下面这两个额外的 if 判断是为了避免那种“从0滚到1的左边 1/3,松手后反而会滚回0”的 bug + if (translation.x < 0 && (ABS(translation.x) > _finalItemSize.width / 2 + self.minimumLineSpacing)) { + } else if (translation.x > 0 && ABS(translation.x > _finalItemSize.width / 2)) { + } else { + CGFloat remainder = progress - currentIndex; + CGFloat offset = remainder * itemSpacing; + // 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; + } + + return proposedContentOffset; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h new file mode 100644 index 00000000..27211b87 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h @@ -0,0 +1,95 @@ +/** + * 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. + */ + +// +// QMUIConsole.h +// QMUIKit +// +// Created by MoLice on 2019/J/11. +// + +#import +#import +#import "QMUIConsoleToolbar.h" +#import "QMUIConsoleViewController.h" +#import "QMUILog+QMUIConsole.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + 在设备屏幕上显示一个控制台,输出代码里的日志。支持搜索、按 Level/Name 过滤。用法: + + 1. 调用 [QMUIConsole log:...] 直接打印 level 为 "Normal"、name 为 "Default" 的日志。 + 2. 调用 [QMUIConsole logWithLevel:name:logString:] 打印详细日志,则在控制台里可以按照 level 和 name 分类筛选显示。 + 3. 当屏幕上出现小圆钮时,点击可以打开控制台,小圆钮会移动到控制台右上角,再次点击小圆钮即可收起控制台。 + 4. 如果要隐藏小圆钮,长按即可。 + + @note 默认只在 DEBUG 下才会显示窗口,其他环境下只会打印日志但不会出现控制台界面。可通过 canShow 属性修改这个策略。 + */ +@interface QMUIConsole : NSObject + ++ (nonnull instancetype)sharedInstance; + +/** + 打印日志到控制台 + + @param level 级别分类,业务自己规定一套统一的划分方式即可,如果 nil 则默认为 @"Normal" + @param name 日志的业务分类,例如属于某个控件、某种类型,也是自己规定一套统一的划分方式即可,如果 nil 则默认为 @"Default" + @param logString 支持 NSString/NSAttributedString/NSObject,如果是 NSString 则默认样式由 [QMUIConsole appearance].textAttributes 控制 + */ ++ (void)logWithLevel:(nullable NSString *)level name:(nullable NSString *)name logString:(id)logString; + +/** + 相当于 level:@"Normal" name:@"Default" 的 log + + @param logString 支持 NSString/NSAttributedString/NSObject,如果是 NSString 则默认样式由 [QMUIConsole appearance].textAttributes 控制 + */ ++ (void)log:(id)logString; + +/** + 清空当前控制台内容 + */ ++ (void)clear; + +/** + 显示控制台。由于 QMUIConsole.showConsoleAutomatically 默认为 YES,所以只要输出 log 就会自动显示控制台,一般无需手动调用 show 方法。 + */ ++ (void)show; + +/** + 隐藏控制台。 + */ ++ (void)hide; + +/// 决定控制台是否能显示出来,当值为 NO 时,即便 +show 方法被调用也不会显示控制台,默认在 DEBUG 下为 YES,其他环境下为 NO。业务项目可自行修改。 +/// 这个值为 NO 也不影响日志的打印,只是不会显示出来而已。 +@property(nonatomic, assign) BOOL canShow; + +/// 当打印 log 的时候自动让控制台显示出来,默认为 YES,为 NO 时则只记录 log,当手动调用 +show 方法时才会出现控制台。 +@property(nonatomic, assign) BOOL showConsoleAutomatically; + +/// 控制台的背景色 +@property(nullable, nonatomic, strong) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; + +/// 控制台文本的默认样式 +@property(nullable, nonatomic, strong) NSDictionary *textAttributes UI_APPEARANCE_SELECTOR; + +/// log 里的时间戳的颜色 +@property(nullable, nonatomic, strong) NSDictionary *timeAttributes UI_APPEARANCE_SELECTOR; + +/// 搜索结果高亮的背景色 +@property(nullable, nonatomic, strong) UIColor *searchResultHighlightedBackgroundColor UI_APPEARANCE_SELECTOR; +@end + +@interface QMUIConsole (UIAppearance) + ++ (nonnull instancetype)appearance; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m new file mode 100644 index 00000000..5e7ed03f --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m @@ -0,0 +1,148 @@ +/** + * 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. + */ + +// +// QMUIConsole.m +// QMUIKit +// +// Created by MoLice on 2019/J/11. +// + +#import "QMUIConsole.h" +#import "QMUICore.h" +#import "NSParagraphStyle+QMUI.h" +#import "UIView+QMUI.h" +#import "UIWindow+QMUI.h" +#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) QMUIConsoleWindow *consoleWindow; +@property(nonatomic, strong) QMUIConsoleViewController *consoleViewController; +@end + +@implementation QMUIConsole + ++ (instancetype)sharedInstance { + static dispatch_once_t onceToken; + static QMUIConsole *instance = nil; + dispatch_once(&onceToken,^{ + instance = [[super allocWithZone:NULL] init]; + instance.canShow = IS_DEBUG; + instance.showConsoleAutomatically = YES; + instance.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:.8]; + instance.textAttributes = @{NSFontAttributeName: [UIFont fontWithName:@"Menlo" size:12], + NSForegroundColorAttributeName: [UIColor whiteColor], + NSParagraphStyleAttributeName: ({ + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16]; + paragraphStyle.paragraphSpacing = 8; + paragraphStyle; + }), + }; + instance.timeAttributes = ({ + NSMutableDictionary *attributes = instance.textAttributes.mutableCopy; + attributes[NSForegroundColorAttributeName] = [attributes[NSForegroundColorAttributeName] qmui_colorWithAlpha:.6 backgroundColor:instance.backgroundColor]; + attributes.copy; + }); + instance.searchResultHighlightedBackgroundColor = [UIColorBlue colorWithAlphaComponent:.8]; + }); + return instance; +} + ++ (instancetype)appearance { + return [self sharedInstance]; +} + ++ (id)allocWithZone:(struct _NSZone *)zone{ + return [self sharedInstance]; +} + ++ (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) { + [QMUIConsole show]; + } +} + ++ (void)log:(id)logString { + [self logWithLevel:nil name:nil logString:logString]; +} + ++ (void)clear { + [[QMUIConsole sharedInstance].consoleViewController clear]; +} + ++ (void)show { + QMUIConsole *console = [QMUIConsole sharedInstance]; + if (console.canShow) { + + 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]; + } +} + ++ (void)hide { + [QMUIConsole sharedInstance].consoleWindow.hidden = YES; +} + +- (void)initConsoleWindowIfNeeded { + if (!self.consoleWindow) { + self.consoleWindow = [[QMUIConsoleWindow alloc] init]; + self.consoleViewController = [[QMUIConsoleViewController alloc] init]; + self.consoleWindow.rootViewController = self.consoleViewController; + } +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor { + _backgroundColor = backgroundColor; + self.consoleViewController.backgroundColor = backgroundColor; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h new file mode 100644 index 00000000..8419c765 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h @@ -0,0 +1,36 @@ +/** + * 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. + */ + +// +// QMUIConsoleToolbar.h +// QMUIKit +// +// Created by MoLice on 2019/J/11. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIButton; +@class QMUITextField; + +@interface QMUIConsoleToolbar : UIView + +@property(nonatomic, strong, readonly) QMUIButton *levelButton; +@property(nonatomic, strong, readonly) QMUIButton *nameButton; +@property(nonatomic, strong, readonly) QMUIButton *clearButton; +@property(nonatomic, strong, readonly) QMUITextField *searchTextField; +@property(nonatomic, strong, readonly) UILabel *searchResultCountLabel; +@property(nonatomic, strong, readonly) QMUIButton *searchResultPreviousButton; +@property(nonatomic, strong, readonly) QMUIButton *searchResultNextButton; + +- (void)setNeedsLayoutSearchResultViews; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m new file mode 100644 index 00000000..c8958fff --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m @@ -0,0 +1,156 @@ +/** + * 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. + */ + +// +// QMUIConsoleToolbar.m +// QMUIKit +// +// Created by MoLice on 2019/J/11. +// + +#import "QMUIConsoleToolbar.h" +#import "QMUIConsole.h" +#import "QMUICore.h" +#import "QMUIButton.h" +#import "QMUITextField.h" +#import "UITextField+QMUI.h" +#import "UIImage+QMUI.h" +#import "UIView+QMUI.h" +#import "UIColor+QMUI.h" +#import "UIImage+QMUI.h" +#import "UIControl+QMUI.h" + +@interface QMUIConsoleToolbar () + +@property(nonatomic, strong) UIView *searchRightView; +@end + +@implementation QMUIConsoleToolbar + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + _levelButton = [[QMUIButton alloc] init]; + UIImage *filterImage = [[QMUIHelper imageWithName:@"QMUI_console_filter"] qmui_imageResizedInLimitedSize:CGSizeMake(14, 14)]; + UIImage *filterSelectedImage = [[QMUIHelper imageWithName:@"QMUI_console_filter_selected"] qmui_imageResizedInLimitedSize:CGSizeMake(14, 14)]; + + [self.levelButton setImage:filterImage forState:UIControlStateNormal]; + [self.levelButton setImage:filterSelectedImage forState:UIControlStateSelected]; + [self.levelButton setImage:filterSelectedImage forState:UIControlStateSelected|UIControlStateHighlighted]; + [self.levelButton setImage:filterSelectedImage forState:UIControlStateSelected|UIControlStateDisabled]; + [self.levelButton setTitle:@"Level" forState:UIControlStateNormal]; + self.levelButton.titleLabel.font = UIFontMake(7); + self.levelButton.imagePosition = QMUIButtonImagePositionTop; + self.levelButton.tintColorAdjustsTitleAndImage = UIColorWhite; + [self addSubview:self.levelButton]; + + _nameButton = [[QMUIButton alloc] init]; + [self.nameButton setImage:filterImage forState:UIControlStateNormal]; + [self.nameButton setImage:filterSelectedImage forState:UIControlStateSelected]; + [self.nameButton setImage:filterSelectedImage forState:UIControlStateSelected|UIControlStateHighlighted]; + [self.nameButton setImage:filterSelectedImage forState:UIControlStateSelected|UIControlStateDisabled]; + [self.nameButton setTitle:@"Name" forState:UIControlStateNormal]; + self.nameButton.titleLabel.font = UIFontMake(7); + self.nameButton.imagePosition = QMUIButtonImagePositionTop; + self.nameButton.tintColorAdjustsTitleAndImage = UIColorWhite; + [self addSubview:self.nameButton]; + + _searchTextField = [[QMUITextField alloc] init]; + self.searchTextField.clearButtonMode = UITextFieldViewModeWhileEditing; + self.searchTextField.tintColor = [QMUIConsole appearance].textAttributes[NSForegroundColorAttributeName]; + self.searchTextField.textColor = self.searchTextField.tintColor; + self.searchTextField.placeholderColor = [self.searchTextField.textColor colorWithAlphaComponent:.6]; + self.searchTextField.font = [QMUIConsole appearance].textAttributes[NSFontAttributeName]; + self.searchTextField.keyboardAppearance = UIKeyboardAppearanceDark; + self.searchTextField.returnKeyType = UIReturnKeySearch; + self.searchTextField.autocapitalizationType = UITextAutocapitalizationTypeNone; + self.searchTextField.autocorrectionType = UITextAutocorrectionTypeNo; + self.searchTextField.layer.borderWidth = PixelOne; + self.searchTextField.layer.borderColor = [[UIColor whiteColor] colorWithAlphaComponent:.3].CGColor; + self.searchTextField.layer.cornerRadius = 3; + self.searchTextField.placeholder = @"Search..."; + [self addSubview:self.searchTextField]; + + _clearButton = [[QMUIButton alloc] init]; + [self.clearButton setImage:[QMUIHelper imageWithName:@"QMUI_console_clear"] forState:UIControlStateNormal]; + [self addSubview:self.clearButton]; + + self.searchRightView = [[UIView alloc] init]; + + _searchResultCountLabel = [[UILabel alloc] init]; + self.searchResultCountLabel.textColor = self.searchTextField.placeholderColor; + self.searchResultCountLabel.font = UIFontMake(11); + [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]; + [self.searchResultPreviousButton sizeToFit]; + [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]; + [self.searchResultNextButton sizeToFit]; + [self.searchRightView addSubview:self.searchResultNextButton]; + + self.searchTextField.rightView = self.searchRightView; + self.searchTextField.rightViewMode = UITextFieldViewModeNever; + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + UIEdgeInsets paddings = UIEdgeInsetsMake(8, 8, 8, 8); + + 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); + + 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.safeAreaInsets.right - paddings.right - contentHeight); + + CGFloat searchTextFieldMarginHorizontal = 8; + CGFloat searchTextFieldMinX = x + searchTextFieldMarginHorizontal; + self.searchTextField.frame = CGRectMake(searchTextFieldMinX, paddings.top, CGRectGetMinX(self.clearButton.frame) - searchTextFieldMarginHorizontal - searchTextFieldMinX, contentHeight); +} + +- (void)setNeedsLayoutSearchResultViews { + CGFloat paddingHorizontal = 4; + CGFloat buttonSpacing = 2; + CGFloat countLabelMarginRight = 4; + [self.searchResultCountLabel sizeToFit]; + + self.searchRightView.qmui_width = paddingHorizontal * 2 + self.searchResultCountLabel.qmui_width + countLabelMarginRight + self.searchResultPreviousButton.qmui_width + buttonSpacing + self.searchResultNextButton.qmui_width; + self.searchRightView.qmui_height = self.searchTextField.qmui_height; + + self.searchResultNextButton.qmui_right = self.searchRightView.qmui_width - paddingHorizontal; + self.searchResultNextButton.qmui_top = self.searchResultNextButton.qmui_topWhenCenterInSuperview; + self.searchResultNextButton.qmui_outsideEdge = UIEdgeInsetsMake(-self.searchResultNextButton.qmui_top, -buttonSpacing / 2, -self.searchResultNextButton.qmui_top, -paddingHorizontal); + + self.searchResultPreviousButton.qmui_right = self.searchResultNextButton.qmui_left - buttonSpacing; + self.searchResultPreviousButton.qmui_top = self.searchResultPreviousButton.qmui_topWhenCenterInSuperview; + self.searchResultNextButton.qmui_outsideEdge = UIEdgeInsetsMake(-self.searchResultPreviousButton.qmui_top, -buttonSpacing / 2, -self.searchResultPreviousButton.qmui_top, -paddingHorizontal); + + + self.searchResultCountLabel.qmui_right = self.searchResultPreviousButton.qmui_left - countLabelMarginRight; + self.searchResultCountLabel.qmui_top = self.searchResultCountLabel.qmui_topWhenCenterInSuperview; + + [self.searchTextField setNeedsLayout]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h new file mode 100644 index 00000000..7c264272 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h @@ -0,0 +1,39 @@ +/** + * 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. +// + +#import +#import +#import "QMUICommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIButton; +@class QMUITableView; +@class QMUIConsoleToolbar; + +@interface QMUIConsoleViewController : QMUICommonViewController + +@property(nonatomic, strong, readonly) QMUIButton *popoverButton; +@property(nonatomic, strong, readonly) QMUITableView *tableView; +@property(nonatomic, strong, readonly) QMUIConsoleToolbar *toolbar; +@property(nonatomic, strong, readonly) NSDateFormatter *dateFormatter; + +@property(nonatomic, strong) UIColor *backgroundColor; + +- (void)logWithLevel:(nullable NSString *)level name:(nullable NSString *)name logString:(id)logString; +- (void)log:(id)logString; +- (void)clear; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m new file mode 100644 index 00000000..696aff20 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m @@ -0,0 +1,706 @@ +/** + * 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. +// + +#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" +#import "UIScrollView+QMUI.h" +#import "UIViewController+QMUI.h" +#import "UIView+QMUI.h" +#import "UIImage+QMUI.h" +#import "NSObject+QMUI.h" +#import "CAAnimation+QMUI.h" +#import "NSArray+QMUI.h" +#import "QMUIConsole.h" +#import "QMUIPopupMenuView.h" + +@interface QMUIConsoleLogItem : NSObject + +@property(nullable, nonatomic, copy) NSString *level; +@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 = [[self alloc] init]; + logItem.level = level ?: @"Normal"; + logItem.name = name ?: @"Default"; + + NSDictionary *timeAttributes = [QMUIConsole appearance].timeAttributes; + logItem.timeString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ ", timeString] attributes:timeAttributes]; + + NSDictionary *textAttributes = [QMUIConsole appearance].textAttributes; + NSAttributedString *string = nil; + if ([logString isKindOfClass:[NSAttributedString class]]) { + string = (NSAttributedString *)logString; + } else if ([logString isKindOfClass:[NSString class]]) { + string = [[NSAttributedString alloc] initWithString:(NSString *)logString attributes:textAttributes]; + } else { + string = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", logString] attributes:textAttributes]; + } + 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 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, 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; +@property(nonatomic, assign) BOOL popoverAnimating; +@end + +@implementation QMUIConsoleViewController + +- (void)didInitialize { + [super didInitialize]; + self.backgroundColor = [QMUIConsole appearance].backgroundColor; + + _dateFormatter = [[NSDateFormatter alloc] init]; + self.dateFormatter.dateFormat = @"HH:mm:ss.SSS"; + + self.logItems = [[NSMutableArray alloc] init]; + self.selectedLevels = [[NSMutableArray alloc] init]; + self.selectedNames = [[NSMutableArray alloc] init]; +} + +- (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.view addSubview:self.containerView]; + [self.containerView addSubview:self.tableView]; + [self.containerView addSubview:self.toolbar]; + + __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 generatePopupMenuViewWithSelectedArray:self.selectedNames]; + self.nameMenu.willHideBlock = ^(BOOL hidesByUserTap, BOOL animated) { + weakSelf.toolbar.nameButton.selected = weakSelf.selectedNames.count > 0; + }; + self.nameMenu.sourceView = self.toolbar.nameButton; + + [self updateToolbarButtonState]; + + [self.view addSubview:self.popoverButton]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.view.backgroundColor = [UIColor clearColor]; + __weak __typeof(self)weakSelf = self; + 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]) { + [menuView hideWithAnimated:YES]; + return weakSelf.view;// 也即不再传递这次事件了,相当于无效点击 + } + + if (originalView == weakSelf.view) { + if (weakSelf.toolbar.searchTextField.isFirstResponder) { + [weakSelf.view endEditing:YES]; + } + return nil; + } + return originalView; + }; +} + +- (CGRect)safetyPopoverButtonFrame:(CGRect)popoverButtonFrame { + CGRect safetyBounds = CGRectInsetEdges(self.view.bounds, self.view.safeAreaInsets); + if (!CGRectContainsRect(safetyBounds, self.popoverButton.frame)) { + 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; +} + +- (void)layoutPopoverButton { + if (self.popoverPanGesture.enabled) { + CGPoint popoverButtonOrigin; + NSValue *bindObject = [self.popoverButton qmui_getBoundObjectForKey:@"origin"]; + if (bindObject) { + popoverButtonOrigin = ((NSValue *)[self.popoverButton qmui_getBoundObjectForKey:@"origin"]).CGPointValue; + } else { + 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 { + self.popoverButton.qmui_frameApplyTransform = CGRectSetXY(self.popoverButton.frame, CGRectGetMaxX(self.containerView.frame) - 10 - CGRectGetWidth(self.popoverButton.bounds), CGRectGetMinY(self.containerView.frame) + 10); + } +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + if (self.popoverAnimating) return; + + [self layoutPopoverButton]; + + 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.safeAreaInsets.bottom; + self.toolbar.qmui_height = toolbarHeight; + self.toolbar.qmui_width = self.containerView.qmui_width; + self.toolbar.qmui_bottom = self.containerView.qmui_height; + + 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.safeAreaInsets); + }]; +} + +- (BOOL)shouldAutorotate { + return [QMUIHelper visibleViewController].shouldAutorotate; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + return [QMUIHelper visibleViewController].supportedInterfaceOrientations; +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor { + _backgroundColor = backgroundColor; + if (self.isViewLoaded) { + self.containerView.backgroundColor = 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]; + dispatch_async(dispatch_get_main_queue(), ^{// 避免频繁打 log 时卡顿 + [self printLog]; + }); +} + +- (void)log:(id)logString { + [self logWithLevel:nil name:nil logString:logString]; +} + +- (void)printLog { + 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]; + return shouldPrintLevel && shouldPrintName; + }]; + 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 时内部会自动滚动 + } + }]; + } +} + +- (void)clear { + [self.selectedLevels removeAllObjects]; + [self.selectedNames removeAllObjects]; + [self.logItems removeAllObjects]; + self.toolbar.levelButton.enabled = NO; + self.toolbar.levelButton.selected = NO; + self.toolbar.nameButton.enabled = NO; + self.toolbar.nameButton.selected = NO; + [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 + +- (void)handlePopoverTouchEvent:(QMUIButton *)button { + [self.view setNeedsLayout]; + [self.view layoutIfNeeded]; + + self.popoverAnimating = YES; + CGAffineTransform scale = CGAffineTransformMakeScale(CGRectGetWidth(self.popoverButton.frame) / CGRectGetWidth(self.containerView.frame), CGRectGetHeight(self.popoverButton.frame) / CGRectGetHeight(self.containerView.frame)); + CGAffineTransform translation = CGAffineTransformMakeTranslation(self.popoverButton.center.x - self.containerView.center.x, self.popoverButton.center.y - self.containerView.center.y); + CGAffineTransform transform = CGAffineTransformConcat(scale, translation); + CGFloat cornerRadius = MIN(CGRectGetWidth(self.containerView.bounds), CGRectGetHeight(self.containerView.bounds)); + + if (self.containerView.hidden) { + self.popoverPanGesture.enabled = NO; + self.popoverLongPressGesture.enabled = NO; + [UIView animateWithDuration:.1 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + self.popoverButton.alpha = 0; + } completion:nil]; + + self.containerView.alpha = 0; + self.containerView.hidden = NO; + self.containerView.layer.cornerRadius = cornerRadius / 2; + self.containerView.transform = transform; + [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.69 initialSpringVelocity:5 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + self.containerView.alpha = 1; + self.containerView.transform = CGAffineTransformIdentity; + self.containerView.layer.cornerRadius = 0; + } completion:^(BOOL finished) { + [self layoutPopoverButton]; + self.popoverButton.transform = CGAffineTransformMakeScale(0, 0); + [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.69 initialSpringVelocity:5 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + self.popoverButton.alpha = .3; + self.popoverButton.transform = CGAffineTransformIdentity; + } completion:^(BOOL finished) { + self.popoverAnimating = NO; + }]; + }]; + } else { + self.popoverPanGesture.enabled = YES; + self.popoverLongPressGesture.enabled = YES; + [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.69 initialSpringVelocity:5 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + self.popoverButton.alpha = 1; + self.containerView.alpha = 0; + self.containerView.transform = transform; + self.containerView.layer.cornerRadius = cornerRadius / 2; + [self.view endEditing:YES]; + } completion:^(BOOL finished) { + self.containerView.hidden = YES; + self.containerView.transform = CGAffineTransformIdentity; + self.containerView.layer.cornerRadius = 0; + + [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.69 initialSpringVelocity:5 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + [self layoutPopoverButton]; + self.popoverButton.alpha = 1; + } completion:^(BOOL finished) { + self.popoverAnimating = NO; + }]; + }]; + } +} + +- (void)handlePopoverPanGestureRecognizer:(UIPanGestureRecognizer *)gesture { + switch (gesture.state) { + case UIGestureRecognizerStateBegan: + self.popoverAnimating = YES; + [self.popoverButton qmui_bindObject:[NSValue valueWithCGPoint:self.popoverButton.frame.origin] forKey:@"origin"]; + break; + case UIGestureRecognizerStateChanged: { + CGPoint translation = [gesture translationInView:self.view]; + self.popoverButton.transform = CGAffineTransformMakeTranslation(translation.x, translation.y); + } + break; + case UIGestureRecognizerStateCancelled: + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateFailed: { + CGRect popoverButtonFrame = [self safetyPopoverButtonFrame:self.popoverButton.frame]; + BOOL animated = CGRectEqualToRect(popoverButtonFrame, self.popoverButton.frame); + [UIView qmui_animateWithAnimated:animated duration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + self.popoverButton.transform = CGAffineTransformIdentity; + self.popoverButton.frame = popoverButtonFrame; + } completion:^(BOOL finished) { + [self.popoverButton qmui_bindObject:[NSValue valueWithCGPoint:popoverButtonFrame.origin] forKey:@"origin"]; + self.popoverAnimating = NO; + }]; + } + break; + default: + break; + } +} + +- (void)handlePopverLongPressGestureRecognizer:(UILongPressGestureRecognizer *)gesture { + if (gesture.state == UIGestureRecognizerStateBegan) { + CFTimeInterval duration = 0.5; + CAKeyframeAnimation *scale = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; + scale.values = @[@1.0, @1.2, @0.2]; + scale.keyTimes = @[@0.0, @(.2 / duration), @1]; + scale.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; + scale.duration = duration; + scale.fillMode = kCAFillModeForwards; + scale.removedOnCompletion = NO; + __weak __typeof(self)weakSelf = self; + scale.qmui_animationDidStopBlock = ^(__kindof CAAnimation *aAnimation, BOOL finished) { + [QMUIConsole hide]; + [weakSelf.popoverButton.layer removeAnimationForKey:@"scale"]; + [[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium] impactOccurred]; + }; + [self.popoverButton.layer addAnimation:scale forKey:@"scale"]; + } +} + +#pragma mark - Toolbar Buttons + +- (void)updateToolbarButtonState { + self.toolbar.levelButton.enabled = self.logItems.count > 0; + self.toolbar.nameButton.enabled = self.logItems.count > 0; +} + +- (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.maskViewBackgroundColor = nil; + menuView.backgroundColor = UIColorWhite; + 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: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 { + __weak __typeof(self)weakSelf = self; + 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) { + 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]; + }]; + [items addObject:item]; + }]; + return items.copy; +} + +- (void)handleLevelButtonEvent:(UIButton *)button { + self.levelMenu.items = [self popupMenuItemsByTitleBlock:^NSString *(QMUIConsoleLogItem *logItem) { + return logItem.level; + } selectedArray:self.selectedLevels]; + [self.levelMenu showWithAnimated:YES]; + button.selected = YES; +} + +- (void)handleNameButtonEvent:(UIButton *)button { + self.nameMenu.items = [self popupMenuItemsByTitleBlock:^NSString *(QMUIConsoleLogItem *logItem) { + return logItem.name; + } selectedArray:self.selectedNames]; + [self.nameMenu showWithAnimated:YES]; + button.selected = YES; +} + +- (void)handleClearButtonEvent:(UIButton *)button { + [self clear]; +} + +#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]; + + 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.searchResultsTotalCount - 1; + } else { + self.currentHighlightedResultIndex --; + } +} + +- (void)handleSearchResultNextButtonEvent:(QMUIButton *)button { + if (self.currentHighlightedResultIndex == self.searchResultsTotalCount - 1) { + self.currentHighlightedResultIndex = 0; + } else { + self.currentHighlightedResultIndex ++; + } +} + +- (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]; + } + } + } +} + +- (void)handleKeyboardWillChangeFrame:(QMUIKeyboardUserInfo *)userInfo { + CGFloat height = [userInfo heightInView:self.view]; + self.containerView.transform = CGAffineTransformMakeTranslation(0, -height); + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.levelMenu.isShowing) [self.levelMenu updateLayout]; + if (self.nameMenu.isShowing) [self.nameMenu updateLayout]; + }); +} + +#pragma mark - + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + return YES; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h new file mode 100644 index 00000000..5b1259ed --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h @@ -0,0 +1,24 @@ +/** + * 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. + */ + +// +// QMUILog+QMUIConsole.h +// QMUIKit +// +// Created by MoLice on 2019/J/15. +// + +#import "QMUILog.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QMUILogger (QMUIConsole) + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m new file mode 100644 index 00000000..1eb1526f --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m @@ -0,0 +1,55 @@ +/** + * 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. + */ + +// +// QMUILog+QMUIConsole.m +// QMUIKit +// +// Created by MoLice on 2019/J/15. +// + +#import "QMUILog+QMUIConsole.h" +#import "QMUIConsole.h" +#import "QMUICore.h" + +@implementation QMUILogger (QMUIConsole) + ++ (void)load { + static dispatch_once_t onceToken; + 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) { + + // call super + void (*originSelectorIMP)(id, SEL, const char *, int, const char *, QMUILogItem *); + originSelectorIMP = (void (*)(id, SEL, const char *, int, const char *, QMUILogItem *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, file, line, func, logItem); + + if (!QMUICMIActivated || !ShouldPrintQMUIWarnLogToConsole) return; + if (!logItem.enabled) return; + if (logItem.level != QMUILogLevelWarn) return; + + 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(); + } + + }; + }); + }); +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.h new file mode 100644 index 00000000..e1396ef7 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.h @@ -0,0 +1,228 @@ +/** + * 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. + */ + +// +// QMUIDialogViewController.h +// WeRead +// +// Created by QMUI Team on 16/7/8. +// + +#import +#import "QMUICommonViewController.h" +#import "QMUIModalPresentationViewController.h" +#import "QMUITableView.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIButton; +@class QMUILabel; +@class QMUITextField; +@class QMUITableViewCell; + +/** + * 弹窗组件基类,自带`headerView`、`contentView`、`footerView`,并通过`addCancelButtonWithText:block:`、`addSubmitButtonWithText:block:`方法来添加取消、确定按钮。 + * 建议将一个自定义的UIView设置给`contentView`属性,此时弹窗将会自动帮你计算大小并布局。大小取决于你的contentView的sizeThatFits:返回值。 + * 弹窗继承自`QMUICommonViewController`,因此可直接使用self.titleView的功能来实现双行标题,具体请查看`QMUINavigationTitleView`。 + * `QMUIDialogViewController`支持以类似`UIAppearance`的方式来统一设置全局的dialog样式,例如`[QMUIDialogViewController appearance].headerViewHeight = 48;`。 + * + * @see QMUIDialogSelectionViewController + * @see QMUIDialogTextFieldViewController + */ +@interface QMUIDialogViewController : QMUICommonViewController + +@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR; +@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; +@property(nullable, nonatomic, strong) UIColor *headerViewBackgroundColor UI_APPEARANCE_SELECTOR; +@property(nonatomic, assign) UIEdgeInsets contentViewMargins UI_APPEARANCE_SELECTOR; +@property(nullable, nonatomic, strong) UIColor *contentViewBackgroundColor UI_APPEARANCE_SELECTOR;// 对自定义 contentView 无效 +@property(nullable, nonatomic, strong) UIColor *footerSeparatorColor UI_APPEARANCE_SELECTOR; +@property(nonatomic, assign) CGFloat footerViewHeight UI_APPEARANCE_SELECTOR; +@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, copy) NSDictionary *buttonTitleAttributes UI_APPEARANCE_SELECTOR; + +@property(nullable, nonatomic, strong, readonly) UIView *headerView; +@property(nullable, nonatomic, strong, readonly) CALayer *headerViewSeparatorLayer; + +/// dialog的主体内容部分,默认是一个空的白色UIView,建议设置为自己的UIView +/// dialog会通过询问contentView的sizeThatFits得到当前内容的大小 +@property(nullable, nonatomic, strong) UIView *contentView; + +@property(nullable, nonatomic, strong, readonly) UIView *footerView; +@property(nullable, nonatomic, strong, readonly) CALayer *footerViewSeparatorLayer; + +@property(nullable, nonatomic, strong, readonly) QMUIButton *cancelButton; +@property(nullable, nonatomic, strong, readonly) QMUIButton *submitButton; +@property(nullable, nonatomic, strong, readonly) CALayer *buttonSeparatorLayer; + +/** + 添加位于左下角的取消按钮,取消按钮点击时默认会自动 hide 弹窗,无需自己在 block 里调用 hide。 + + 同一时间只能存在一个取消按钮,所以每次添加都会移除上一个取消按钮。 + + @param buttonText 按钮文字 + @param block 按钮点击后的事件。取消按钮会自动 hide 弹窗,无需在 block 里调用 hide + */ +- (void)addCancelButtonWithText:(NSString *)buttonText block:(void (^ _Nullable)(__kindof QMUIDialogViewController *aDialogViewController))block; + +/** + 移除当前的取消按钮 + */ +- (void)removeCancelButton; + +/** + 添加位于右下角的提交按钮 + + 同一时间只能存在一个提交按钮,所以每次添加都会移除上一个提交按钮 + + @param buttonText 按钮文字 + @param block 按钮点击后的事件,如果需要在点击后关闭浮层,需要在 block 里自行调用 hide + */ +- (void)addSubmitButtonWithText:(NSString *)buttonText block:(void (^ _Nullable)(__kindof QMUIDialogViewController *aDialogViewController))block; + +/** + 移除提交按钮 + */ +- (void)removeSubmitButton; + +/** + 用于展示 dialog 的 modalPresentationViewController + */ +@property(nullable, nonatomic, strong) QMUIModalPresentationViewController *modalPresentationViewController; + +/** + 以动画形式显示弹窗,等同于 [self showWithAnimated:YES completion:nil] + */ +- (void)show; + +/** + 显示弹窗 + + @param animated 是否用动画的形式 + @param completion 弹窗显示出来后的回调 + */ +- (void)showWithAnimated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion; + +/** + 以动画形式隐藏弹窗,等同于 [self hideWithAnimated:YES completion:nil] + */ +- (void)hide; + +/** + 隐藏弹窗 + + @param animated 是否用动画的形式 + @param completion 弹窗隐藏后的回调 + */ +- (void)hideWithAnimated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion; + +@end + +@interface QMUIDialogViewController (UIAppearance) + ++ (instancetype)appearance; +@end + +/// 表示没有选中的item +extern const NSInteger QMUIDialogSelectionViewControllerSelectedItemIndexNone; + +/** + * 支持列表选择的弹窗,通过 `items` 指定要展示的所有选项(暂时只支持`NSString`)。默认使用单选,可通过 `allowsMultipleSelection` 支持多选。 + * 单选模式下,通过 `selectedItemIndex` 可获取当前被选中的选项,也可在初始化完dialog后设置这个属性来达到默认值的效果。 + * 多选模式下,通过 `selectedItemIndexes` 可获取当前被选中的多个选项,可也在初始化完dialog后设置这个属性来达到默认值的效果。 + */ +@interface QMUIDialogSelectionViewController : QMUIDialogViewController + +/// 每一行的高度,如果使用了 heightForItemBlock 则该属性不生效,默认值为配置表里的 TableViewCellNormalHeight +@property(nonatomic, assign) CGFloat rowHeight UI_APPEARANCE_SELECTOR; + +@property(nullable, nonatomic, strong, readonly) QMUITableView *tableView; + +@property(nullable, nonatomic, copy) NSArray *items; + +/// 表示单选模式下已选中的item序号,默认为QMUIDialogSelectionViewControllerSelectedItemIndexNone。此属性与 `selectedItemIndexes` 互斥。 +@property(nonatomic, assign) NSInteger selectedItemIndex; + +/// 表示多选模式下已选中的item序号,默认为nil。此属性与 `selectedItemIndex` 互斥。 +@property(nullable, nonatomic, strong) NSMutableSet *selectedItemIndexes; + +/// 控制是否允许多选,默认为NO。 +@property(nonatomic, assign) BOOL allowsMultipleSelection; + +@property(nullable, nonatomic, copy) void (^cellForItemBlock)(__kindof QMUIDialogSelectionViewController *aDialogViewController, __kindof QMUITableViewCell *cell, NSUInteger itemIndex); +@property(nullable, nonatomic, copy) CGFloat (^heightForItemBlock)(__kindof QMUIDialogSelectionViewController *aDialogViewController, NSUInteger itemIndex); +@property(nullable, nonatomic, copy) BOOL (^canSelectItemBlock)(__kindof QMUIDialogSelectionViewController *aDialogViewController, NSUInteger itemIndex); +@property(nullable, nonatomic, copy) void (^didSelectItemBlock)(__kindof QMUIDialogSelectionViewController *aDialogViewController, NSUInteger itemIndex); +@property(nullable, nonatomic, copy) void (^didDeselectItemBlock)(__kindof QMUIDialogSelectionViewController *aDialogViewController, NSUInteger itemIndex); + +@end + +/** + * 支持单行文本输入的弹窗,可通过`textField.maximumLength`来控制最长可输入的字符,超过则无法继续输入。 + * 可通过`enablesSubmitButtonAutomatically`来自动设置`submitButton.enabled`的状态 + */ +@interface QMUIDialogTextFieldViewController : QMUIDialogViewController + +@property(nullable, nonatomic, strong) UIFont *textFieldLabelFont UI_APPEARANCE_SELECTOR; + +@property(nullable, nonatomic, strong) UIColor *textFieldLabelTextColor UI_APPEARANCE_SELECTOR; + +@property(nullable, nonatomic, strong) UIFont *textFieldFont UI_APPEARANCE_SELECTOR; + +@property(nullable, nonatomic, strong) UIColor *textFieldTextColor UI_APPEARANCE_SELECTOR; + +@property(nullable, nonatomic, strong) UIColor *textFieldSeparatorColor UI_APPEARANCE_SELECTOR; + +/// 输入框上方文字的间距,如果不存在文字则不使用这个间距 +@property(nonatomic, assign) UIEdgeInsets textFieldLabelMargins UI_APPEARANCE_SELECTOR; + +/// 输入框本身的间距,注意输入框内部自带 textInsets,所以可能文字实际的显示位置会比这个间距更往内部一点 +@property(nonatomic, assign) UIEdgeInsets textFieldMargins UI_APPEARANCE_SELECTOR; + +/// 输入框的高度 +@property(nonatomic, assign) CGFloat textFieldHeight UI_APPEARANCE_SELECTOR; + +/// 输入框底部分隔线基于默认布局的偏移,注意分隔线默认的布局为:宽度是输入框宽度减去输入框左右的 textInsets,y 紧贴输入框底部。如果 textFieldSeparatorLayer.hidden = YES 则布局时不考虑这个间距 +@property(nonatomic, assign) UIEdgeInsets textFieldSeparatorInsets UI_APPEARANCE_SELECTOR; + +- (void)addTextFieldWithTitle:(nullable NSString *)textFieldTitle configurationHandler:(void (^ _Nullable)(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer))configurationHandler; + +@property(nullable, nonatomic, copy, readonly) NSArray *textFieldTitleLabels; +@property(nullable, nonatomic, copy, readonly) NSArray *textFields; +@property(nullable, nonatomic, copy, readonly) NSArray *textFieldSeparatorLayers; + +/// 是否应该自动管理输入框的键盘 Return 事件,默认为 YES,YES 表示当点击 Return 按钮时,视为点击了 dialog 的 submit 按钮。你也可以通过 UITextFieldDelegate 自己管理,此时请将此属性置为 NO。 +@property(nonatomic, assign) BOOL shouldManageTextFieldsReturnEventAutomatically; + +/// 是否自动控制提交按钮的enabled状态,默认为YES,则当任一输入框内容为空时禁用提交按钮 +@property(nonatomic, assign) BOOL enablesSubmitButtonAutomatically; + +@property(nullable, nonatomic, copy) BOOL (^shouldEnableSubmitButtonBlock)(__kindof QMUIDialogTextFieldViewController *aDialogViewController); + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.m new file mode 100644 index 00000000..94b2c1dc --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIDialogViewController.m @@ -0,0 +1,963 @@ +/** + * 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. + */ + +// +// QMUIDialogViewController.m +// WeRead +// +// Created by QMUI Team on 16/7/8. +// + +#import "QMUIDialogViewController.h" +#import "QMUICore.h" +#import "QMUIButton.h" +#import "QMUILabel.h" +#import "QMUITextField.h" +#import "QMUITableViewCell.h" +#import "QMUINavigationTitleView.h" +#import "CALayer+QMUI.h" +#import "UITableView+QMUI.h" +#import "NSString+QMUI.h" +#import "UIScrollView+QMUI.h" +#import "QMUIAppearance.h" + +@implementation QMUIDialogViewController (UIAppearance) + ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + 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]; + }); +} + +@end + +@interface QMUIDialogViewController () + +@property(nonatomic, assign) BOOL hasCustomContentView; +@property(nonatomic,copy) void (^cancelButtonBlock)(QMUIDialogViewController *dialogViewController); +@property(nonatomic,copy) void (^submitButtonBlock)(QMUIDialogViewController *dialogViewController); +@end + +@implementation QMUIDialogViewController + +- (void)didInitialize { + [super didInitialize]; + + [self qmui_applyAppearance]; + + _contentView = [[UIView alloc] init]; // 特地不使用setter,从而不要影响self.hasCustomContentView的默认值 + self.contentView.backgroundColor = self.contentViewBackgroundColor; + + _headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, self.headerViewHeight)]; + self.headerView.backgroundColor = self.headerViewBackgroundColor; + + // 使用自带的QMUINavigationTitleView,支持loading、subTitle + [self.headerView addSubview:self.titleView]; + + // 加上分隔线 + _headerViewSeparatorLayer = [CALayer layer]; + [self.headerViewSeparatorLayer qmui_removeDefaultAnimations]; + self.headerViewSeparatorLayer.backgroundColor = self.headerSeparatorColor.CGColor; + [self.headerView.layer addSublayer:self.headerViewSeparatorLayer]; + + _footerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, self.footerViewHeight)]; + self.footerView.backgroundColor = self.footerViewBackgroundColor; + self.footerView.hidden = YES; + + _footerViewSeparatorLayer = [CALayer layer]; + [self.footerViewSeparatorLayer qmui_removeDefaultAnimations]; + self.footerViewSeparatorLayer.backgroundColor = self.footerSeparatorColor.CGColor; + [self.footerView.layer addSublayer:self.footerViewSeparatorLayer]; + + _buttonSeparatorLayer = [CALayer layer]; + [self.buttonSeparatorLayer qmui_removeDefaultAnimations]; + self.buttonSeparatorLayer.backgroundColor = self.footerViewSeparatorLayer.backgroundColor; + self.buttonSeparatorLayer.hidden = YES; + [self.footerView.layer addSublayer:self.buttonSeparatorLayer]; + + self.modalPresentationViewController = [[QMUIModalPresentationViewController alloc] init]; + self.modalPresentationViewController.modal = YES; +} + +- (void)setCornerRadius:(CGFloat)cornerRadius { + _cornerRadius = cornerRadius; + if ([self isViewLoaded]) { + self.view.layer.cornerRadius = cornerRadius; + } +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor { + _backgroundColor = backgroundColor; + if ([self isViewLoaded]) { + self.view.backgroundColor = backgroundColor; + } +} + +- (void)setTitleTintColor:(UIColor *)titleTintColor { + _titleTintColor = titleTintColor; + [self updateTitleViewColor]; +} + +- (void)setTitleLabelFont:(UIFont *)titleLabelFont { + _titleLabelFont = titleLabelFont; + self.titleView.titleLabel.font = titleLabelFont; + self.titleView.verticalTitleFont = titleLabelFont; +} + +- (void)setTitleLabelTextColor:(UIColor *)titleLabelTextColor { + _titleLabelTextColor = titleLabelTextColor; + [self updateTitleViewColor]; +} + +- (void)setSubTitleLabelFont:(UIFont *)subTitleLabelFont { + _subTitleLabelFont = subTitleLabelFont; + self.titleView.subtitleLabel.font = subTitleLabelFont; + self.titleView.verticalSubtitleFont = subTitleLabelFont; +} + +- (void)setSubTitleLabelTextColor:(UIColor *)subTitleLabelTextColor { + _subTitleLabelTextColor = 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 { + _headerSeparatorColor = headerSeparatorColor; + self.headerViewSeparatorLayer.backgroundColor = headerSeparatorColor.CGColor; +} + +- (void)setFooterSeparatorColor:(UIColor *)footerSeparatorColor { + _footerSeparatorColor = footerSeparatorColor; + self.footerViewSeparatorLayer.backgroundColor = footerSeparatorColor.CGColor; + self.buttonSeparatorLayer.backgroundColor = footerSeparatorColor.CGColor; +} + +- (void)setHeaderViewHeight:(CGFloat)headerViewHeight { + _headerViewHeight = headerViewHeight; + [self.modalPresentationViewController updateLayout]; +} + +- (void)setHeaderViewBackgroundColor:(UIColor *)headerViewBackgroundColor { + _headerViewBackgroundColor = headerViewBackgroundColor; + self.headerView.backgroundColor = headerViewBackgroundColor; +} + +- (void)setContentViewMargins:(UIEdgeInsets)contentViewMargins { + _contentViewMargins = contentViewMargins; + [self.modalPresentationViewController updateLayout]; +} + +- (void)setContentViewBackgroundColor:(UIColor *)contentViewBackgroundColor { + _contentViewBackgroundColor = contentViewBackgroundColor; + if (!self.hasCustomContentView) { + self.contentView.backgroundColor = contentViewBackgroundColor; + } +} + +- (void)setFooterViewHeight:(CGFloat)footerViewHeight { + _footerViewHeight = footerViewHeight; +} + +- (void)setFooterViewBackgroundColor:(UIColor *)footerViewBackgroundColor { + _footerViewBackgroundColor = footerViewBackgroundColor; + self.footerView.backgroundColor = footerViewBackgroundColor; +} + +- (void)setButtonTitleAttributes:(NSDictionary *)buttonTitleAttributes { + _buttonTitleAttributes = buttonTitleAttributes; + if (self.cancelButton) { + [self.cancelButton setAttributedTitle:[[NSAttributedString alloc] initWithString:[self.cancelButton attributedTitleForState:UIControlStateNormal].string attributes:buttonTitleAttributes] forState:UIControlStateNormal]; + } + if (self.submitButton) { + [self.submitButton setAttributedTitle:[[NSAttributedString alloc] initWithString:[self.submitButton attributedTitleForState:UIControlStateNormal].string attributes:buttonTitleAttributes] forState:UIControlStateNormal]; + } +} + +- (void)setButtonBackgroundColor:(UIColor *)buttonBackgroundColor { + _buttonBackgroundColor = buttonBackgroundColor; + if (self.cancelButton) { + self.cancelButton.backgroundColor = buttonBackgroundColor; + } + if (self.submitButton) { + self.submitButton.backgroundColor = buttonBackgroundColor; + } +} + +- (void)setButtonHighlightedBackgroundColor:(UIColor *)buttonHighlightedBackgroundColor { + _buttonHighlightedBackgroundColor = buttonHighlightedBackgroundColor; + if (self.cancelButton) { + self.cancelButton.highlightedBackgroundColor = buttonHighlightedBackgroundColor; + } + if (self.submitButton) { + self.submitButton.highlightedBackgroundColor = buttonHighlightedBackgroundColor; + } +} + +BeginIgnoreClangWarning(-Wobjc-missing-super-calls) +- (void)setupNavigationItems { + // 不继承父类的实现,从而避免把 self.titleView 放到 navigationItem 上 + // [super setupNavigationItems]; +} +EndIgnoreClangWarning + +- (void)viewDidLoad { + [super viewDidLoad]; + + // subviews 的初始化都放到 didInitialize 里,以保证初始化完 dialog 就能被外界访问到。但真正加到 self.view 上还是等到 viewDidLoad 时 + [self.view addSubview:self.contentView]; + [self.view addSubview:self.headerView]; + [self.view addSubview:self.footerView]; + + self.view.clipsToBounds = YES; + self.view.backgroundColor = self.backgroundColor; + self.view.layer.cornerRadius = self.cornerRadius; +} + +- (void)setContentView:(UIView *)contentView { + if (_contentView != contentView) { + [_contentView removeFromSuperview]; + _contentView = contentView; + if ([self isViewLoaded]) { + [self.view insertSubview:_contentView atIndex:0]; + } + self.hasCustomContentView = YES; + } else { + self.hasCustomContentView = NO; + } +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + BOOL isFooterViewShowing = self.footerView && !self.footerView.hidden; + + self.headerView.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), self.headerViewHeight); + self.headerViewSeparatorLayer.frame = CGRectFlatMake(0, self.headerViewHeight, CGRectGetWidth(self.view.bounds), PixelOne); + + CGFloat headerViewPaddingHorizontal = 16; + CGFloat headerViewContentWidth = CGRectGetWidth(self.headerView.bounds) - headerViewPaddingHorizontal * 2; + CGSize titleViewSize = [self.titleView sizeThatFits:CGSizeMake(headerViewContentWidth, CGFLOAT_MAX)]; + CGFloat titleViewWidth = MIN(titleViewSize.width, headerViewContentWidth); + self.titleView.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.headerView.bounds), titleViewWidth), CGFloatGetCenter(CGRectGetHeight(self.headerView.bounds), titleViewSize.height), titleViewWidth, titleViewSize.height); + + if (isFooterViewShowing) { + self.footerView.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - self.footerViewHeight, CGRectGetWidth(self.view.bounds), self.footerViewHeight); + self.footerViewSeparatorLayer.frame = CGRectMake(0, -PixelOne, CGRectGetWidth(self.footerView.bounds), PixelOne); + + NSUInteger buttonCount = self.footerView.subviews.count; + if (buttonCount == 1) { + QMUIButton *button = self.cancelButton ? : self.submitButton; + button.frame = self.footerView.bounds; + self.buttonSeparatorLayer.hidden = YES; + } else { + CGFloat buttonWidth = flat(CGRectGetWidth(self.footerView.bounds) / buttonCount); + self.cancelButton.frame = CGRectMake(0, 0, buttonWidth, CGRectGetHeight(self.footerView.bounds)); + self.submitButton.frame = CGRectMake(CGRectGetMaxX(self.cancelButton.frame), 0, CGRectGetWidth(self.footerView.bounds) - CGRectGetMaxX(self.cancelButton.frame), CGRectGetHeight(self.footerView.bounds)); + self.buttonSeparatorLayer.hidden = NO; + self.buttonSeparatorLayer.frame = CGRectMake(CGRectGetMaxX(self.cancelButton.frame), 0, PixelOne, CGRectGetHeight(self.footerView.bounds)); + } + } + + CGFloat contentViewMinY = CGRectGetMaxY(self.headerView.frame) + self.contentViewMargins.top; + CGFloat contentViewHeight = (isFooterViewShowing ? CGRectGetMinY(self.footerView.frame) - self.contentViewMargins.bottom : CGRectGetHeight(self.view.bounds)) - contentViewMinY; + self.contentView.frame = CGRectMake(self.contentViewMargins.left, contentViewMinY, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(self.contentViewMargins), contentViewHeight); +} + +- (void)addCancelButtonWithText:(NSString *)buttonText block:(void (^)(__kindof QMUIDialogViewController *))block { + [self removeCancelButton]; + + _cancelButton = [self generateButtonWithText:buttonText]; + [self.cancelButton addTarget:self action:@selector(handleCancelButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + + self.footerView.hidden = NO; + [self.footerView addSubview:self.cancelButton]; + + self.cancelButtonBlock = block; +} + +- (void)removeCancelButton { + [_cancelButton removeFromSuperview]; + self.cancelButtonBlock = nil; + _cancelButton = nil; + if (!self.cancelButton && !self.submitButton) { + self.footerView.hidden = YES; + } +} + +- (void)addSubmitButtonWithText:(NSString *)buttonText block:(void (^)(__kindof QMUIDialogViewController *dialogViewController))block { + [self removeSubmitButton]; + + _submitButton = [self generateButtonWithText:buttonText]; + [self.submitButton addTarget:self action:@selector(handleSubmitButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + + self.footerView.hidden = NO; + [self.footerView addSubview:self.submitButton]; + + self.submitButtonBlock = block; +} + +- (void)removeSubmitButton { + [_submitButton removeFromSuperview]; + self.submitButtonBlock = nil; + _submitButton = nil; + if (!self.cancelButton && !self.submitButton) { + self.footerView.hidden = YES; + } +} + +- (QMUIButton *)generateButtonWithText:(NSString *)buttonText { + QMUIButton *button = [[QMUIButton alloc] init]; + button.titleLabel.font = UIFontBoldMake((IS_320WIDTH_SCREEN) ? 14 : 15); + button.backgroundColor = self.buttonBackgroundColor; + button.highlightedBackgroundColor = self.buttonHighlightedBackgroundColor; + [button setAttributedTitle:[[NSAttributedString alloc] initWithString:buttonText attributes:self.buttonTitleAttributes] forState:UIControlStateNormal]; + return button; +} + +- (void)handleCancelButtonEvent:(QMUIButton *)cancelButton { + [self hideWithAnimated:YES completion:nil]; + if (self.cancelButtonBlock) { + self.cancelButtonBlock(self); + } +} + +- (void)handleSubmitButtonEvent:(QMUIButton *)submitButton { + if (self.submitButtonBlock) { + // 把自己传过去,通过参数来引用 self,避免在 block 里直接引用 dialog 导致内存泄漏 + self.submitButtonBlock(self); + } +} + +- (void)show { + [self showWithAnimated:YES completion:nil]; +} + +- (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { + self.modalPresentationViewController.contentViewMargins = self.dialogViewMargins; + self.modalPresentationViewController.maximumContentViewWidth = self.maximumContentViewWidth; + self.modalPresentationViewController.contentViewController = self; + [self.modalPresentationViewController showWithAnimated:YES completion:completion]; +} + +- (void)hide { + [self hideWithAnimated:YES completion:nil]; +} + +- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { + [self.modalPresentationViewController hideWithAnimated:animated completion:^(BOOL finished) { + if (completion) { + completion(finished); + } + self.modalPresentationViewController.contentViewController = nil; + }]; +} + +#pragma mark - + +- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller keyboardHeight:(CGFloat)keyboardHeight limitSize:(CGSize)limitSize { + if (!self.hasCustomContentView) { + return limitSize; + } + + BOOL isFooterViewShowing = self.footerView && !self.footerView.hidden; + CGFloat footerHeight = isFooterViewShowing ? self.footerViewHeight : 0; + + CGFloat contentViewVerticalMargin = UIEdgeInsetsGetVerticalValue(self.contentViewMargins); + + CGSize contentViewLimitSize = CGSizeMake(limitSize.width, limitSize.height - self.headerViewHeight - contentViewVerticalMargin - footerHeight); + CGSize contentViewSize = [self.contentView sizeThatFits:contentViewLimitSize]; + + CGSize finalSize = CGSizeMake(MIN(limitSize.width, contentViewSize.width), MIN(limitSize.height, self.headerViewHeight + contentViewSize.height + contentViewVerticalMargin + footerHeight)); + return finalSize; +} + +#pragma mark - + +- (void)hideModalPresentationComponent { + [self hideWithAnimated:NO completion:nil]; +} + +@end + +@implementation QMUIDialogSelectionViewController (UIAppearance) + ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + QMUIDialogSelectionViewController.appearance.rowHeight = TableViewCellNormalHeight; + }); +} + +@end + +const NSInteger QMUIDialogSelectionViewControllerSelectedItemIndexNone = -1; + +@interface QMUIDialogSelectionViewController () + +@property(nonatomic,strong,readwrite) QMUITableView *tableView; +@end + +@implementation QMUIDialogSelectionViewController + +- (void)didInitialize { + [super didInitialize]; + + self.selectedItemIndex = QMUIDialogSelectionViewControllerSelectedItemIndexNone; + self.selectedItemIndexes = [[NSMutableSet alloc] init]; + + self.tableView = [[QMUITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; + self.tableView.dataSource = self; + self.tableView.delegate = self; + self.tableView.alwaysBounceVertical = NO; + self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + + // 因为要根据 tableView sizeThatFits: 算出 dialog 的高度,所以禁用 estimated 特性,不然算出来结果不准确 + self.tableView.estimatedRowHeight = 0; + self.tableView.estimatedSectionHeaderHeight = 0; + self.tableView.estimatedSectionFooterHeight = 0; + + self.contentView = self.tableView; + self.tableView.backgroundColor = self.contentViewBackgroundColor;// QMUIDialogSelectionViewController 使用了 customContentView,所以默认不会自动应用到 self.contentViewBackgroundColor,这里手动应用一次 +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + // 当前的分组不在可视区域内,则滚动到可视区域(只对单选有效) + if (self.selectedItemIndex != QMUIDialogSelectionViewControllerSelectedItemIndexNone && self.selectedItemIndex < self.items.count && ![self.tableView qmui_cellVisibleAtIndexPath:[NSIndexPath indexPathForRow:self.selectedItemIndex inSection:0]]) { + [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.selectedItemIndex inSection:0] atScrollPosition:UITableViewScrollPositionMiddle animated:animated]; + } +} + +- (void)setContentViewBackgroundColor:(UIColor *)contentViewBackgroundColor { + [super setContentViewBackgroundColor:contentViewBackgroundColor]; + self.tableView.backgroundColor = contentViewBackgroundColor; +} + +- (void)setItems:(NSArray *)items { + _items = [items copy]; + [self.tableView reloadData]; + if (self.modalPresentationViewController.visible) { + [self.modalPresentationViewController updateLayout]; + } +} + +- (void)setSelectedItemIndex:(NSInteger)selectedItemIndex { + [self.selectedItemIndexes removeAllObjects]; + _selectedItemIndex = selectedItemIndex; +} + +- (void)setSelectedItemIndexes:(NSMutableSet *)selectedItemIndexes { + self.selectedItemIndex = QMUIDialogSelectionViewControllerSelectedItemIndexNone; + _selectedItemIndexes = selectedItemIndexes; +} + +- (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection { + _allowsMultipleSelection = allowsMultipleSelection; + self.selectedItemIndex = QMUIDialogSelectionViewControllerSelectedItemIndexNone; +} + +- (void)setRowHeight:(CGFloat)rowHeight { + _rowHeight = rowHeight; + [self.tableView setNeedsLayout]; +} + +#pragma mark - + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.items.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *identifier = @"cell"; + QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[QMUITableViewCell alloc] initForTableView:tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]; + cell.backgroundColor = nil;// 使用 tableView 的背景色即可 + } + cell.textLabel.text = self.items[indexPath.row]; + + if (self.allowsMultipleSelection) { + // 多选 + if ([self.selectedItemIndexes containsObject:@(indexPath.row)]) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + } else { + cell.accessoryType = UITableViewCellAccessoryNone; + } + } else { + // 单选 + if (self.selectedItemIndex == indexPath.row) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + } else { + cell.accessoryType = UITableViewCellAccessoryNone; + } + } + + [cell updateCellAppearanceWithIndexPath:indexPath]; + + if (self.cellForItemBlock) { + self.cellForItemBlock(self, cell, indexPath.row); + } + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + if (self.heightForItemBlock) { + return self.heightForItemBlock(self, indexPath.row); + } + return self.rowHeight; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + + // 单选情况下如果重复选中已被选中的cell,则什么都不做 + if (!self.allowsMultipleSelection && self.selectedItemIndex == indexPath.row) { + [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; + return; + } + + // 不允许选中当前cell,直接return + if (self.canSelectItemBlock && !self.canSelectItemBlock(self, indexPath.row)) { + [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; + return; + } + + if (self.allowsMultipleSelection) { + if ([self.selectedItemIndexes containsObject:@(indexPath.row)]) { + // 当前的cell已经被选中,则取消选中 + [self.selectedItemIndexes removeObject:@(indexPath.row)]; + if (self.didDeselectItemBlock) { + self.didDeselectItemBlock(self, indexPath.row); + } + } else { + [self.selectedItemIndexes addObject:@(indexPath.row)]; + if (self.didSelectItemBlock) { + self.didSelectItemBlock(self, indexPath.row); + } + } + if ([tableView qmui_cellVisibleAtIndexPath:indexPath]) { + [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; + } + } else { + BOOL isSelectedIndexPathBeforeVisible = NO; + + // 选中新的cell时,先反选之前被选中的那个cell + NSIndexPath *selectedIndexPathBefore = nil; + if (self.selectedItemIndex != QMUIDialogSelectionViewControllerSelectedItemIndexNone) { + selectedIndexPathBefore = [NSIndexPath indexPathForRow:self.selectedItemIndex inSection:0]; + if (self.didDeselectItemBlock) { + self.didDeselectItemBlock(self, selectedIndexPathBefore.row); + } + isSelectedIndexPathBeforeVisible = [tableView qmui_cellVisibleAtIndexPath:selectedIndexPathBefore]; + } + + self.selectedItemIndex = indexPath.row; + + // 如果之前被选中的那个cell也在可视区域里,则也要用动画去刷新它,否则只需要用动画刷新当前已选中的cell即可,之前被选中的那个交给cellForRow去刷新 + if (isSelectedIndexPathBeforeVisible) { + [tableView reloadRowsAtIndexPaths:@[selectedIndexPathBefore, indexPath] withRowAnimation:UITableViewRowAnimationFade]; + } else { + [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; + } + + if (self.didSelectItemBlock) { + self.didSelectItemBlock(self, indexPath.row); + } + } +} + +@end + +@implementation QMUIDialogTextFieldViewController (UIAppearance) + ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + 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); + }); +} + +@end + +@interface QMUIDialogTextFieldViewController () + +@property(nonatomic, strong) UIScrollView *scrollView; +@property(nonatomic, strong) NSMutableArray *mutableTitleLabels; +@property(nonatomic, strong) NSMutableArray *mutableTextFields; +@property(nonatomic, strong) NSMutableArray *mutableSeparatorLayers; +@end + +@implementation QMUIDialogTextFieldViewController + +- (void)didInitialize { + [super didInitialize]; + + self.mutableTitleLabels = [[NSMutableArray alloc] init]; + self.mutableTextFields = [[NSMutableArray alloc] init]; + self.mutableSeparatorLayers = [[NSMutableArray alloc] init]; + + self.shouldManageTextFieldsReturnEventAutomatically = YES; + self.enablesSubmitButtonAutomatically = YES; + + self.scrollView = [[UIScrollView alloc] init]; + self.scrollView.scrollsToTop = NO; + self.scrollView.clipsToBounds = YES; + self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + self.contentView = self.scrollView; + self.scrollView.backgroundColor = self.contentViewBackgroundColor; +} + +- (void)addTextFieldWithTitle:(NSString *)textFieldTitle configurationHandler:(void (^)(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer))configurationHandler { + QMUILabel *label = [self generateTextFieldTitleLabel]; + label.text = textFieldTitle; + if (textFieldTitle.length <= 0) { + label.hidden = YES; + } + [self.mutableTitleLabels addObject:label]; + + QMUITextField *textField = [self generateTextField]; + [self.mutableTextFields addObject:textField]; + + CALayer *separatorLayer = [self generateTextFieldSeparatorLayer]; + [self.mutableSeparatorLayers addObject:separatorLayer]; + + if (configurationHandler) { + configurationHandler(label, textField, separatorLayer); + } +} + +- (QMUILabel *)generateTextFieldTitleLabel { + QMUILabel *textFieldLabel = [[QMUILabel alloc] init]; + textFieldLabel.font = self.textFieldLabelFont; + textFieldLabel.textColor = self.textFieldLabelTextColor; + [self.contentView addSubview:textFieldLabel]; + return textFieldLabel; +} + +- (QMUITextField *)generateTextField { + QMUITextField *textField = [[QMUITextField alloc] init]; + textField.delegate = self; + textField.font = self.textFieldFont; + textField.textColor = self.textFieldTextColor; + textField.backgroundColor = nil; + textField.returnKeyType = UIReturnKeyNext; + textField.clearButtonMode = UITextFieldViewModeWhileEditing; + textField.enablesReturnKeyAutomatically = self.enablesSubmitButtonAutomatically; + [textField addTarget:self action:@selector(handleTextFieldTextDidChangeEvent:) forControlEvents:UIControlEventEditingChanged]; + [self.contentView addSubview:textField]; + return textField; +} + +- (CALayer *)generateTextFieldSeparatorLayer { + CALayer *textFieldSeparatorLayer = [CALayer qmui_separatorLayer]; + textFieldSeparatorLayer.backgroundColor = self.textFieldSeparatorColor.CGColor; + [self.contentView.layer addSublayer:textFieldSeparatorLayer]; + return textFieldSeparatorLayer; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + // 全部基于 contentView 布局即可 + + QMUIAssert(self.mutableTitleLabels.count == self.mutableTextFields.count && self.mutableTextFields.count == self.mutableSeparatorLayers.count, NSStringFromClass(self.class), @"标题、输入框、分隔线的数量不匹配"); + + CGFloat minY = 0; + + for (NSInteger i = 0; i < self.mutableTitleLabels.count; i++) { + QMUILabel *label = self.mutableTitleLabels[i]; + QMUITextField *textField = self.mutableTextFields[i]; + CALayer *separatorLayer = self.mutableSeparatorLayers[i]; + + if (!label.hidden) { + [label sizeToFit]; + label.frame = CGRectFlatMake(self.textFieldLabelMargins.left, minY + self.textFieldLabelMargins.top, CGRectGetWidth(self.contentView.bounds) - UIEdgeInsetsGetHorizontalValue(self.textFieldLabelMargins), CGRectGetHeight(label.frame)); + minY = CGRectGetMaxY(label.frame) + self.textFieldLabelMargins.bottom; + } + + textField.frame = CGRectFlatMake(self.textFieldMargins.left, minY + self.textFieldMargins.top, CGRectGetWidth(self.contentView.bounds) - UIEdgeInsetsGetHorizontalValue(self.textFieldMargins), self.textFieldHeight); + minY = CGRectGetMaxY(textField.frame) + self.textFieldMargins.bottom; + + // 宽度基于 textField 的宽度减去 textField.textInsets,从而保证与文字对齐 + if (!separatorLayer.hidden) { + CGFloat separatorMinX = CGRectGetMinX(textField.frame) + textField.textInsets.left + self.textFieldSeparatorInsets.left; + CGFloat separatorWidth = CGRectGetWidth(textField.frame) - UIEdgeInsetsGetHorizontalValue(textField.textInsets) - UIEdgeInsetsGetHorizontalValue(self.textFieldSeparatorInsets); + separatorLayer.frame = CGRectMake(separatorMinX, minY + self.textFieldSeparatorInsets.top, separatorWidth, PixelOne); + minY = CGRectGetMinY(separatorLayer.frame) + self.textFieldSeparatorInsets.bottom;// 用 minY 是因为分隔线高度不占位 + } + } + + self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.scrollView.bounds), minY); +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + [self.mutableTextFields.firstObject becomeFirstResponder]; + + if (self.enablesSubmitButtonAutomatically) { + // 触发所有输入框的 enablesReturnKeyAutomatically 属性的更新 + self.enablesSubmitButtonAutomatically = self.enablesSubmitButtonAutomatically; + } + + // 最后一个输入框默认是 Done,其他输入框都是 Next + self.mutableTextFields.lastObject.returnKeyType = UIReturnKeyDone; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + [self.view endEditing:YES]; +} + +#pragma mark - Getters & Setters + +- (void)setContentViewBackgroundColor:(UIColor *)contentViewBackgroundColor { + [super setContentViewBackgroundColor:contentViewBackgroundColor]; + self.scrollView.backgroundColor = contentViewBackgroundColor; +} + +- (void)setTextFieldLabelFont:(UIFont *)textFieldLabelFont { + _textFieldLabelFont = textFieldLabelFont; + [self.mutableTitleLabels enumerateObjectsUsingBlock:^(QMUILabel * _Nonnull label, NSUInteger idx, BOOL * _Nonnull stop) { + label.font = textFieldLabelFont; + }]; + if (self.mutableTitleLabels.count) { + [self.modalPresentationViewController updateLayout]; + } +} + +- (void)setTextFieldLabelTextColor:(UIColor *)textFieldLabelTextColor { + _textFieldLabelTextColor = textFieldLabelTextColor; + [self.mutableTitleLabels enumerateObjectsUsingBlock:^(QMUILabel * _Nonnull label, NSUInteger idx, BOOL * _Nonnull stop) { + label.textColor = textFieldLabelTextColor; + }]; +} + +- (void)setTextFieldFont:(UIFont *)textFieldFont { + _textFieldFont = textFieldFont; + [self.mutableTextFields enumerateObjectsUsingBlock:^(QMUITextField * _Nonnull textField, NSUInteger idx, BOOL * _Nonnull stop) { + textField.font = textFieldFont; + }]; +} + +- (void)setTextFieldTextColor:(UIColor *)textFieldTextColor { + _textFieldTextColor = textFieldTextColor; + [self.mutableTextFields enumerateObjectsUsingBlock:^(QMUITextField * _Nonnull textField, NSUInteger idx, BOOL * _Nonnull stop) { + textField.textColor = textFieldTextColor; + }]; +} + +- (void)setTextFieldSeparatorColor:(UIColor *)textFieldSeparatorColor { + _textFieldSeparatorColor = textFieldSeparatorColor; + [self.mutableSeparatorLayers enumerateObjectsUsingBlock:^(CALayer * _Nonnull layer, NSUInteger idx, BOOL * _Nonnull stop) { + layer.backgroundColor = textFieldSeparatorColor.CGColor; + }]; +} + +- (void)setTextFieldLabelMargins:(UIEdgeInsets)textFieldLabelMargins { + _textFieldLabelMargins = textFieldLabelMargins; + if (self.mutableTitleLabels.count) { + [self.modalPresentationViewController updateLayout]; + } +} + +- (void)setTextFieldMargins:(UIEdgeInsets)textFieldMargins { + _textFieldMargins = textFieldMargins; + if (self.textFields.count) { + [self.modalPresentationViewController updateLayout]; + } +} + +- (void)setTextFieldHeight:(CGFloat)textFieldHeight { + _textFieldHeight = textFieldHeight; + if (self.textFields.count) { + [self.modalPresentationViewController updateLayout]; + } +} + +- (void)setTextFieldSeparatorInsets:(UIEdgeInsets)textFieldSeparatorInsets { + _textFieldSeparatorInsets = textFieldSeparatorInsets; + if (self.mutableSeparatorLayers.count) { + [self.modalPresentationViewController updateLayout]; + } +} + +- (NSArray *)textFieldTitleLabels { + return self.mutableTitleLabels.copy; +} + +- (NSArray *)textFields { + return self.mutableTextFields.copy; +} + +- (NSArray *)textFieldSeparatorLayers { + return self.mutableSeparatorLayers.copy; +} + +#pragma mark - Submit Button Enables + +- (void)setEnablesSubmitButtonAutomatically:(BOOL)enablesSubmitButtonAutomatically { + _enablesSubmitButtonAutomatically = enablesSubmitButtonAutomatically; + [self.mutableTextFields enumerateObjectsUsingBlock:^(QMUITextField * _Nonnull textField, NSUInteger idx, BOOL * _Nonnull stop) { + // enablesSubmitButtonAutomatically 只对最后一个输入框生效 + if (enablesSubmitButtonAutomatically && idx != self.mutableTextFields.count - 1) { + textField.enablesReturnKeyAutomatically = NO; + } else { + textField.enablesReturnKeyAutomatically = enablesSubmitButtonAutomatically; + } + }]; + if (enablesSubmitButtonAutomatically) { + [self updateSubmitButtonEnables]; + } +} + +- (void)updateSubmitButtonEnables { + self.submitButton.enabled = [self shouldEnabledSubmitButton]; +} + +- (BOOL)shouldEnabledSubmitButton { + if (self.shouldEnableSubmitButtonBlock) { + return self.shouldEnableSubmitButtonBlock(self); + } + + if (self.enablesSubmitButtonAutomatically) { + __block BOOL enabled = NO; + [self.mutableTextFields enumerateObjectsUsingBlock:^(QMUITextField * _Nonnull textField, NSUInteger idx, BOOL * _Nonnull stop) { + NSInteger textLength = textField.text.qmui_trim.length; + enabled = 0 < textLength && textLength <= textField.maximumTextLength; + if (!enabled) { + *stop = YES; + } + }]; + return enabled; + } + + return YES; +} + +- (void)handleTextFieldTextDidChangeEvent:(QMUITextField *)textField { + if ([self.mutableTextFields containsObject:textField]) { + [self updateSubmitButtonEnables]; + } +} + +- (void)addSubmitButtonWithText:(NSString *)buttonText block:(void (^)(__kindof QMUIDialogViewController *dialogViewController))block { + [super addSubmitButtonWithText:buttonText block:block]; + [self updateSubmitButtonEnables]; +} + +#pragma mark - + +- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller keyboardHeight:(CGFloat)keyboardHeight limitSize:(CGSize)limitSize { + + CGFloat textFieldLabelHeight = 0; + for (QMUILabel *label in self.mutableTitleLabels) { + if (!label.hidden) { + CGFloat labelHeight = flat([label sizeThatFits:CGSizeMax].height); + textFieldLabelHeight += labelHeight + UIEdgeInsetsGetVerticalValue(self.textFieldLabelMargins); + } + } + + CGFloat textFieldHeight = self.mutableTextFields.count * (self.textFieldHeight + UIEdgeInsetsGetVerticalValue(self.textFieldMargins)); + + CGFloat separatorHeight = 0; + for (CALayer *separatorLayer in self.mutableSeparatorLayers) { + if (!separatorLayer.hidden) { + separatorHeight += UIEdgeInsetsGetVerticalValue(self.textFieldSeparatorInsets); + } + } + + CGFloat contentHeight = textFieldLabelHeight + textFieldHeight + separatorHeight + UIEdgeInsetsGetVerticalValue(self.scrollView.adjustedContentInset); + CGFloat contentViewVerticalMargin = UIEdgeInsetsGetVerticalValue(self.contentViewMargins); + + BOOL isFooterViewShowing = self.footerView && !self.footerView.hidden; + CGFloat footerHeight = isFooterViewShowing ? self.footerViewHeight : 0; + + CGSize finalSize = CGSizeMake(limitSize.width, MIN(limitSize.height, self.headerViewHeight + contentHeight + contentViewVerticalMargin + footerHeight)); + return finalSize; +} + +#pragma mark - + +- (BOOL)textFieldShouldReturn:(QMUITextField *)textField { + if (!self.shouldManageTextFieldsReturnEventAutomatically) { + return NO; + } + + if (![self.mutableTextFields containsObject:textField]) { + return NO; + } + + if (self.mutableTextFields.count > 1) { + if (textField != self.mutableTextFields.lastObject && textField.returnKeyType == UIReturnKeyNext) { + NSUInteger index = [self.mutableTextFields indexOfObject:textField]; + [self.mutableTextFields[index + 1] becomeFirstResponder]; + return YES; + } + } + + // 有 submitButton 则响应它,没有的话响应 cancel,再没有就降下键盘即可(体验与 UIAlertController 一致) + + if (self.submitButton.enabled) { + [self.submitButton sendActionsForControlEvents:UIControlEventTouchUpInside]; + return NO; + } + + return NO; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.h b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.h new file mode 100644 index 00000000..f9e3b0ae --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.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. + */ + +// +// QMUIEmotionInputManager.h +// qmui +// +// Created by QMUI Team on 16/9/8. +// + +#import +#import + +@class QMUIEmotionView; + +/** + * 提供一个常见的通用表情面板,能为绑定的`UITextField`或`UITextView`提供表情的相关功能,包括点击表情输入对应的表情名字、点击删除按钮删除表情。 + * 使用方式: + * 1. 使用 init 方法初始化。 + * 2. 通过 `boundTextField` 或 `boundTextView` 关联一个输入框,建议这些输入框使用 `QMUITextField` 或 `QMUITextView`,原因看下面的 warning。 + * 3. 将所有表情通过 `self.emotionView.emotions` 设置进去,注意这个数组里的所有 `QMUIEmotion` 的 `displayName` 都应该使用左右标识符包裹起来(例如中括号“[]”),并且所有表情的左右标识符都应该保持一致。 + * 4. 将 `self.emotionView` add 到界面上即可。 + * + * @warning 一个`QMUIEmotionInputManager`无法同时绑定`boundTextField`和`boundTextView`,在两者都绑定的情况下,优先使用`boundTextField`。 + * @warning 由于`QMUIEmotionInputManager`里面多个地方会调用`boundTextView.text`,而`setText:`并不会触发`UITextViewDelegate`的`textViewDidChange:`或`UITextViewTextDidChangeNotification`,以及 `UITextField` 的 `UIControlEventEditingChanged` 事件,从而在刷新表情面板里的发送按钮的enabled状态时可能不及时,所以推荐使用 `QMUITextView` 代替 `UITextView`、用 `QMUITextField` 代替 `UITextField`,并确保它们的`shouldResponseToProgrammaticallyTextChanges`属性是 `YES`(默认即为 `YES`)。 + * @warning 由于表情的插入、删除都会受当前输入框的光标所在位置的影响,所以请在适当的时机更新`selectedRangeForBoundTextInput`的值,具体情况请查看该属性的注释。 + */ +@interface QMUIEmotionInputManager : NSObject + +/// 要绑定的 UITextField +@property(nonatomic, weak) UITextField *boundTextField; + +/// 要绑定的 UITextView +@property(nonatomic, weak) UITextView *boundTextView; + +/** + * `selectedRangeForBoundTextInput`决定了表情将会被插入(删除)的位置,因此使用控件的时候需要及时更新它。 + * + * 通常用到的更新时机包括: + * - 降下键盘显示表情面板之前(调用resignFirstResponder、endEditing:之前) + * - 的`textViewDidChangeSelection:`回调里 + * - 输入框里的文字发生变化时,例如点了发送按钮后输入框文字会被清空,此时要重置`selectedRangeForBoundTextInput`为0 + */ +@property(nonatomic, assign) NSRange selectedRangeForBoundTextInput; + +/** + * 表情面板,已被设置了默认的`didSelectEmotionBlock`和`didSelectDeleteButtonBlock`,在`QMUIEmotionInputManager`初始化完后,即可将`emotionView`添加到界面上。 + */ +@property(nonatomic, strong, readonly) QMUIEmotionView *emotionView; + +/** + * 将当前光标所在位置的表情删除,在调用前请注意更新`selectedRangeForBoundTextInput` + * @param forceDelete 当没有删除掉表情的情况下(可能光标前面并不是一个表情字符),要不要强制删掉光标前的字符。YES表示强制删掉,NO表示不删,交给系统键盘处理 + * @return 表示是否成功删除了文字(如果并不是删除表情,而是删除普通字符,也是返回YES) + */ +- (BOOL)deleteEmotionDisplayNameAtCurrentSelectedRangeForce:(BOOL)forceDelete; + +/** + * 在 `UITextViewDelegate` 的 `textView:shouldChangeTextInRange:replacementText:` 或者 `QMUITextFieldDelegate` 的 `textField:shouldChangeTextInRange:replacementText:` 方法里调用,根据返回值来决定是否应该调用 `deleteEmotionDisplayNameAtCurrentSelectedRangeForce:` + + @param range 要发生变化的文字所在的range + @param text 要被替换为的文字 + + @return 是否会接管键盘的删除按钮事件,`YES` 表示接管,可调用 `deleteEmotionDisplayNameAtCurrentSelectedRangeForce:` 方法,`NO` 表示不可接管,应该使用系统自身的删除事件响应。 + */ +- (BOOL)shouldTakeOverControlDeleteKeyWithChangeTextInRange:(NSRange)range replacementText:(NSString *)text; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m new file mode 100644 index 00000000..b594a698 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionInputManager.m @@ -0,0 +1,142 @@ +/** + * 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. + */ + +// +// QMUIEmotionInputManager.m +// qmui +// +// Created by QMUI Team on 16/9/8. +// + +#import "QMUIEmotionInputManager.h" +#import "QMUICore.h" +#import "NSString+QMUI.h" +#import "QMUIEmotionView.h" + +@protocol QMUIEmotionInputViewProtocol + +@property(nonatomic, copy) NSString *text; +@property(nonatomic, assign, readonly) NSRange selectedRange; +@end + +@implementation QMUIEmotionInputManager + +- (instancetype)init { + self = [super init]; + if (self) { + _emotionView = [[QMUIEmotionView alloc] init]; + __weak QMUIEmotionInputManager *weakSelf = self; + self.emotionView.didSelectEmotionBlock = ^(NSInteger index, QMUIEmotion *emotion) { + if (!weakSelf.boundInputView) return; + + NSString *inputText = weakSelf.boundInputView.text; + // 用一个局部变量先保存selectedRangeForBoundTextInput的值,是为了避免在接下来这段代码执行的过程中,外部可能修改了self.selectedRangeForBoundTextInput的值,导致计算错误 + NSRange selectedRange = weakSelf.selectedRangeForBoundTextInput; + if (selectedRange.location <= inputText.length) { + // 在输入框文字的中间插入表情 + NSMutableString *mutableText = [NSMutableString stringWithString:inputText ?: @""]; + [mutableText insertString:emotion.displayName atIndex:selectedRange.location]; + weakSelf.boundInputView.text = mutableText;// UITextView setText:会触发textViewDidChangeSelection:,而如果在这个delegate里更新self.selectedRangeForBoundTextInput,就会导致计算错误 + selectedRange = NSMakeRange(selectedRange.location + emotion.displayName.length, 0); + } else { + // 在输入框文字的结尾插入表情 + inputText = [inputText stringByAppendingString:emotion.displayName]; + weakSelf.boundInputView.text = inputText; + selectedRange = NSMakeRange(weakSelf.boundInputView.text.length, 0);// 始终都应该从 boundInputView.text 获取最终的文字,因为可能在 setText: 时受 maximumTextLength 的限制导致文字截断 + } + weakSelf.selectedRangeForBoundTextInput = selectedRange; + }; + self.emotionView.didSelectDeleteButtonBlock = ^{ + [weakSelf deleteEmotionDisplayNameAtCurrentSelectedRangeForce:YES]; + }; + } + return self; +} + +- (UIView *)boundInputView { + if (self.boundTextField) { + return (UIView *)self.boundTextField; + } else if (self.boundTextView) { + return (UIView *)self.boundTextView; + } + return nil; +} + +- (BOOL)deleteEmotionDisplayNameAtCurrentSelectedRangeForce:(BOOL)forceDelete { + if (!self.boundInputView) return NO; + + NSRange selectedRange = self.selectedRangeForBoundTextInput; + NSString *text = self.boundInputView.text; + + // 没有文字或者光标位置前面没文字 + if (!text.length || NSMaxRange(selectedRange) == 0) { + return NO; + } + + BOOL hasDeleteEmotionDisplayNameSuccess = NO; + NSString *exampleEmotionDisplayName = self.emotionView.emotions.firstObject.displayName; + NSString *emotionDisplayNameLeftSign = exampleEmotionDisplayName ? [exampleEmotionDisplayName substringWithRange:NSMakeRange(0, 1)] : nil; + NSString *emotionDisplayNameRightSign = exampleEmotionDisplayName ? [exampleEmotionDisplayName substringWithRange:NSMakeRange(exampleEmotionDisplayName.length - 1, 1)] : nil; + NSInteger emotionDisplayNameMinimumLength = 3;// 表情里的最短displayName的长度,也即“[x]” + NSInteger lengthForStringBeforeSelectedRange = selectedRange.location; + NSString *lastCharacterBeforeSelectedRange = [text substringWithRange:NSMakeRange(selectedRange.location - 1, 1)]; + if ([lastCharacterBeforeSelectedRange isEqualToString:emotionDisplayNameRightSign] && lengthForStringBeforeSelectedRange >= emotionDisplayNameMinimumLength) { + NSInteger beginIndex = lengthForStringBeforeSelectedRange - (emotionDisplayNameMinimumLength - 1);// 从"]"之前的第n个字符开始查找 + NSInteger endIndex = MAX(0, lengthForStringBeforeSelectedRange - 5);// 直到"]"之前的第n个字符结束查找,这里写5只是简单的限定,这个数字只要比所有表情的displayName长度长就行了 + for (NSInteger i = beginIndex; i >= endIndex; i --) { + NSString *checkingCharacter = [text substringWithRange:NSMakeRange(i, 1)]; + if ([checkingCharacter isEqualToString:emotionDisplayNameRightSign]) { + // 查找过程中还没遇到"["就已经遇到"]"了,说明是非法的表情字符串,所以直接终止 + break; + } + + if ([checkingCharacter isEqualToString:emotionDisplayNameLeftSign]) { + NSRange deletingDisplayNameRange = NSMakeRange(i, lengthForStringBeforeSelectedRange - i); + self.boundInputView.text = [text stringByReplacingCharactersInRange:deletingDisplayNameRange withString:@""]; + self.selectedRangeForBoundTextInput = NSMakeRange(deletingDisplayNameRange.location, 0); + hasDeleteEmotionDisplayNameSuccess = YES; + break; + } + } + } + + if (hasDeleteEmotionDisplayNameSuccess) { + return YES; + } + + if (forceDelete) { + if (NSMaxRange(selectedRange) <= text.length) { + if (selectedRange.length > 0) { + // 如果选中区域是一段文字,则删掉这段文字 + self.boundInputView.text = [text stringByReplacingCharactersInRange:selectedRange withString:@""]; + self.selectedRangeForBoundTextInput = NSMakeRange(selectedRange.location, 0); + } else if (selectedRange.location > 0) { + // 如果并没有选中一段文字,则删掉光标前一个字符 + NSString *textAfterDelete = [text qmui_stringByRemoveCharacterAtIndex:selectedRange.location - 1]; + self.boundInputView.text = textAfterDelete; + self.selectedRangeForBoundTextInput = NSMakeRange(selectedRange.location - (text.length - textAfterDelete.length), 0); + } + } else { + // 选中区域超过文字长度了,非法数据,则直接删掉最后一个字符 + self.boundInputView.text = [text qmui_stringByRemoveLastCharacter]; + self.selectedRangeForBoundTextInput = NSMakeRange(self.boundInputView.text.length, 0); + } + + return YES; + } + + return NO; +} + +- (BOOL)shouldTakeOverControlDeleteKeyWithChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + BOOL isDeleteKeyPressed = text.length == 0 && self.boundInputView.text.length - 1 == range.location; + BOOL hasMarkedText = !!self.boundInputView.markedTextRange; + return isDeleteKeyPressed && !hasMarkedText; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.h b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.h new file mode 100644 index 00000000..7bf3e296 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.h @@ -0,0 +1,141 @@ +/** + * 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. + */ + +// +// QMUIEmotionView.h +// qmui +// +// Created by QMUI Team on 16/9/6. +// + +#import + +@class QMUIButton; + +/** + * 代表一个表情的数据对象 + */ +@interface QMUIEmotion : NSObject + +/// 当前表情的标识符,可用于区分不同表情 +@property(nonatomic, copy) NSString *identifier; + +/// 当前表情展示出来的名字,可用于输入框里的占位文字,请务必使用统一的左右标识符将表情名称包裹起来(例如常见的“[]”),否则在 `QMUIEmotionInputManager` 里会因为找不到标识符而无法准确识别出一串文本里的哪些字符是代表一个表情。合法的 displayName 例子:“[委屈]” +@property(nonatomic, copy) NSString *displayName; + +/// 表情对应的图片。若表情图片存放于项目内,则建议用当前表情的`identifier`作为图片名 +@property(nonatomic, strong) UIImage *image; + +/** + * 快速生成一个`QMUIEmotion`对象,并且以`identifier`为图片名在当前项目里查找,作为表情的图片 + * @param identifier 表情的标识符,也会被当成图片的名字 + * @param displayName 表情展示出来的名字 + */ ++ (instancetype)emotionWithIdentifier:(NSString *)identifier displayName:(NSString *)displayName; + +@end + + + + +/** + * 表情控件,支持任意表情的展示,每个表情以相同的大小显示。 + * + * 使用方式: + * + * - 通过`initWithFrame:`初始化,如果面板高度不变,建议在init时就设置好,若最终布局以父类的`layoutSubviews`为准,则也可通过`init`方法初始化,再在`layoutSubviews`里计算布局 + * - 通过调整`paddingInPage`、`emotionSize`等变量来自定义UI + * - 通过`emotions`设置要展示的表情 + * - 通过`didSelectEmotionBlock`设置选中表情时的回调,通过`didSelectDeleteButtonBlock`来响应面板内的删除按钮 + * - 为`sendButton`添加`addTarget:action:forState:`事件,从而触发发送逻辑 + * + * 本控件支持通过`UIAppearance`设置全局的默认样式。若要修改控件内的`UIPageControl`的样式,可通过`[UIPageControl appearanceWhenContainedInInstancesOfClasses:@[[QMUIEmotionView class]]]`的方式来修改。 + */ +@interface QMUIEmotionView : UIView + +/// 要展示的所有表情 +@property(nonatomic, copy) NSArray *emotions; + +/** + * 选中表情时的回调 + * @argv index 被选中的表情在`emotions`里的索引 + * @argv emotion 被选中的表情对应的`QMUIEmotion`对象 + * @see QMUIEmotion + */ +@property(nonatomic, copy) void (^didSelectEmotionBlock)(NSInteger index, QMUIEmotion *emotion); + +/// 删除按钮的点击事件回调 +@property(nonatomic, copy) void (^didSelectDeleteButtonBlock)(void); + +/// 用于展示表情面板的横向滚动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; + +/// 控件底部的分页控件,可点击切换表情页面 +@property(nonatomic, strong, readonly) UIPageControl *pageControl; + +/// 控件右下角的发送按钮 +@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; + +/// 每一页表情允许的最大行数,默认为4 +@property(nonatomic, assign) NSInteger numberOfRowsPerPage UI_APPEARANCE_SELECTOR; + +/// 表情的图片大小,不管`QMUIEmotion.image.size`多大,都会被缩放到`emotionSize`里显示,默认为{30, 30} +@property(nonatomic, assign) CGSize emotionSize UI_APPEARANCE_SELECTOR; + +/// 表情点击时的背景遮罩相对于`emotionSize`往外拓展的区域,负值表示遮罩比表情还大,正值表示遮罩比表情还小,默认为{-3, -3, -3, -3} +@property(nonatomic, assign) UIEdgeInsets emotionSelectedBackgroundExtension UI_APPEARANCE_SELECTOR; + +/// 表情与表情之间的最小水平间距,默认为10 +@property(nonatomic, assign) CGFloat minimumEmotionHorizontalSpacing UI_APPEARANCE_SELECTOR; + +/// 表情面板右下角的删除按钮的图片,默认为`[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; + +/// 发送按钮的背景色,默认为`UIColorBlue` +@property(nonatomic, strong) UIColor *sendButtonBackgroundColor UI_APPEARANCE_SELECTOR; + +/// 发送按钮的圆角大小,默认为4 +@property(nonatomic, assign) CGFloat sendButtonCornerRadius UI_APPEARANCE_SELECTOR; + +/// 发送按钮布局时的外边距,相对于控件右下角。仅right/bottom有效,默认为{0, 0, 16, 16} +@property(nonatomic, assign) UIEdgeInsets sendButtonMargins UI_APPEARANCE_SELECTOR; + +/// 分页控件距离底部的间距,默认为22 +@property(nonatomic, assign) CGFloat pageControlMarginBottom UI_APPEARANCE_SELECTOR; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.m b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.m new file mode 100644 index 00000000..e73bfd7b --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmotionView.m @@ -0,0 +1,640 @@ +/** + * 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. + */ + +// +// QMUIEmotionView.m +// qmui +// +// Created by QMUI Team on 16/9/6. +// + +#import "QMUIEmotionView.h" +#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" +#import "QMUILog.h" + +@implementation QMUIEmotion + ++ (instancetype)emotionWithIdentifier:(NSString *)identifier displayName:(NSString *)displayName { + 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]; +} + +@end + +@class QMUIEmotionPageView; + +@protocol QMUIEmotionPageViewDelegate + +@optional +- (void)emotionPageView:(QMUIEmotionPageView *)emotionPageView didSelectEmotion:(QMUIEmotion *)emotion atIndex:(NSInteger)index; +- (void)emotionPageViewDidLayoutEmotions:(QMUIEmotionPageView *)emotionPageView; +@end + +/// 表情面板每一页的cell,在drawRect里将所有表情绘制上去,同时自带一个末尾的删除按钮 +@interface QMUIEmotionPageView : UICollectionViewCell + +@property(nonatomic, weak) QMUIEmotionView *delegate; + +/// 表情被点击时盖在表情上方用于表示选中的遮罩 +@property(nonatomic, strong) UIView *emotionSelectedBackgroundView; + +/// 表情面板右下角的删除按钮 +@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; + +/// 记录当前pageView里所有表情的可点击区域的rect,在drawRect:里更新,在tap事件里使用 +@property(nonatomic, strong) NSMutableArray *emotionHittingRects; + +/// 负责实现表情的点击 +@property(nonatomic, strong) UITapGestureRecognizer *tapGestureRecognizer; + +/// 整个pageView内部的padding +@property(nonatomic, assign) UIEdgeInsets padding; + +/// 每个pageView能展示表情的行数 +@property(nonatomic, assign) NSInteger numberOfRows; + +/// 每个表情的绘制区域大小,表情图片最终会以UIViewContentModeScaleAspectFit的方式撑满这个大小。表情计算布局时也是基于这个大小来算的。 +@property(nonatomic, assign) CGSize emotionSize; + +/// 点击表情时出现的遮罩要在表情所在的矩形位置拓展多少空间,负值表示遮罩比emotionSize更大,正值表示遮罩比emotionSize更小。最终判断表情点击区域时也是以拓展后的区域来判定的 +@property(nonatomic, assign) UIEdgeInsets emotionSelectedBackgroundExtension; + +/// 表情与表情之间的水平间距的最小值,实际值可能比这个要大一点(pageView会把剩余空间分配到表情的水平间距里) +@property(nonatomic, assign) CGFloat minimumEmotionHorizontalSpacing; + +/// debug模式会把表情的绘制矩形显示出来 +@property(nonatomic, assign) BOOL debug; + +@property(nonatomic, assign, readonly) BOOL needsLayoutEmotions; + +@property(nonatomic, assign) CGRect previousLayoutFrame; + +@end + +@implementation QMUIEmotionPageView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.backgroundColor = UIColorClear; + + self.emotionSelectedBackgroundView = [[UIView alloc] init]; + self.emotionSelectedBackgroundView.userInteractionEnabled = NO; + self.emotionSelectedBackgroundView.backgroundColor = UIColorMakeWithRGBA(0, 0, 0, .16); + self.emotionSelectedBackgroundView.layer.cornerRadius = 3; + self.emotionSelectedBackgroundView.alpha = 0; + [self addSubview:self.emotionSelectedBackgroundView]; + + self.emotionHittingRects = [[NSMutableArray alloc] init]; + self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGestureRecognizer:)]; + [self addGestureRecognizer:self.tapGestureRecognizer]; + } + 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]; + + 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)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; + 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; + 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]]; + emotionlayer.frame = emotionRect; + emotionlayer.hidden = NO; + } + + 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]; + } +} + + +- (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:^{ + self.emotionSelectedBackgroundView.alpha = 1; + } completion:^(BOOL finished) { + [UIView animateWithDuration:.08 animations:^{ + self.emotionSelectedBackgroundView.alpha = 0; + } completion:nil]; + }]; + if ([self.delegate respondsToSelector:@selector(emotionPageView:didSelectEmotion:atIndex:)]) { + [self.delegate emotionPageView:self didSelectEmotion:emotion atIndex:i]; + } + if (self.debug) { + QMUILog(NSStringFromClass(self.class), @"点击的是当前页里的第 %@ 个表情,%@", @(i), emotion); + } + return; + } + } +} + +- (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 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 + +@implementation QMUIEmotionView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self didInitializedWithFrame:frame]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self didInitializedWithFrame:CGRectZero]; + } + 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; + + self.pagedEmotions = [[NSMutableArray alloc] init]; + + _collectionViewLayout = [[UICollectionViewFlowLayout alloc] init]; + self.collectionViewLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + self.collectionViewLayout.minimumLineSpacing = 0; + self.collectionViewLayout.minimumInteritemSpacing = 0; + self.collectionViewLayout.sectionInset = UIEdgeInsetsZero; + + _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; + self.collectionView.showsHorizontalScrollIndicator = NO; + self.collectionView.dataSource = self; + self.collectionView.delegate = self; + [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]; + + _sendButton = [[QMUIButton alloc] init]; + [self.sendButton setTitle:@"发送" forState:UIControlStateNormal]; + self.sendButton.contentEdgeInsets = UIEdgeInsetsMake(5, 17, 5, 17); + [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; + if (self.verticalAlignment) { + [self setNeedsLayout]; + } else { + [self pageEmotions]; + } +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + [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); + } +} + +- (void)pageEmotions { + [self.pagedEmotions removeAllObjects]; + self.pageControl.numberOfPages = 0; + + if (!CGRectIsEmpty(self.collectionView.bounds) && self.emotions.count && !CGSizeIsEmpty(self.emotionSize)) { + CGFloat contentWidthInPage = CGRectGetWidth(self.collectionView.bounds) - UIEdgeInsetsGetHorizontalValue(self.paddingInPage); + NSInteger maximumEmotionCountPerRowInPage = (contentWidthInPage + self.minimumEmotionHorizontalSpacing) / (self.emotionSize.width + self.minimumEmotionHorizontalSpacing); + NSInteger maximumEmotionCountPerPage = maximumEmotionCountPerRowInPage * self.numberOfRowsPerPage - 1;// 删除按钮占一个表情位置 + NSInteger pageCount = ceil((CGFloat)self.emotions.count / (CGFloat)maximumEmotionCountPerPage); + for (NSInteger i = 0; i < pageCount; i ++) { + NSRange emotionRangeForPage = NSMakeRange(maximumEmotionCountPerPage * i, maximumEmotionCountPerPage); + if (NSMaxRange(emotionRangeForPage) > self.emotions.count) { + // 最后一页可能不满一整页,所以取剩余的所有表情即可 + emotionRangeForPage.length = self.emotions.count - emotionRangeForPage.location; + } + NSArray *emotionForPage = [self.emotions objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:emotionRangeForPage]]; + [self.pagedEmotions addObject:emotionForPage]; + } + self.pageControl.numberOfPages = pageCount; + } + + [self.collectionView reloadData]; + [self.collectionView qmui_scrollToTop]; +} + +- (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 { + _sendButtonTitleAttributes = sendButtonTitleAttributes; + [self.sendButton setAttributedTitle:[[NSAttributedString alloc] initWithString:[self.sendButton currentTitle] attributes:_sendButtonTitleAttributes] forState:UIControlStateNormal]; +} + +- (void)setSendButtonBackgroundColor:(UIColor *)sendButtonBackgroundColor { + _sendButtonBackgroundColor = sendButtonBackgroundColor; + self.sendButton.backgroundColor = _sendButtonBackgroundColor; +} + +- (void)setSendButtonCornerRadius:(CGFloat)sendButtonCornerRadius { + _sendButtonCornerRadius = 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 { + return self.pagedEmotions.count; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + QMUIEmotionPageView *pageView = [collectionView dequeueReusableCellWithReuseIdentifier:@"page" forIndexPath:indexPath]; + pageView.delegate = self; + pageView.emotions = self.pagedEmotions[indexPath.item]; + pageView.padding = self.paddingInPage; + pageView.numberOfRows = self.numberOfRowsPerPage; + pageView.emotionSize = self.emotionSize; + pageView.emotionSelectedBackgroundExtension = self.emotionSelectedBackgroundExtension; + pageView.minimumEmotionHorizontalSpacing = self.minimumEmotionHorizontalSpacing; + [pageView updateDeleteButton:self.deleteButton]; + pageView.deleteButtonOffset = self.deleteButtonOffset; + pageView.debug = self.debug; + [pageView setNeedsDisplay]; + return pageView; +} + +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + if (scrollView == self.collectionView) { + NSInteger currentPage = round(scrollView.contentOffset.x / CGRectGetWidth(scrollView.bounds)); + self.pageControl.currentPage = currentPage; + } +} + +#pragma mark - + +- (void)emotionPageView:(QMUIEmotionPageView *)emotionPageView didSelectEmotion:(QMUIEmotion *)emotion atIndex:(NSInteger)index { + if (self.didSelectEmotionBlock) { + NSInteger index = [self.emotions indexOfObject:emotion]; + self.didSelectEmotionBlock(index, emotion); + } +} + +- (void)emotionPageViewDidLayoutEmotions:(QMUIEmotionPageView *)emotionPageView { + if (self.verticalAlignment) { + [self adjustEmotionsAlpha]; + } +} + +#pragma mark - Getter + +- (UIScrollView *)scrollView { + return self.verticalScrollView; +} + +@end + +@interface QMUIEmotionView (UIAppearance) + +@end + +@implementation QMUIEmotionView (UIAppearance) + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self setDefaultAppearance]; + }); +} + ++ (void)setDefaultAppearance { + QMUIEmotionView *appearance = [QMUIEmotionView appearance]; + 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; + appearance.emotionSize = CGSizeMake(30, 30); + appearance.emotionSelectedBackgroundExtension = UIEdgeInsetsMake(-3, -3, -3, -3); + appearance.minimumEmotionHorizontalSpacing = 10; + appearance.sendButtonTitleAttributes = @{NSFontAttributeName: UIFontMake(15), NSForegroundColorAttributeName: UIColorWhite}; + appearance.sendButtonBackgroundColor = UIColorBlue; + 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); + pageControlAppearance.currentPageIndicatorTintColor = UIColorMake(162, 162, 162); +} + +@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIEmptyView.h b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.h similarity index 81% rename from QMUI/QMUIKit/UIComponents/QMUIEmptyView.h rename to QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.h index a4ebb6d4..c4d74647 100644 --- a/QMUI/QMUIKit/UIComponents/QMUIEmptyView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.h @@ -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. + */ + // // QMUIEmptyView.h // qmui // -// Created by 李凯 on 2016/10/9. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2016/10/9. // #import +@class QMUIButton; + @protocol QMUIEmptyViewLoadingViewProtocol @optional @@ -26,7 +35,7 @@ @property(nonatomic, strong, readonly) UIImageView *imageView; @property(nonatomic, strong, readonly) UILabel *textLabel; @property(nonatomic, strong, readonly) UILabel *detailTextLabel; -@property(nonatomic, strong, readonly) UIButton *actionButton; +@property(nonatomic, strong, readonly) QMUIButton *actionButton; // 可通过调整这些insets来控制间距 @property(nonatomic, assign) UIEdgeInsets imageViewInsets UI_APPEARANCE_SELECTOR; // 默认为(0, 0, 36, 0) diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.m b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.m new file mode 100644 index 00000000..ad7fec75 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIEmptyView.m @@ -0,0 +1,303 @@ +/** + * 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. + */ + +// +// QMUIEmptyView.m +// qmui +// +// Created by QMUI Team on 2016/10/9. +// + +#import "QMUIEmptyView.h" +#import "QMUICore.h" +#import "UIControl+QMUI.h" +#import "NSParagraphStyle+QMUI.h" +#import "UIView+QMUI.h" +#import "QMUIButton.h" +#import "QMUIAppearance.h" + +@interface QMUIEmptyView () + +@property(nonatomic, strong) UIScrollView *scrollView; // 保证内容超出屏幕时也不至于直接被clip(比如横屏时) + +@end + +@implementation QMUIEmptyView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self didInitialize]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self didInitialize]; + } + return self; +} + +- (void)didInitialize { + // 系统默认会在view即将被add到window上时才设置这些值,这个时机有点晚了,因为我们可能在add到window之前就进行sizeThatFits计算或对view进行截图等操作,因此这里提前到init时就去做 + [self qmui_applyAppearance]; + + self.scrollView = [[UIScrollView alloc] init]; + self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + self.scrollView.showsVerticalScrollIndicator = NO; + self.scrollView.showsHorizontalScrollIndicator = NO; + self.scrollView.scrollsToTop = NO; + self.scrollView.contentInset = UIEdgeInsetsMake(0, 16, 0, 16); + [self addSubview:self.scrollView]; + + _contentView = [[UIView alloc] init]; + [self.scrollView addSubview:self.contentView]; + + _loadingView = (UIView *)[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; + ((UIActivityIndicatorView *)self.loadingView).hidesWhenStopped = NO; // 此控件是通过loadingView.hidden属性来控制显隐的,如果UIActivityIndicatorView的hidesWhenStopped属性设置为YES的话,则手动设置它的hidden属性就会失效,因此这里要置为NO + [self.contentView addSubview:self.loadingView]; + + _imageView = [[UIImageView alloc] init]; + self.imageView.contentMode = UIViewContentModeCenter; + [self.contentView addSubview:self.imageView]; + + _textLabel = [[UILabel alloc] init]; + self.textLabel.textAlignment = NSTextAlignmentCenter; + self.textLabel.numberOfLines = 0; + [self.contentView addSubview:self.textLabel]; + + _detailTextLabel = [[UILabel alloc] init]; + self.detailTextLabel.textAlignment = NSTextAlignmentCenter; + self.detailTextLabel.numberOfLines = 0; + [self.contentView addSubview:self.detailTextLabel]; + + _actionButton = [[QMUIButton alloc] init]; + self.actionButton.qmui_outsideEdge = UIEdgeInsetsMake(-20, -20, -20, -20); + self.actionButton.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + [self.contentView addSubview:self.actionButton]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + self.scrollView.frame = self.bounds; + + CGSize contentViewSize = CGSizeFlatted([self sizeThatContentViewFits]); + // contentView 默认垂直居中于 scrollView + self.contentView.frame = CGRectFlatMake(0, CGRectGetMidY(self.scrollView.bounds) - contentViewSize.height / 2 + self.verticalOffset, contentViewSize.width, contentViewSize.height); + + // 如果 contentView 要比 scrollView 高,则置顶展示 + if (CGRectGetHeight(self.contentView.bounds) > CGRectGetHeight(self.scrollView.bounds)) { + self.contentView.frame = CGRectSetY(self.contentView.frame, 0); + } + + 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; + + if (!self.imageView.hidden) { + [self.imageView sizeToFit]; + self.imageView.frame = CGRectSetXY(self.imageView.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.contentView.bounds, self.imageView.frame) + self.imageViewInsets.left - self.imageViewInsets.right, originY + self.imageViewInsets.top); + originY = CGRectGetMaxY(self.imageView.frame) + self.imageViewInsets.bottom; + } + + if (!self.loadingView.hidden) { + self.loadingView.frame = CGRectSetXY(self.loadingView.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.contentView.bounds, self.loadingView.frame) + self.loadingViewInsets.left - self.loadingViewInsets.right, originY + self.loadingViewInsets.top); + originY = CGRectGetMaxY(self.loadingView.frame) + self.loadingViewInsets.bottom; + } + + if (!self.textLabel.hidden) { + self.textLabel.frame = CGRectFlatMake(self.textLabelInsets.left, originY + self.textLabelInsets.top, CGRectGetWidth(self.contentView.bounds) - UIEdgeInsetsGetHorizontalValue(self.textLabelInsets), QMUIViewSelfSizingHeight); + originY = CGRectGetMaxY(self.textLabel.frame) + self.textLabelInsets.bottom; + } + + if (!self.detailTextLabel.hidden) { + self.detailTextLabel.frame = CGRectFlatMake(self.detailTextLabelInsets.left, originY + self.detailTextLabelInsets.top, CGRectGetWidth(self.contentView.bounds) - UIEdgeInsetsGetHorizontalValue(self.detailTextLabelInsets), QMUIViewSelfSizingHeight); + originY = CGRectGetMaxY(self.detailTextLabel.frame) + self.detailTextLabelInsets.bottom; + } + + if (!self.actionButton.hidden) { + [self.actionButton sizeToFit]; + self.actionButton.frame = CGRectSetXY(self.actionButton.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.contentView.bounds, self.actionButton.frame) + self.actionButtonInsets.left - self.actionButtonInsets.right, originY + self.actionButtonInsets.top); + originY = CGRectGetMaxY(self.actionButton.frame) + self.actionButtonInsets.bottom; + } +} + +- (CGSize)sizeThatContentViewFits { + CGFloat resultWidth = CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.contentInset); + 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; + } + + return CGSizeMake(resultWidth, resultHeight); +} + +- (void)updateDetailTextLabelWithText:(NSString *)text { + if (self.detailTextLabelFont && self.detailTextLabelTextColor && text) { + NSAttributedString *string = [[NSAttributedString alloc] initWithString:text attributes:@{ + NSFontAttributeName: self.detailTextLabelFont, + NSForegroundColorAttributeName: self.detailTextLabelTextColor, + NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:self.detailTextLabelFont.pointSize + 10 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter] + }]; + self.detailTextLabel.attributedText = string; + } + self.detailTextLabel.hidden = !text; + [self setNeedsLayout]; +} + +- (void)setLoadingView:(UIView *)loadingView { + if (self.loadingView != loadingView) { + [self.loadingView removeFromSuperview]; + _loadingView = loadingView; + [self.contentView addSubview:loadingView]; + } + [self setNeedsLayout]; +} + +- (void)setLoadingViewHidden:(BOOL)hidden { + self.loadingView.hidden = hidden; + if (!hidden && [self.loadingView respondsToSelector:@selector(startAnimating)]) { + [self.loadingView startAnimating]; + } + [self setNeedsLayout]; +} + +- (void)setImage:(UIImage *)image { + self.imageView.image = image; + self.imageView.hidden = !image; + [self setNeedsLayout]; +} + +- (void)setTextLabelText:(NSString *)text { + self.textLabel.text = text; + self.textLabel.hidden = !text; + [self setNeedsLayout]; +} + +- (void)setDetailTextLabelText:(NSString *)text { + [self updateDetailTextLabelWithText:text]; +} + +- (void)setActionButtonTitle:(NSString *)title { + [self.actionButton setTitle:title forState:UIControlStateNormal]; + self.actionButton.hidden = !title; + [self setNeedsLayout]; +} + +- (void)setImageViewInsets:(UIEdgeInsets)imageViewInsets { + _imageViewInsets = imageViewInsets; + [self setNeedsLayout]; +} + +- (void)setTextLabelInsets:(UIEdgeInsets)textLabelInsets { + _textLabelInsets = textLabelInsets; + [self setNeedsLayout]; +} + +- (void)setDetailTextLabelInsets:(UIEdgeInsets)detailTextLabelInsets { + _detailTextLabelInsets = detailTextLabelInsets; + [self setNeedsLayout]; +} + +- (void)setActionButtonInsets:(UIEdgeInsets)actionButtonInsets { + _actionButtonInsets = actionButtonInsets; + [self setNeedsLayout]; +} + +- (void)setVerticalOffset:(CGFloat)verticalOffset { + _verticalOffset = verticalOffset; + [self setNeedsLayout]; +} + +- (void)setTextLabelFont:(UIFont *)textLabelFont { + _textLabelFont = textLabelFont; + self.textLabel.font = textLabelFont; + [self setNeedsLayout]; +} + +- (void)setDetailTextLabelFont:(UIFont *)detailTextLabelFont { + _detailTextLabelFont = detailTextLabelFont; + [self updateDetailTextLabelWithText:self.detailTextLabel.text]; +} + +- (void)setActionButtonFont:(UIFont *)actionButtonFont { + _actionButtonFont = actionButtonFont; + self.actionButton.titleLabel.font = actionButtonFont; + [self setNeedsLayout]; +} + +- (void)setTextLabelTextColor:(UIColor *)textLabelTextColor { + _textLabelTextColor = textLabelTextColor; + self.textLabel.textColor = textLabelTextColor; +} + +- (void)setDetailTextLabelTextColor:(UIColor *)detailTextLabelTextColor { + _detailTextLabelTextColor = detailTextLabelTextColor; + [self updateDetailTextLabelWithText:self.detailTextLabel.text]; +} + +- (void)setActionButtonTitleColor:(UIColor *)actionButtonTitleColor { + _actionButtonTitleColor = actionButtonTitleColor; + [self.actionButton setTitleColor:actionButtonTitleColor forState:UIControlStateNormal]; + [self.actionButton setTitleColor:[actionButtonTitleColor colorWithAlphaComponent:ButtonHighlightedAlpha] forState:UIControlStateHighlighted]; + [self.actionButton setTitleColor:[actionButtonTitleColor colorWithAlphaComponent:ButtonDisabledAlpha] forState:UIControlStateDisabled]; +} + +@end + +@interface QMUIEmptyView (UIAppearance) + +@end + +@implementation QMUIEmptyView (UIAppearance) + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self setDefaultAppearance]; + }); +} + ++ (void)setDefaultAppearance { + QMUIEmptyView *appearance = [QMUIEmptyView appearance]; + appearance.imageViewInsets = UIEdgeInsetsMake(0, 0, 36, 0); + appearance.loadingViewInsets = UIEdgeInsetsMake(0, 0, 36, 0); + appearance.textLabelInsets = UIEdgeInsetsMake(0, 0, 10, 0); + appearance.detailTextLabelInsets = UIEdgeInsetsMake(0, 0, 14, 0); + appearance.actionButtonInsets = UIEdgeInsetsZero; + appearance.verticalOffset = -30; + + appearance.textLabelFont = UIFontMake(15); + appearance.detailTextLabelFont = UIFontMake(14); + appearance.actionButtonFont = UIFontMake(15); + + appearance.textLabelTextColor = UIColorMake(93, 100, 110); + appearance.detailTextLabelTextColor = UIColorMake(133, 140, 150); + appearance.actionButtonTitleColor = ButtonTintColor; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h b/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h new file mode 100644 index 00000000..a362bbb2 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.h @@ -0,0 +1,49 @@ +/** + * 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. + */ + +// +// QMUIFloatLayoutView.h +// qmui +// +// Created by QMUI Team on 2016/11/10. +// + +#import + +/// 用于属性 maximumItemSize,是它的默认值。表示 item 的最大宽高会自动根据当前 floatLayoutView 的内容大小来调整,从而避免 item 内容过多时可能溢出 floatLayoutView。 +extern const CGSize QMUIFloatLayoutViewAutomaticalMaximumItemSize; + +/** + * 做类似 CSS 里的 float:left 的布局,自行使用 addSubview: 将子 View 添加进来即可。 + * + * 支持通过 `contentMode` 属性修改子 View 的对齐方式,目前仅支持 `UIViewContentModeLeft` 和 `UIViewContentModeRight`,默认为 `UIViewContentModeLeft`。 + */ +@interface QMUIFloatLayoutView : UIView + +/** + * QMUIFloatLayoutView 内部的间距,默认为 UIEdgeInsetsZero + */ +@property(nonatomic, assign) UIEdgeInsets padding; + +/** + * item 的最小宽高,默认为 CGSizeZero,也即不限制。 + */ +@property(nonatomic, assign) IBInspectable CGSize minimumItemSize; + +/** + * item 的最大宽高,默认为 QMUIFloatLayoutViewAutomaticalMaximumItemSize,也即不超过 floatLayoutView 自身最大内容宽高。 + */ +@property(nonatomic, assign) IBInspectable CGSize maximumItemSize; + +/** + * item 之间的间距,默认为 UIEdgeInsetsZero。 + * + * @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 new file mode 100644 index 00000000..2bfa426a --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIFloatLayoutView.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. + */ + +// +// QMUIFloatLayoutView.m +// qmui +// +// Created by QMUI Team on 2016/11/10. +// + +#import "QMUIFloatLayoutView.h" +#import "QMUICore.h" + +#define ValueSwitchAlignLeftOrRight(valueLeft, valueRight) ([self shouldAlignRight] ? valueRight : valueLeft) + +const CGSize QMUIFloatLayoutViewAutomaticalMaximumItemSize = {-1, -1}; + +@implementation QMUIFloatLayoutView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self didInitialize]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self didInitialize]; + } + return self; +} + +- (void)didInitialize { + self.contentMode = UIViewContentModeLeft; + self.minimumItemSize = CGSizeZero; + self.maximumItemSize = QMUIFloatLayoutViewAutomaticalMaximumItemSize; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return [self layoutSubviewsWithSize:size shouldLayout:NO]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + [self layoutSubviewsWithSize:self.bounds.size shouldLayout:YES]; +} + +- (CGSize)layoutSubviewsWithSize:(CGSize)size shouldLayout:(BOOL)shouldLayout { + NSArray *visibleItemViews = [self visibleSubviews]; + + if (visibleItemViews.count == 0) { + return CGSizeMake(UIEdgeInsetsGetHorizontalValue(self.padding), UIEdgeInsetsGetVerticalValue(self.padding)); + } + + // 如果是左对齐,则代表 item 左上角的坐标,如果是右对齐,则代表 item 右上角的坐标 + 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; + 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 = 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 的 + itemViewFrame = CGRectMake(ValueSwitchAlignLeftOrRight(self.padding.left, size.width - self.padding.right - itemViewSize.width), currentRowMaxY, itemViewSize.width, itemViewSize.height); + itemViewOrigin.y = CGRectGetMinY(itemViewFrame); + } else { + // 当前行放得下 + 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); + + if (shouldLayout) { + itemView.frame = itemViewFrame; + } + } + + // 最后一行不需要考虑 itemMarins.bottom,所以这里减掉 + currentRowMaxY -= self.itemMargins.bottom; + + CGSize resultSize = CGSizeMake(size.width, currentRowMaxY + self.padding.bottom); + return resultSize; +} + +- (NSArray *)visibleSubviews { + NSMutableArray *visibleItemViews = [[NSMutableArray alloc] init]; + + for (NSInteger i = 0, l = self.subviews.count; i < l; i++) { + UIView *itemView = self.subviews[i]; + if (!itemView.hidden) { + [visibleItemViews addObject:itemView]; + } + } + + return visibleItemViews; +} + +- (BOOL)shouldAlignRight { + return self.contentMode == UIViewContentModeRight; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIGridView.h b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.h new file mode 100644 index 00000000..80e35b7a --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.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. + */ + +// +// QMUIGridView.h +// qmui +// +// Created by QMUI Team on 15/1/30. +// + +#import + +/** + * 用于做九宫格布局,会将内部所有的 subview 根据指定的列数和行高,把每个 item(也即 subview) 拉伸到相同的大小。 + * + * 支持在 item 和 item 之间显示分隔线,分隔线支持虚线。 + * + * @warning 注意分隔线是占位的,把 item 隔开,而不是盖在某个 item 上。 + */ +@interface QMUIGridView : UIView + +/// 指定要显示的列数,默认为 0 +@property(nonatomic, assign) IBInspectable NSInteger columnCount; + +/// 指定每一行的高度,默认为 0 +@property(nonatomic, assign) IBInspectable CGFloat rowHeight; + +/// 内部的 padding,默认为 UIEdgeInsetsZero +@property(nonatomic, assign) UIEdgeInsets padding; + +/// 指定 item 之间的分隔线宽度,默认为 0 +@property(nonatomic, assign) IBInspectable CGFloat separatorWidth; + +/// 指定 item 之间的分隔线颜色,默认为 UIColorSeparator +@property(nonatomic, strong) IBInspectable UIColor *separatorColor; + +/// item 之间的分隔线是否要用虚线显示,默认为 NO +@property(nonatomic, assign) IBInspectable BOOL separatorDashed; + +/// 候选的初始化方法,亦可通过 initWithFrame:、init 来初始化。 +- (instancetype)initWithColumn:(NSInteger)column rowHeight:(CGFloat)rowHeight; +@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIGridView.m b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.m similarity index 75% rename from QMUI/QMUIKit/UIComponents/QMUIGridView.m rename to QMUI/QMUIKit/QMUIComponents/QMUIGridView.m index 03114fc5..b8214088 100644 --- a/QMUI/QMUIKit/UIComponents/QMUIGridView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIGridView.m @@ -1,9 +1,16 @@ +/** + * 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. + */ + // // QMUIGridView.m // qmui // -// Created by MoLice on 15/1/30. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/1/30. // #import "QMUIGridView.h" @@ -19,7 +26,7 @@ @implementation QMUIGridView - (instancetype)initWithFrame:(CGRect)frame column:(NSInteger)column rowHeight:(CGFloat)rowHeight { if (self = [super initWithFrame:frame]) { - [self didInitialized]; + [self didInitialize]; self.columnCount = column; self.rowHeight = rowHeight; } @@ -36,12 +43,12 @@ - (instancetype)initWithFrame:(CGRect)frame { - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; + [self didInitialize]; } return self; } -- (void)didInitialized { +- (void)didInitialize { self.separatorLayer = [CAShapeLayer layer]; [self.separatorLayer qmui_removeDefaultAnimations]; self.separatorLayer.hidden = YES; @@ -63,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 { @@ -74,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; } @@ -102,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/UIComponents/QMUIImagePreviewView.h b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h similarity index 80% rename from QMUI/QMUIKit/UIComponents/QMUIImagePreviewView.h rename to QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h index 17079058..85838dc0 100644 --- a/QMUI/QMUIKit/UIComponents/QMUIImagePreviewView.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h @@ -1,9 +1,16 @@ +/** + * 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. + */ + // // QMUIImagePreviewView.h // qmui // -// Created by MoLice on 2016/11/30. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2016/11/30. // #import @@ -29,8 +36,6 @@ typedef NS_ENUM (NSUInteger, QMUIImagePreviewMediaType) { // 返回要展示的媒体资源的类型(图片、live photo、视频),如果不实现此方法,则 QMUIImagePreviewView 将无法选择最合适的 cell 来复用从而略微增大系统开销 - (QMUIImagePreviewMediaType)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView assetTypeAtIndex:(NSUInteger)index; -@optional - /** * 当左右的滚动停止时会触发这个方法 * @param imagePreviewView 当前预览的 QMUIImagePreviewView @@ -45,8 +50,6 @@ typedef NS_ENUM (NSUInteger, QMUIImagePreviewMediaType) { */ - (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView willScrollHalfToIndex:(NSUInteger)index; -@optional - @end /** @@ -63,7 +66,7 @@ typedef NS_ENUM (NSUInteger, QMUIImagePreviewMediaType) { * * @see QMUIImagePreviewViewController */ -@interface QMUIImagePreviewView : UIView +@interface QMUIImagePreviewView : UIView @property(nonatomic, weak) id delegate; @property(nonatomic, strong, readonly) UICollectionView *collectionView; @@ -78,13 +81,13 @@ typedef NS_ENUM (NSUInteger, QMUIImagePreviewMediaType) { @end -@interface QMUIImagePreviewView (QMUIZoomImageView) +@interface QMUIImagePreviewView (QMUIZoomImageView) /** * 获取某个 QMUIZoomImageView 所对应的 index * @return zoomImageView 对应的 index,若当前的 zoomImageView 不可见,会返回 0 */ -- (NSUInteger)indexForZoomImageView:(QMUIZoomImageView *)zoomImageView; +- (NSInteger)indexForZoomImageView:(QMUIZoomImageView *)zoomImageView; /** * 获取某个 index 对应的 zoomImageView diff --git a/QMUI/QMUIKit/UIComponents/QMUIImagePreviewView.m b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m similarity index 78% rename from QMUI/QMUIKit/UIComponents/QMUIImagePreviewView.m rename to QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m index 074161f6..9aa70d68 100644 --- a/QMUI/QMUIKit/UIComponents/QMUIImagePreviewView.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m @@ -1,18 +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. + */ + // // QMUIImagePreviewView.m // qmui // -// Created by MoLice on 2016/11/30. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2016/11/30. // #import "QMUIImagePreviewView.h" #import "QMUICore.h" #import "QMUICollectionViewPagingLayout.h" -#import "QMUIZoomImageView.h" #import "NSObject+QMUI.h" #import "UICollectionView+QMUI.h" +#import "UIView+QMUI.h" #import "QMUIEmptyView.h" +#import "QMUILog.h" +#import "QMUIPieProgressView.h" +#import "QMUIButton.h" @interface QMUIImagePreviewCell : UICollectionViewCell @@ -34,7 +44,7 @@ - (instancetype)initWithFrame:(CGRect)frame { - (void)layoutSubviews { [super layoutSubviews]; - self.zoomImageView.frame = self.contentView.bounds; + self.zoomImageView.qmui_frameApplyTransform = self.contentView.bounds; } @end @@ -73,14 +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; + 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]; @@ -113,9 +124,9 @@ - (void)setCurrentImageIndex:(NSUInteger)currentImageIndex animated:(BOOL)animat [self.collectionView reloadData]; if (currentImageIndex < [self.collectionView numberOfItemsInSection:0]) { [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:currentImageIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:animated]; + [self.collectionView layoutIfNeeded];// scroll immediately } else { - // dataSource 里的图片数量和当前 View 层的图片数量不匹配 - QMUILog(@"%@ %s,collectionView.numberOfItems = %@, collectionViewDataSource.numberOfItems = %@, currentImageIndex = %@", NSStringFromClass([self class]), __func__, @([self.collectionView numberOfItemsInSection:0]), @([self.collectionView.dataSource numberOfSectionsInCollectionView:self.collectionView]), @(_currentImageIndex)); + QMUILog(@"QMUIImagePreviewView", @"dataSource 里的图片数量和当前显示出来的图片数量不匹配, collectionView.numberOfItems = %@, collectionViewDataSource.numberOfItems = %@, currentImageIndex = %@", @([self.collectionView numberOfItemsInSection:0]), @([self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:1]), @(_currentImageIndex)); } } @@ -149,18 +160,17 @@ - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cell QMUIImagePreviewCell *cell = (QMUIImagePreviewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath]; QMUIZoomImageView *zoomView = cell.zoomImageView; ((UIActivityIndicatorView *)zoomView.emptyView.loadingView).color = self.loadingColor; + zoomView.cloudProgressView.tintColor = self.loadingColor; + zoomView.cloudDownloadRetryButton.tintColor = self.loadingColor; zoomView.delegate = self; // 因为 cell 复用的问题,很可能此时会显示一张错误的图片,因此这里要清空所有图片的显示 zoomView.image = nil; - zoomView.livePhoto = nil; zoomView.videoPlayerItem = nil; - [zoomView showLoading]; - + zoomView.livePhoto = nil; if ([self.delegate respondsToSelector:@selector(imagePreviewView:renderZoomImageView:atIndex:)]) { [self.delegate imagePreviewView:self renderZoomImageView:zoomView atIndex:indexPath.item]; } - [zoomView hideEmptyView]; return cell; } @@ -207,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]) { @@ -228,13 +245,13 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { @implementation QMUIImagePreviewView (QMUIZoomImageView) -- (NSUInteger)indexForZoomImageView:(QMUIZoomImageView *)zoomImageView { +- (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 NSUIntegerMax; + return NSNotFound; } - (QMUIZoomImageView *)zoomImageViewAtIndex:(NSUInteger)index { @@ -246,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 @@ -275,27 +292,26 @@ - (void)longPressInZoomingImageView:(QMUIZoomImageView *)imageView { } } -- (void)zoomImageView:(QMUIZoomImageView *)imageView didHideVideoToolbar:(BOOL)didHide { +- (void)didTouchICloudRetryButtonInZoomImageView:(QMUIZoomImageView *)imageView { [self checkIfDelegateMissing]; if ([self.delegate respondsToSelector:_cmd]) { - [self.delegate zoomImageView:imageView didHideVideoToolbar:didHide]; + [self.delegate didTouchICloudRetryButtonInZoomImageView:imageView]; } } -- (BOOL)enabledZoomViewInZoomImageView:(QMUIZoomImageView *)imageView { +- (void)zoomImageView:(QMUIZoomImageView *)imageView didHideVideoToolbar:(BOOL)didHide { [self checkIfDelegateMissing]; if ([self.delegate respondsToSelector:_cmd]) { - return [self.delegate enabledZoomViewInZoomImageView:imageView]; + [self.delegate zoomImageView:imageView didHideVideoToolbar:didHide]; } - return YES; } -- (UIEdgeInsets)contentInsetsForVideoToolbar:(QMUIZoomImageViewVideoToolbar *)toolbar inZoomingImageView:(QMUIZoomImageView *)zoomImageView { +- (BOOL)enabledZoomViewInZoomImageView:(QMUIZoomImageView *)imageView { [self checkIfDelegateMissing]; if ([self.delegate respondsToSelector:_cmd]) { - return [self.delegate contentInsetsForVideoToolbar:toolbar inZoomingImageView:zoomImageView]; + return [self.delegate enabledZoomViewInZoomImageView:imageView]; } - return toolbar.contentInsets; + return YES; } @end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h new file mode 100644 index 00000000..be6ff859 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h @@ -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. + */ + +// +// QMUIImagePreviewViewController.h +// qmui +// +// Created by QMUI Team on 2016/11/30. +// + +#import +#import "QMUICommonViewController.h" +#import "QMUIImagePreviewView.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIImagePreviewViewTransitionAnimator; + +typedef NS_ENUM(NSUInteger, QMUIImagePreviewViewControllerTransitioningStyle) { + /// present 时整个界面渐现,dismiss 时整个界面渐隐,默认。 + QMUIImagePreviewViewControllerTransitioningStyleFade, + + /// present 时从某个指定的位置缩放到屏幕中央,dismiss 时缩放到指定位置,必须实现 sourceImageView 并返回一个非空的值 + QMUIImagePreviewViewControllerTransitioningStyleZoom +}; + +extern const CGFloat QMUIImagePreviewViewControllerCornerRadiusAutomaticDimension; + +/** + * 图片预览控件,主要功能由内部自带的 QMUIImagePreviewView 提供,由于以 viewController 的形式存在,所以适用于那种在单独界面里展示图片,或者需要从某张目标图片的位置以动画的形式放大进入预览界面的场景。 + * + * 使用方式: + * + * 1. 使用 init 方法初始化 + * 2. 添加 self.imagePreviewView 的 delegate + * 3. 以 push 或 present 的方式打开界面。如果是 present,则支持 QMUIImagePreviewViewControllerTransitioningStyle 里定义的动画。特别地,如果使用 zoom 方式,则需要通过 sourceImageView() 返回一个原界面上的 view 以作为 present 动画的起点和 dismiss 动画的终点。 + * + * @see QMUIImagePreviewView + */ +@interface QMUIImagePreviewViewController : QMUICommonViewController + +/// 图片背后的黑色背景,默认为配置表里的 UIColorBlack +@property(nullable, nonatomic, strong) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; + +@property(null_resettable, nonatomic, strong, readonly) QMUIImagePreviewView *imagePreviewView; + +/// 以 present 方式进入大图预览的时候使用的转场动画 animator,可通过 QMUIImagePreviewViewTransitionAnimator 提供的若干个 block 属性自定义动画,也可以完全重写一个自己的 animator。 +@property(nullable, nonatomic, strong) __kindof QMUIImagePreviewViewTransitionAnimator *transitioningAnimator; + +/// present 时的动画,默认为 fade,当修改了 presentingStyle 时会自动把 dismissingStyle 也修改为相同的值。 +@property(nonatomic, assign) QMUIImagePreviewViewControllerTransitioningStyle presentingStyle; + +/// dismiss 时的动画,默认为 fade,默认与 presentingStyle 的值相同,若需要与之不同,请在设置完 presentingStyle 之后再设置 dismissingStyle。 +@property(nonatomic, assign) QMUIImagePreviewViewControllerTransitioningStyle dismissingStyle; + +/// 当以 zoom 动画进入/退出大图预览时,会通过这个 block 获取到原本界面上的图片所在的 view,从而进行动画的位置计算,如果返回的值为 nil,则会强制使用 fade 动画。当同时存在 sourceImageView 和 sourceImageRect 时,只有 sourceImageRect 会被调用。 +@property(nullable, nonatomic, copy) UIView * _Nullable (^sourceImageView)(void); + +/// 当以 zoom 动画进入/退出大图预览时,会通过这个 block 获取到原本界面上的图片所在的 view,从而进行动画的位置计算,如果返回的值为 CGRectZero,则会强制使用 fade 动画。注意返回值要进行坐标系转换。当同时存在 sourceImageView 和 sourceImageRect 时,只有 sourceImageRect 会被调用。 +@property(nullable, nonatomic, copy) CGRect (^sourceImageRect)(void); + +/// 当以 zoom 动画进入/退出大图预览时,可以指定一个圆角值,默认为 QMUIImagePreviewViewControllerCornerRadiusAutomaticDimension,也即自动从 sourceImageView.layer.cornerRadius 获取,如果使用的是 sourceImageRect 或希望自定义圆角值,则直接给 sourceImageCornerRadius 赋值即可。 +@property(nonatomic, assign) CGFloat sourceImageCornerRadius; + +/// 是否支持手势拖拽退出预览模式,默认为 YES。仅对以 present 方式进入大图预览的场景有效。 +@property(nonatomic, assign) BOOL dismissingGestureEnabled; + +@end + +@interface QMUIImagePreviewViewController (UIAppearance) + ++ (instancetype)appearance; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m new file mode 100644 index 00000000..167572ab --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m @@ -0,0 +1,282 @@ +/** + * 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. + */ + +// +// QMUIImagePreviewViewController.m +// qmui +// +// Created by QMUI Team on 2016/11/30. +// + +#import "QMUIImagePreviewViewController.h" +#import "QMUICore.h" +#import "QMUIImagePreviewViewTransitionAnimator.h" +#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 initAppearance]; + }); +} + ++ (void)initAppearance { + QMUIImagePreviewViewController.appearance.backgroundColor = UIColorBlack; +} + +@end + +@interface QMUIImagePreviewViewController () + +@property(nonatomic, strong) UIPanGestureRecognizer *dismissingGesture; +@property(nonatomic, assign) CGPoint gestureBeganLocation; +@property(nonatomic, weak) QMUIZoomImageView *gestureZoomImageView; +@property(nonatomic, assign) BOOL canShowPresentingViewControllerWhenGesturing; +@property(nonatomic, assign) BOOL originalStatusBarHidden; +@property(nonatomic, assign) BOOL statusBarHidden; +@end + +@implementation QMUIImagePreviewViewController + +- (void)didInitialize { + [super didInitialize]; + + self.sourceImageCornerRadius = QMUIImagePreviewViewControllerCornerRadiusAutomaticDimension; + + _dismissingGestureEnabled = YES; + + [self qmui_applyAppearance]; + + self.qmui_prefersHomeIndicatorAutoHiddenBlock = ^BOOL{ + return YES; + }; + + + // present style + self.transitioningAnimator = [[QMUIImagePreviewViewTransitionAnimator alloc] init]; + self.modalPresentationStyle = UIModalPresentationCustom; + self.modalPresentationCapturesStatusBarAppearance = YES; + self.transitioningDelegate = self; +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor { + _backgroundColor = backgroundColor; + if ([self isViewLoaded]) { + self.view.backgroundColor = backgroundColor; + } +} + +@synthesize imagePreviewView = _imagePreviewView; +- (QMUIImagePreviewView *)imagePreviewView { + if (!_imagePreviewView) { + _imagePreviewView = [[QMUIImagePreviewView alloc] initWithFrame:self.isViewLoaded ? self.view.bounds : CGRectZero]; + } + return _imagePreviewView; +} + +- (void)initSubviews { + [super initSubviews]; + self.view.backgroundColor = self.backgroundColor; + [self.view addSubview:self.imagePreviewView]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + self.imagePreviewView.qmui_frameApplyTransform = self.view.bounds; + + UIViewController *backendViewController = [self visibleViewControllerWithViewController:self.presentingViewController]; + self.canShowPresentingViewControllerWhenGesturing = [QMUIHelper interfaceOrientationMask:backendViewController.supportedInterfaceOrientations containsInterfaceOrientation:UIApplication.sharedApplication.statusBarOrientation]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + if (self.qmui_isPresented) { + [self initObjectsForZoomStyleIfNeeded]; + } + [self.imagePreviewView.collectionView reloadData]; + [self.imagePreviewView.collectionView layoutIfNeeded]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + if (self.qmui_isPresented) { + self.statusBarHidden = YES; + } + [self setNeedsStatusBarAppearanceUpdate]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + self.statusBarHidden = self.originalStatusBarHidden; + [self setNeedsStatusBarAppearanceUpdate]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + [self removeObjectsForZoomStyle]; + [self resetDismissingGesture]; +} + +- (void)setPresentingStyle:(QMUIImagePreviewViewControllerTransitioningStyle)presentingStyle { + _presentingStyle = presentingStyle; + self.dismissingStyle = presentingStyle; +} + +- (void)setTransitioningAnimator:(__kindof QMUIImagePreviewViewTransitionAnimator *)transitioningAnimator { + _transitioningAnimator = transitioningAnimator; + transitioningAnimator.imagePreviewViewController = self; +} + +- (BOOL)prefersStatusBarHidden { + if (self.qmui_visibleState < QMUIViewControllerDidAppear || self.qmui_visibleState >= QMUIViewControllerDidDisappear) { + // 在 present/dismiss 动画过程中,都使用原界面的状态栏显隐状态 + if (self.presentingViewController) { + BOOL statusBarHidden = self.presentingViewController.view.window.windowScene.statusBarManager.statusBarHidden; + self.originalStatusBarHidden = statusBarHidden; + return self.originalStatusBarHidden; + } + return [super prefersStatusBarHidden]; + } + return self.statusBarHidden; +} + +#pragma mark - 动画 + +- (void)initObjectsForZoomStyleIfNeeded { + if (!self.dismissingGesture && self.dismissingGestureEnabled) { + self.dismissingGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleDismissingPreviewGesture:)]; + [self.view addGestureRecognizer:self.dismissingGesture]; + } +} + +- (void)removeObjectsForZoomStyle { + [self.dismissingGesture removeTarget:self action:@selector(handleDismissingPreviewGesture:)]; + [self.view removeGestureRecognizer:self.dismissingGesture]; + self.dismissingGesture = nil; +} + +- (void)handleDismissingPreviewGesture:(UIPanGestureRecognizer *)gesture { + + if (!self.dismissingGestureEnabled) return; + + switch (gesture.state) { + case UIGestureRecognizerStateBegan: + self.gestureBeganLocation = [gesture locationInView:self.view]; + self.gestureZoomImageView = [self.imagePreviewView zoomImageViewAtIndex:self.imagePreviewView.currentImageIndex]; + self.gestureZoomImageView.scrollView.clipsToBounds = NO;// 当 contentView 被放大后,如果不去掉 clipToBounds,那么手势退出预览时,contentView 溢出的那部分内容就看不到 + break; + + case UIGestureRecognizerStateChanged: { + CGPoint location = [gesture locationInView:self.view]; + CGFloat horizontalDistance = location.x - self.gestureBeganLocation.x; + CGFloat verticalDistance = location.y - self.gestureBeganLocation.y; + CGFloat ratio = 1.0; + CGFloat alpha = 1.0; + if (verticalDistance > 0) { + // 往下拉的话,图片缩小,但图片移动距离与手指移动距离保持一致 + ratio = 1.0 - verticalDistance / CGRectGetHeight(self.view.bounds) / 2; + + // 如果预览大图支持横竖屏而背后的界面只支持竖屏,则在横屏时手势拖拽不要露出背后的界面 + if (self.canShowPresentingViewControllerWhenGesturing) { + alpha = 1.0 - verticalDistance / CGRectGetHeight(self.view.bounds) * 1.8; + } + } else { + // 往上拉的话,图片不缩小,但手指越往上移动,图片将会越难被拖走 + CGFloat a = self.gestureBeganLocation.y + 100;// 后面这个加数越大,拖动时会越快达到不怎么拖得动的状态 + CGFloat b = 1 - pow((a - fabs(verticalDistance)) / a, 2); + CGFloat contentViewHeight = CGRectGetHeight(self.gestureZoomImageView.contentViewRectInZoomImageView); + CGFloat c = (CGRectGetHeight(self.view.bounds) - contentViewHeight) / 2; + verticalDistance = -c * b; + } + CGAffineTransform transform = CGAffineTransformMakeTranslation(horizontalDistance, verticalDistance); + transform = CGAffineTransformScale(transform, ratio, ratio); + self.gestureZoomImageView.transform = transform; + self.view.backgroundColor = [self.view.backgroundColor colorWithAlphaComponent:alpha]; + BOOL statusBarHidden = alpha >= 1 ? YES : self.originalStatusBarHidden; + if (statusBarHidden != self.statusBarHidden) { + self.statusBarHidden = statusBarHidden; + [self setNeedsStatusBarAppearanceUpdate]; + } + } + break; + + case UIGestureRecognizerStateEnded: { + CGPoint location = [gesture locationInView:self.view]; + CGFloat verticalDistance = location.y - self.gestureBeganLocation.y; + if (verticalDistance > CGRectGetHeight(self.view.bounds) / 2 / 3) { + + // 如果背后的界面支持的方向与当前预览大图的界面不一样,则为了避免在 dismiss 后看到背后界面的旋转,这里提前触发背后界面的 viewWillAppear,从而借助 AutomaticallyRotateDeviceOrientation 的功能去提前旋转到正确方向。(备忘,如果不这么处理,标准的触发 viewWillAppear: 的时机是在 animator 的 animateTransition: 时,这里就算重复调用一次也不会导致 viewWillAppear: 多次触发) + // 这里只能解决手势拖拽的 dismiss,如果是业务代码手动调用 dismiss 则无法兼顾,再看怎么处理。 + if (!self.canShowPresentingViewControllerWhenGesturing) { + [self.presentingViewController beginAppearanceTransition:YES animated:YES]; + } + + [self dismissViewControllerAnimated:YES completion:nil]; + } else { + [self cancelDismissingGesture]; + } + } + break; + default: + [self cancelDismissingGesture]; + break; + } +} + +// 手势判定失败,恢复到手势前的状态 +- (void)cancelDismissingGesture { + self.statusBarHidden = YES; + [UIView animateWithDuration:.2 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + [self setNeedsStatusBarAppearanceUpdate]; + [self resetDismissingGesture]; + } completion:NULL]; +} + +// 清理手势相关的变量 +- (void)resetDismissingGesture { + self.gestureZoomImageView.transform = CGAffineTransformIdentity; + self.gestureBeganLocation = CGPointZero; + self.gestureZoomImageView = nil; + self.view.backgroundColor = self.backgroundColor; +} + +// 不使用 qmui_visibleViewControllerIfExist 是因为不想考虑 presentedViewController +- (UIViewController *)visibleViewControllerWithViewController:(UIViewController *)viewController { + if ([viewController isKindOfClass:[UINavigationController class]]) { + return [self visibleViewControllerWithViewController:((UINavigationController *)viewController).topViewController]; + } + + if ([viewController isKindOfClass:[UITabBarController class]]) { + return [self visibleViewControllerWithViewController:((UITabBarController *)viewController).selectedViewController]; + } + + return viewController; +} + +#pragma mark - + +- (id)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { + return self.transitioningAnimator; +} + +- (id)animationControllerForDismissedController:(UIViewController *)dismissed { + return self.transitioningAnimator; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h new file mode 100644 index 00000000..543aa393 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h @@ -0,0 +1,75 @@ +/** + * 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. + */ + +// +// QMUIImagePreviewViewTransitionAnimator.h +// QMUIKit +// +// Created by MoLice on 2018/D/19. +// + +#import +#import +#import "QMUIImagePreviewViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + 负责处理 QMUIImagePreviewViewController 被 present/dismiss 时的动画,如果需要自定义动画效果,可按需修改 animationEnteringBlock、animationBlock、animationCompletionBlock。 + @see QMUIImagePreviewViewController.transitioningAnimator + */ +@interface QMUIImagePreviewViewTransitionAnimator : NSObject + +/// 当前图片预览控件的引用,在为 QMUIImagePreviewViewController.transitioningAnimator 赋值时会自动建立这个引用关系 +@property(nonatomic, weak) QMUIImagePreviewViewController *imagePreviewViewController; + +/// 转场动画的持续时长,默认为 0.25 +@property(nonatomic, assign) NSTimeInterval duration; + +/// 当 sourceImageView 本身带圆角时,动画过程中会通过这个 layer 来处理圆角的动画 +@property(nonatomic, strong, readonly) CALayer *cornerRadiusMaskLayer; + +/** + 动画开始前的准备工作可以在这里做 + + @param animator 当前的动画器 animator + @param isPresenting YES 表示当前正在 present,NO 表示正在 dismiss + @param style 当前动画的样式 + @param sourceImageRect 原界面上显示图片的 view 在 imagePreviewViewController.view 坐标系里的 rect,仅在 style 为 zoom 时有值,style 为 fade 时为 CGRectZero + @param zoomImageView 当前图片 + @param transitionContext 转场动画的上下文,可通过它获取前后界面、动画容器等信息 + */ +@property(nonatomic, copy) void (^animationEnteringBlock)(__kindof QMUIImagePreviewViewTransitionAnimator *animator, BOOL isPresenting, QMUIImagePreviewViewControllerTransitioningStyle style, CGRect sourceImageRect, QMUIZoomImageView *zoomImageView, id _Nullable transitionContext); + +/** + 转场时的实际动画内容,整个 block 会在一个 UIView animation block 里被调用,因此直接写动画内容即可,无需包裹一个 animation block + + @param animator 当前的动画器 animator + @param isPresenting YES 表示当前正在 present,NO 表示正在 dismiss + @param style 当前动画的样式 + @param sourceImageRect 原界面上显示图片的 view 在 imagePreviewViewController.view 坐标系里的 rect,仅在 style 为 zoom 时有值,style 为 fade 时为 CGRectZero + @param zoomImageView 当前图片 + @param transitionContext 转场动画的上下文,可通过它获取前后界面、动画容器等信息 + */ +@property(nonatomic, copy) void (^animationBlock)(__kindof QMUIImagePreviewViewTransitionAnimator *animator, BOOL isPresenting, QMUIImagePreviewViewControllerTransitioningStyle style, CGRect sourceImageRect, QMUIZoomImageView *zoomImageView, id _Nullable transitionContext); + +/** + 动画结束后的事情,在执行完这个 block 后才会调用 [transitionContext completeTransition:] + + @param animator 当前的动画器 animator + @param isPresenting YES 表示当前正在 present,NO 表示正在 dismiss + @param style 当前动画的样式 + @param sourceImageRect 原界面上显示图片的 view 在 imagePreviewViewController.view 坐标系里的 rect,仅在 style 为 zoom 时有值,style 为 fade 时为 CGRectZero + @param zoomImageView 当前图片 + @param transitionContext 转场动画的上下文,可通过它获取前后界面、动画容器等信息 + */ +@property(nonatomic, copy) void (^animationCompletionBlock)(__kindof QMUIImagePreviewViewTransitionAnimator *animator, BOOL isPresenting, QMUIImagePreviewViewControllerTransitioningStyle style, CGRect sourceImageRect, QMUIZoomImageView *zoomImageView, id _Nullable transitionContext); + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m new file mode 100644 index 00000000..f8c12b26 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m @@ -0,0 +1,219 @@ +/** + * 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. + */ + +// +// QMUIImagePreviewViewTransitionAnimator.m +// QMUIKit +// +// Created by MoLice on 2018/D/19. +// + +#import "QMUIImagePreviewViewTransitionAnimator.h" +#import "QMUICore.h" +#import "CALayer+QMUI.h" + +@implementation QMUIImagePreviewViewTransitionAnimator + +- (instancetype)init { + if (self = [super init]) { + self.duration = .25; + + _cornerRadiusMaskLayer = [CALayer layer]; + [self.cornerRadiusMaskLayer qmui_removeDefaultAnimations]; + self.cornerRadiusMaskLayer.backgroundColor = [UIColor whiteColor].CGColor; + + self.animationEnteringBlock = ^(__kindof QMUIImagePreviewViewTransitionAnimator * _Nonnull animator, BOOL isPresenting, QMUIImagePreviewViewControllerTransitioningStyle style, CGRect sourceImageRect, QMUIZoomImageView * _Nonnull zoomImageView, id _Nullable transitionContext) { + + UIView *previewView = animator.imagePreviewViewController.view; + + if (style == QMUIImagePreviewViewControllerTransitioningStyleFade) { + + previewView.alpha = isPresenting ? 0 : 1; + + } else if (style == QMUIImagePreviewViewControllerTransitioningStyleZoom) { + + CGRect contentViewFrame = [previewView convertRect:zoomImageView.contentViewRectInZoomImageView fromView:nil]; + CGPoint contentViewCenterInZoomImageView = CGPointGetCenterWithRect(zoomImageView.contentViewRectInZoomImageView); + if (CGRectIsEmpty(contentViewFrame)) { + // 有可能 start preview 时图片还在 loading,此时拿到的 content rect 是 zero,所以做个保护 + contentViewFrame = [previewView convertRect:zoomImageView.frame fromView:zoomImageView.superview]; + contentViewCenterInZoomImageView = CGPointGetCenterWithRect(contentViewFrame); + } + CGPoint centerInZoomImageView = CGPointGetCenterWithRect(zoomImageView.bounds);// 注意不是 zoomImageView 的 center,而是 zoomImageView 这个容器里的中心点 + CGFloat horizontalRatio = CGRectGetWidth(sourceImageRect) / CGRectGetWidth(contentViewFrame); + CGFloat verticalRatio = CGRectGetHeight(sourceImageRect) / CGRectGetHeight(contentViewFrame); + CGFloat finalRatio = MAX(horizontalRatio, verticalRatio); + + CGAffineTransform fromTransform = CGAffineTransformIdentity; + CGAffineTransform toTransform = CGAffineTransformIdentity; + CGAffineTransform transform = CGAffineTransformIdentity; + + // 先缩再移 + transform = CGAffineTransformScale(transform, finalRatio, finalRatio); + CGPoint contentViewCenterAfterScale = CGPointMake(centerInZoomImageView.x + (contentViewCenterInZoomImageView.x - centerInZoomImageView.x) * finalRatio, centerInZoomImageView.y + (contentViewCenterInZoomImageView.y - centerInZoomImageView.y) * finalRatio); + CGSize translationAfterScale = CGSizeMake(CGRectGetMidX(sourceImageRect) - contentViewCenterAfterScale.x, CGRectGetMidY(sourceImageRect) - contentViewCenterAfterScale.y); + transform = CGAffineTransformConcat(transform, CGAffineTransformMakeTranslation(translationAfterScale.width, translationAfterScale.height)); + + if (isPresenting) { + fromTransform = transform; + } else { + toTransform = transform; + } + + CGRect maskFromBounds = zoomImageView.contentView.bounds; + CGRect maskToBounds = zoomImageView.contentView.bounds; + CGRect maskBounds = maskFromBounds; + CGFloat maskHorizontalRatio = CGRectGetWidth(sourceImageRect) / CGRectGetWidth(maskBounds); + CGFloat maskVerticalRatio = CGRectGetHeight(sourceImageRect) / CGRectGetHeight(maskBounds); + CGFloat maskFinalRatio = MAX(maskHorizontalRatio, maskVerticalRatio); + maskBounds = CGRectMakeWithSize(CGSizeMake(CGRectGetWidth(sourceImageRect) / maskFinalRatio, CGRectGetHeight(sourceImageRect) / maskFinalRatio)); + if (isPresenting) { + maskFromBounds = maskBounds; + } else { + maskToBounds = maskBounds; + } + + CGFloat cornerRadius = animator.imagePreviewViewController.sourceImageCornerRadius == QMUIImagePreviewViewControllerCornerRadiusAutomaticDimension && animator.imagePreviewViewController.sourceImageView ? animator.imagePreviewViewController.sourceImageView().layer.cornerRadius : MAX(animator.imagePreviewViewController.sourceImageCornerRadius, 0); + cornerRadius = cornerRadius / maskFinalRatio; + CGFloat fromCornerRadius = isPresenting ? cornerRadius : 0; + CGFloat toCornerRadius = isPresenting ? 0 : cornerRadius; + CABasicAnimation *cornerRadiusAnimation = [CABasicAnimation animationWithKeyPath:@"cornerRadius"]; + cornerRadiusAnimation.fromValue = @(fromCornerRadius); + cornerRadiusAnimation.toValue = @(toCornerRadius); + + CABasicAnimation *boundsAnimation = [CABasicAnimation animationWithKeyPath:@"bounds"]; + boundsAnimation.fromValue = [NSValue valueWithCGRect:CGRectMakeWithSize(maskFromBounds.size)]; + boundsAnimation.toValue = [NSValue valueWithCGRect:CGRectMakeWithSize(maskToBounds.size)]; + + CAAnimationGroup *maskAnimation = [[CAAnimationGroup alloc] init]; + maskAnimation.duration = animator.duration; + maskAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + maskAnimation.fillMode = kCAFillModeForwards; + maskAnimation.removedOnCompletion = NO;// remove 都交给 UIView Block 的 completion 里做,这里是为了避免 Core Animation 和 UIView Animation Block 时间不一致导致的值变动 + maskAnimation.animations = @[cornerRadiusAnimation, boundsAnimation]; + animator.cornerRadiusMaskLayer.position = CGPointGetCenterWithRect(zoomImageView.contentView.bounds);// 不管怎样,mask 都是居中的 + zoomImageView.contentView.layer.mask = animator.cornerRadiusMaskLayer; + [animator.cornerRadiusMaskLayer addAnimation:maskAnimation forKey:@"maskAnimation"]; + + // 动画开始 + zoomImageView.scrollView.clipsToBounds = NO;// 当 contentView 被放大后,如果不去掉 clipToBounds,那么退出预览时,contentView 溢出的那部分内容就看不到 + + if (isPresenting) { + zoomImageView.transform = fromTransform; + previewView.backgroundColor = UIColorClear; + } + + // 发现 zoomImageView.transform 用 UIView Animation Block 实现的话,手势拖拽 dismissing 的情况下,松手时会瞬间跳动到某个位置,然后才继续做动画,改为 Core Animation 就没这个问题 + CABasicAnimation *transformAnimation = [CABasicAnimation animationWithKeyPath:@"transform"]; + transformAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeAffineTransform(toTransform)]; + transformAnimation.duration = animator.duration; + transformAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + transformAnimation.fillMode = kCAFillModeForwards; + transformAnimation.removedOnCompletion = NO;// remove 都交给 UIView Block 的 completion 里做,这里是为了避免 Core Animation 和 UIView Animation Block 时间不一致导致的值变动 + [zoomImageView.layer addAnimation:transformAnimation forKey:@"transformAnimation"]; + }; + }; + + self.animationBlock = ^(__kindof QMUIImagePreviewViewTransitionAnimator * _Nonnull animator, BOOL isPresenting, QMUIImagePreviewViewControllerTransitioningStyle style, CGRect sourceImageRect, QMUIZoomImageView * _Nonnull zoomImageView, id _Nullable transitionContext) { + if (style == QMUIImagePreviewViewControllerTransitioningStyleFade) { + animator.imagePreviewViewController.view.alpha = isPresenting ? 1 : 0; + } else if (style == QMUIImagePreviewViewControllerTransitioningStyleZoom) { + animator.imagePreviewViewController.view.backgroundColor = isPresenting ? animator.imagePreviewViewController.backgroundColor : UIColorClear; + } + }; + + self.animationCompletionBlock = ^(__kindof QMUIImagePreviewViewTransitionAnimator * _Nonnull animator, BOOL isPresenting, QMUIImagePreviewViewControllerTransitioningStyle style, CGRect sourceImageRect, QMUIZoomImageView * _Nonnull zoomImageView, id _Nullable transitionContext) { + + // 由于支持 zoom presenting 和 fade dismissing 搭配使用,所以这里不管是哪种 style 都要做相同的清理工作 + + // for fade + animator.imagePreviewViewController.view.alpha = 1; + + // for zoom + [animator.cornerRadiusMaskLayer removeAnimationForKey:@"maskAnimation"]; + zoomImageView.scrollView.clipsToBounds = YES;// UIScrollView.clipsToBounds default is YES + zoomImageView.contentView.layer.mask = nil; + zoomImageView.transform = CGAffineTransformIdentity; + [zoomImageView.layer removeAnimationForKey:@"transformAnimation"]; + }; + } + return self; +} + +#pragma mark - + +- (void)animateTransition:(nonnull id)transitionContext { + if (!self.imagePreviewViewController) { + return; + } + + UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; + UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; + BOOL isPresenting = fromViewController.presentedViewController == toViewController; + UIViewController *presentingViewController = isPresenting ? fromViewController : toViewController; + BOOL shouldAppearanceTransitionManually = self.imagePreviewViewController.modalPresentationStyle != UIModalPresentationFullScreen;// 触发背后界面的生命周期,从而配合屏幕旋转那边做一些强制旋转的操作 + + QMUIImagePreviewViewControllerTransitioningStyle style = isPresenting ? self.imagePreviewViewController.presentingStyle : self.imagePreviewViewController.dismissingStyle; + CGRect sourceImageRect = CGRectZero; + if (style == QMUIImagePreviewViewControllerTransitioningStyleZoom) { + if (self.imagePreviewViewController.sourceImageRect) { + sourceImageRect = [self.imagePreviewViewController.view convertRect:self.imagePreviewViewController.sourceImageRect() fromView:nil]; + } else if (self.imagePreviewViewController.sourceImageView) { + UIView *sourceImageView = self.imagePreviewViewController.sourceImageView(); + if (sourceImageView) { + sourceImageRect = [self.imagePreviewViewController.view convertRect:sourceImageView.frame fromView:sourceImageView.superview]; + } + } + if (!CGRectEqualToRect(sourceImageRect, CGRectZero) && !CGRectIntersectsRect(sourceImageRect, self.imagePreviewViewController.view.bounds)) { + sourceImageRect = CGRectZero; + } + } + style = style == QMUIImagePreviewViewControllerTransitioningStyleZoom && CGRectEqualToRect(sourceImageRect, CGRectZero) ? QMUIImagePreviewViewControllerTransitioningStyleFade : style;// zoom 类型一定需要有个非 zero 的 sourceImageRect,否则不知道动画的起点/终点,所以当不存在 sourceImageRect 时强制改为用 fade 动画 + + 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; + if (isPresenting) { + [containerView addSubview:toView]; + if (shouldAppearanceTransitionManually) { + [presentingViewController beginAppearanceTransition:NO animated:YES]; + } + } else { + [containerView insertSubview:toView belowSubview:fromView]; + [presentingViewController beginAppearanceTransition:YES animated:YES]; + } + + if (self.animationEnteringBlock) { + self.animationEnteringBlock(self, isPresenting, style, sourceImageRect, zoomImageView, transitionContext); + } + + [UIView animateWithDuration:self.duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + if (self.animationBlock) { + self.animationBlock(self, isPresenting, style, sourceImageRect, zoomImageView, transitionContext); + } + } completion:^(BOOL finished) { + [presentingViewController endAppearanceTransition]; + [transitionContext completeTransition:!transitionContext.transitionWasCancelled]; + if (self.animationCompletionBlock) { + self.animationCompletionBlock(self, isPresenting, style, sourceImageRect, zoomImageView, transitionContext); + } + }]; +} + +- (NSTimeInterval)transitionDuration:(nullable id)transitionContext { + return self.duration; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.h b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.h new file mode 100644 index 00000000..b7da3997 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.h @@ -0,0 +1,271 @@ +/** + * 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. + */ + +// +// QMUIKeyboardManager.h +// qmui +// +// Created by QMUI Team on 2017/3/23. +// + +#import +#import + +@protocol QMUIKeyboardManagerDelegate; +@class QMUIKeyboardUserInfo; + +/** + * `QMUIKeyboardManager` 提供了方便、稳定的管理键盘事件的方案,使用的场景是需要跟随键盘的显示或者隐藏来更改界面的 UI,例如输入框跟随在键盘的顶部。 + * 由于键盘通知是整个 App 全局的,所以经常会遇到 A 的键盘监听回调里接收到 B 的键盘事件,这样的情况往往不是我们想要的,即使可以通过判断当前的 firstResponder 来区分,但还是不能完美的解决问题或者有时候解决起来非常麻烦。`QMUIKeyboardManager` 通过 `delegateEnabled` 和 `targetResponder` 等属性来方便地控制 firstResponder,从而可以实现某个键盘监听回调方法只响应某个 UIResponder 或者某几个 UIResponder 触发的键盘通知。 + * 另外系统的“设置→辅助功能→动态效果→减弱动态效果→首选交叉淡出过渡效果”会改变系统键盘的动画,QMUIKeyboardManager 也兼容了这种情况,如果业务自己处理,很容易会遗漏。 + * + * 使用方式: + * 1. 使用 initWithDelegate: 方法初始化 + * 2. 通过 addTargetResponder: 的方式将要监听的输入框添加进来 + * 3. 在 delegate 方法里(一般用 keyboardWillChangeFrameWithUserInfo:)处理键盘位置变化时的布局 + * + * 另外 QMUIKeyboardManager 同时集成在了 UITextField(QMUI) 和 UITextView(QMUI) 里,具体请查看对应文件。 + * @see UITextField(QMUI) + * @see UITextView(QMUI) + */ +@interface QMUIKeyboardManager : NSObject + +/** + * 指定初始化方法,以 delegate 的方式将键盘事件传递给监听者 + */ +- (instancetype)initWithDelegate:(id)delegate NS_DESIGNATED_INITIALIZER; + +/** + * 获取当前的 delegate + */ +@property(nonatomic, weak, readonly) id delegate; + +/** + * 是否允许触发delegate的回调,常见的场景例如在 UIViewController viewWillAppear: 里打开,在 viewWillDisappear: 里关闭,从而避免在键盘升起的状态下手势返回时界面布局会跟着键盘往下移动。 + * 默认为 YES。 + */ +@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。 + */ +- (BOOL)addTargetResponder:(UIResponder *)targetResponder; + +/** + * 获取当前所有的 target UIResponder,若不存在则返回 nil + */ +- (NSArray *)allTargetResponders; + +/** + * 移除 targetResponder 跟 keyboardManager 的关系,如果成功会返回 YES + */ +- (BOOL)removeTargetResponder:(UIResponder *)targetResponder; + +/** + * 把键盘的rect转为相对于view的rect。一般用来把键盘的rect转化为相对于当前 self.view 的 rect,然后获取 y 值来布局对应的 view(这里一般不要获取键盘的高度,因为对于iPad的键盘,浮动状态下键盘的高度往往不是我们想要的)。 + * @param rect 键盘的rect,一般拿 keyboardUserInfo.endFrame + * @param view 一个特定的view或者window,如果传入nil则相对有当前的 mainWindow + */ ++ (CGRect)convertKeyboardRect:(CGRect)rect toView:(UIView *)view; + +/** + * 获取键盘到顶部到相对于view底部的距离,这个值在某些情况下会等于endFrame.size.height或者visibleKeyboardHeight,不过在iPad浮动键盘的时候就包括了底部的空隙。所以建议使用这个方法。 + */ ++ (CGFloat)distanceFromMinYToBottomInView:(UIView *)view keyboardRect:(CGRect)rect; + +/** + * 根据键盘的动画参数自己构建一个动画,调用者只需要设置view的位置即可 + */ ++ (void)animateWithAnimated:(BOOL)animated keyboardUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion; + +/** + * 这个方法特殊处理 iPad Pro 外接键盘的情况。使用外接键盘在完全不显示键盘的时候,不会调用willShow的通知,所以导致一些通过willShow回调来显示targetResponder的场景(例如微信朋友圈的评论输入框)无法把targetResponder正常的显示出来。通过这个方法,你只需要关心你的show和hide的状态就好了,不需要关心是否 iPad Pro 的情况。 + * @param showBlock 键盘显示回调的block,不能把showBlock理解为系统的show通知,而是你有输入框聚焦了并且期望键盘显示出来。 + * @param hideBlock 键盘隐藏回调的block,不能把hideBlock理解为系统的hide通知,而是键盘即将消失在界面上并且你期望跟随键盘变化的UI回到默认状态。 + */ ++ (void)handleKeyboardNotificationWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo showBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))showBlock hideBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))hideBlock; + +/** + * 键盘面板的私有view,可能为nil + */ ++ (UIView *)keyboardView; + +/** + * 键盘面板所在的私有window,可能为nil + */ ++ (UIWindow *)keyboardWindow; + +/** + * 是否有键盘在显示 + */ ++ (BOOL)isKeyboardVisible; + +/** + * 当期那键盘相对于屏幕的frame + */ ++ (CGRect)currentKeyboardFrame; + +/** + * 当前键盘高度键盘的可见高度 + */ ++ (CGFloat)visibleKeyboardHeight; + +@end + + +@interface QMUIKeyboardUserInfo : NSObject + +/** + * 所在的KeyboardManager + */ +@property(nonatomic, weak, readonly) QMUIKeyboardManager *keyboardManager; + +/** + * 当前键盘的notification + */ +@property(nonatomic, strong, readonly) NSNotification *notification; + +/** + * notification自带的userInfo + */ +@property(nonatomic, strong, readonly) NSDictionary *originUserInfo; + +/** + * 触发键盘事件的UIResponder,注意这里的 `targetResponder` 不一定是通过 `addTargetResponder:` 添加的 UIResponder,而是当前触发键盘事件的 UIResponder。 + */ +@property(nonatomic, weak, readonly) UIResponder *targetResponder; + +/** + * 获取键盘实际宽度 + */ +@property(nonatomic, assign, readonly) CGFloat width; + +/** + * 获取键盘的实际高度 + */ +@property(nonatomic, assign, readonly) CGFloat height; + +/** + * 获取当前键盘在view上的可见高度,也就是键盘和view重叠的高度。如果view=nil,则直接返回键盘的实际高度。 + */ +- (CGFloat)heightInView:(UIView *)view; + +/** + * 获取键盘beginFrame + */ +@property(nonatomic, assign, readonly) CGRect beginFrame; + +/** + * 获取键盘endFrame + */ +@property(nonatomic, assign, readonly) CGRect endFrame; + +/** + * 获取键盘出现动画的duration,对于第三方键盘,这个值有可能为0 + */ +@property(nonatomic, assign, readonly) NSTimeInterval animationDuration; + +/** + * 获取键盘动画的Curve参数 + */ +@property(nonatomic, assign, readonly) UIViewAnimationCurve animationCurve; + +/** + * 获取键盘动画的Options参数 + */ +@property(nonatomic, assign, readonly) UIViewAnimationOptions animationOptions; + +/** + * 当前是否浮动键盘 + */ +@property(nonatomic, assign, readonly) BOOL isFloatingKeyboard; + +@end + + +/** + * `QMUIKeyboardManagerDelegate`里面的方法是对应系统键盘通知的回调方法,具体请看delegate名字,`QMUIKeyboardUserInfo`是对系统的userInfo做了一个封装,可以方便的获取userInfo的属性值。 + */ +@protocol QMUIKeyboardManagerDelegate + +@optional + +/** + * 键盘即将显示 + */ +- (void)keyboardWillShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; + +/** + * 键盘即将隐藏 + */ +- (void)keyboardWillHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; + +/** + * 键盘frame即将发生变化。 + * 这个delegate除了对应系统的willChangeFrame通知外,在iPad下还增加了监听键盘frame变化的KVO来处理浮动键盘,所以调用次数会比系统默认多。需要让界面或者某个view跟随键盘运动,建议在这个通知delegate里面实现,因为willShow和willHide在手机上是准确的,但是在iPad的浮动键盘下是不准确的。另外,如果不需要跟随浮动键盘运动,那么在逻辑代码里面可以通过判断键盘的位置来过滤这种浮动的情况。 + */ +- (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; + +/** + * 键盘已经显示 + */ +- (void)keyboardDidShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; + +/** + * 键盘已经隐藏 + */ +- (void)keyboardDidHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; + +/** + * 键盘frame已经发生变化。 + */ +- (void)keyboardDidChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; + +@end + +@interface UIResponder (KeyboardManager) + +/// 持有KeyboardManager对象 +@property(nonatomic, strong) QMUIKeyboardManager *qmui_keyboardManager; + +@end + +@interface UITextField (QMUI_KeyboardManager) + +/// 键盘相关block,搭配QMUIKeyboardManager一起使用 + +@property(nonatomic, copy) void (^qmui_keyboardWillShowNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); +@property(nonatomic, copy) void (^qmui_keyboardWillHideNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); +@property(nonatomic, copy) void (^qmui_keyboardWillChangeFrameNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); +@property(nonatomic, copy) void (^qmui_keyboardDidShowNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); +@property(nonatomic, copy) void (^qmui_keyboardDidHideNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); +@property(nonatomic, copy) void (^qmui_keyboardDidChangeFrameNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); + +@end + +@interface UITextView (QMUI_KeyboardManager) + +/// 键盘相关block,搭配QMUIKeyboardManager一起使用 + +@property(nonatomic, copy) void (^qmui_keyboardWillShowNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); +@property(nonatomic, copy) void (^qmui_keyboardWillHideNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); +@property(nonatomic, copy) void (^qmui_keyboardWillChangeFrameNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); +@property(nonatomic, copy) void (^qmui_keyboardDidShowNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); +@property(nonatomic, copy) void (^qmui_keyboardDidHideNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); +@property(nonatomic, copy) void (^qmui_keyboardDidChangeFrameNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.m b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.m new file mode 100644 index 00000000..178df3ec --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIKeyboardManager.m @@ -0,0 +1,1205 @@ +/** + * 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. + */ + +// +// QMUIKeyboardManager.m +// qmui +// +// Created by QMUI Team on 2017/3/23. +// + +#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 + +@interface QMUIKeyboardManager () + +@property(nonatomic, strong) NSMutableArray *targetResponderValues; + +@property(nonatomic, strong) QMUIKeyboardUserInfo *lastUserInfo; +@property(nonatomic, assign) CGRect keyboardMoveBeginRect; + +@property(nonatomic, weak) UIResponder *currentResponder; +//@property(nonatomic, weak) UIResponder *currentResponderWhenResign; + +@property(nonatomic, assign) BOOL debug; + +@end + + +@interface UIView (KeyboardManager) + +- (id)qmui_findFirstResponder; + +@end + +@implementation UIView (KeyboardManager) + +- (id)qmui_findFirstResponder { + if (self.isFirstResponder) { + return self; + } + for (UIView *subView in self.subviews) { + id responder = [subView qmui_findFirstResponder]; + if (responder) return responder; + } + return nil; +} + +@end + + +@interface UIResponder () + +/// 系统自己的isFirstResponder有延迟,这里手动记录UIResponder是否isFirstResponder,QMUIKeyboardManager内部自己使用 +@property(nonatomic, assign) BOOL keyboardManager_isFirstResponder; +@end + + +@implementation UIResponder (KeyboardManager) + +QMUISynthesizeIdStrongProperty(qmui_keyboardManager, setQmui_keyboardManager) +QMUISynthesizeBOOLProperty(keyboardManager_isFirstResponder, setKeyboardManager_isFirstResponder) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + OverrideImplementation([UIResponder class], @selector(becomeFirstResponder), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(UIResponder *selfObject) { + selfObject.keyboardManager_isFirstResponder = YES; + + // call super + BOOL (*originSelectorIMP)(id, SEL); + originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD); + + return result; + }; + }); + + OverrideImplementation([UIResponder class], @selector(resignFirstResponder), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^BOOL(UIResponder *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(); + BOOL result = originSelectorIMP(selfObject, originCMD); + + return result; + }; + }); + }); +} + +@end + + +@interface QMUIKeyboardViewFrameObserver : NSObject + +@property (nonatomic, weak) id delegate; +- (void)addToKeyboardView:(UIView *)keyboardView; ++ (instancetype)observerForView:(UIView *)keyboardView; + +@end + +static char kAssociatedObjectKey_KeyboardViewFrameObserver; + +@implementation QMUIKeyboardViewFrameObserver { + __unsafe_unretained UIView *_keyboardView; +} + +- (void)addToKeyboardView:(UIView *)keyboardView { + if (_keyboardView == keyboardView) { + return; + } + if (_keyboardView) { + [self removeFrameObserver]; + objc_setAssociatedObject(_keyboardView, &kAssociatedObjectKey_KeyboardViewFrameObserver, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + _keyboardView = keyboardView; + if (keyboardView) { + [self addFrameObserver]; + } + objc_setAssociatedObject(keyboardView, &kAssociatedObjectKey_KeyboardViewFrameObserver, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (void)addFrameObserver { + if (!_keyboardView) { + return; + } + [_keyboardView addObserver:self forKeyPath:@"frame" options:kNilOptions context:NULL]; + [_keyboardView addObserver:self forKeyPath:@"center" options:kNilOptions context:NULL]; + [_keyboardView addObserver:self forKeyPath:@"bounds" options:kNilOptions context:NULL]; + [_keyboardView addObserver:self forKeyPath:@"transform" options:kNilOptions context:NULL]; +} + +- (void)removeFrameObserver { + [_keyboardView removeObserver:self forKeyPath:@"frame"]; + [_keyboardView removeObserver:self forKeyPath:@"center"]; + [_keyboardView removeObserver:self forKeyPath:@"bounds"]; + [_keyboardView removeObserver:self forKeyPath:@"transform"]; + _keyboardView = nil; +} + +- (void)dealloc { + [self removeFrameObserver]; +} + ++ (instancetype)observerForView:(UIView *)keyboardView { + if (!keyboardView) { + return nil; + } + return objc_getAssociatedObject(keyboardView, &kAssociatedObjectKey_KeyboardViewFrameObserver); +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (![keyPath isEqualToString:@"frame"] && + ![keyPath isEqualToString:@"center"] && + ![keyPath isEqualToString:@"bounds"] && + ![keyPath isEqualToString:@"transform"]) { + return; + } + if ([[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue]) { + return; + } + if ([[change objectForKey:NSKeyValueChangeKindKey] integerValue] != NSKeyValueChangeSetting) { + return; + } + id newValue = [change objectForKey:NSKeyValueChangeNewKey]; + if (newValue == [NSNull null]) { newValue = nil; } + if (self.delegate) { + [self.delegate keyboardViewFrameDidChange:_keyboardView]; + } +} + +@end + + +@interface QMUIKeyboardUserInfo () + +@property(nonatomic, weak, readwrite) QMUIKeyboardManager *keyboardManager; +@property(nonatomic, strong, readwrite) NSNotification *notification; +@property(nonatomic, weak, readwrite) UIResponder *targetResponder; +@property(nonatomic, assign) BOOL isTargetResponderFocused; + +@property(nonatomic, assign, readwrite) CGFloat width; +@property(nonatomic, assign, readwrite) CGFloat height; + +@property(nonatomic, assign, readwrite) CGRect beginFrame; +@property(nonatomic, assign, readwrite) CGRect endFrame; + +@property(nonatomic, assign, readwrite) NSTimeInterval animationDuration; +@property(nonatomic, assign, readwrite) UIViewAnimationCurve animationCurve; +@property(nonatomic, assign, readwrite) UIViewAnimationOptions animationOptions; + +@property(nonatomic, assign, readwrite) BOOL isFloatingKeyboard; + +@end + +@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; + + 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; + } +} + +- (void)setTargetResponder:(UIResponder *)targetResponder { + _targetResponder = targetResponder; + self.isTargetResponderFocused = targetResponder && targetResponder.keyboardManager_isFirstResponder; +} + +- (NSDictionary *)originUserInfo { + return self.notification ? self.notification.userInfo : nil; +} + +- (CGFloat)width { + CGRect keyboardRect = [QMUIKeyboardManager convertKeyboardRect:_endFrame toView:nil]; + return keyboardRect.size.width; +} + +- (CGFloat)height { + CGRect keyboardRect = [QMUIKeyboardManager convertKeyboardRect:_endFrame toView:nil]; + return keyboardRect.size.height; +} + +- (CGFloat)heightInView:(UIView *)view { + if (!view) { + return [self height]; + } + CGRect keyboardRect = [QMUIKeyboardManager convertKeyboardRect:_endFrame toView:view]; + CGRect visibleRect = CGRectIntersection(CGRectFlatted(view.bounds), CGRectFlatted(keyboardRect)); + if (!CGRectIsValidated(visibleRect)) { + return 0; + } + return visibleRect.size.height; +} + +- (CGRect)beginFrame { + return _beginFrame; +} + +- (CGRect)endFrame { + return _endFrame; +} + +- (NSTimeInterval)animationDuration { + return _animationDuration; +} + +- (UIViewAnimationCurve)animationCurve { + return _animationCurve; +} + +- (UIViewAnimationOptions)animationOptions { + return _animationOptions; +} + +@end + + +/** + 1. 系统键盘app启动第一次使用键盘的时候,会调用两轮键盘通知事件,之后就只会调用一次。而搜狗等第三方输入法的键盘,目前发现每次都会调用三次键盘通知事件。总之,键盘的通知事件是不确定的。 + + 2. 搜狗键盘可以修改键盘的高度,在修改键盘高度之后,会调用键盘的keyboardWillChangeFrameNotification和keyboardWillShowNotification通知。 + + 3. 如果从一个聚焦的输入框直接聚焦到另一个输入框,会调用前一个输入框的keyboardWillChangeFrameNotification,在调用后一个输入框的keyboardWillChangeFrameNotification,最后调用后一个输入框的keyboardWillShowNotification(如果此时是浮动键盘,那么后一个输入框的keyboardWillShowNotification不会被调用;)。 + + 4. iPad可以变成浮动键盘,固定->浮动:会调用keyboardWillChangeFrameNotification和keyboardWillHideNotification;浮动->固定:会调用keyboardWillChangeFrameNotification和keyboardWillShowNotification;浮动键盘在移动的时候只会调用keyboardWillChangeFrameNotification通知,并且endFrame为zero,fromFrame不为zero,而是移动前键盘的frame。浮动键盘在聚焦和失焦的时候只会调用keyboardWillChangeFrameNotification,不会调用show和hide的notification。 + + 5. iPad可以拆分为左右的小键盘,小键盘的通知具体基本跟浮动键盘一样。 + + 6. iPad可以外接键盘,外接键盘之后屏幕上就没有虚拟键盘了,但是当我们输入文字的时候,发现底部还是有一条灰色的候选词,条东西也是键盘,它也会触发跟虚拟键盘一样的通知事件。如果点击这条候选词右边的向下箭头,则可以完全隐藏虚拟键盘,这个时候如果失焦再聚焦发现还是没有这条候选词,也就是键盘完全不出来了,如果输入文字,候选词才会重新出来。总结来说就是这条候选词是可以关闭的,关闭之后只有当下次输入才会重新出现。(聚焦和失焦都只调用keyboardWillChangeFrameNotification和keyboardWillHideNotification通知,而且frame始终不变,都是在屏幕下面) + + 7. iOS8 hide 之后高度变成0了,keyboardWillHideNotification还是正常的,所以建议不要使用键盘高度来做动画,而是用键盘的y值;在show和hide的时候endFrame会出现一些奇怪的中间值,但最终值是对的;两个输入框切换聚焦,iOS8不会触发任何键盘通知;iOS8的浮动切换正常; + + 8. iOS8在 固定->浮动 的过程中,后面的keyboardWillChangeFrameNotification和keyboardWillHideNotification里面的endFrame是正确的,而iOS10和iOS9是错的,iOS9的y值是键盘的MaxY,而iOS10的y值是隐藏状态下的y,也就是屏幕高度。所以iOS9和iOS10需要在keyboardDidChangeFrameNotification里面重新刷新一下。 + */ +@implementation QMUIKeyboardManager + ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + +- (instancetype)init { + NSAssert(NO, @"请使用initWithDelegate:初始化"); + return [self initWithDelegate:nil]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + NSAssert(NO, @"请使用initWithDelegate:初始化"); + return [self initWithDelegate:nil]; +} + +- (instancetype)initWithDelegate:(id )delegate { + if (self = [super init]) { + _delegate = delegate; + _delegateEnabled = YES; + _targetResponderValues = [[NSMutableArray alloc] init]; + [self addKeyboardNotification]; + [self qmui_applyAppearance]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (BOOL)addTargetResponder:(UIResponder *)targetResponder { + if (!targetResponder || ![targetResponder isKindOfClass:[UIResponder class]]) { + return NO; + } + targetResponder.qmui_keyboardManager = self; + [self.targetResponderValues addObject:[self packageTargetResponder:targetResponder]]; + return YES; +} + +- (NSArray *)allTargetResponders { + NSMutableArray *targetResponders = nil; + for (int i = 0; i < self.targetResponderValues.count; i++) { + if (!targetResponders) { + targetResponders = [[NSMutableArray alloc] init]; + } + id unPackageValue = [self unPackageTargetResponder:self.targetResponderValues[i]]; + if (unPackageValue && [unPackageValue isKindOfClass:[UIResponder class]]) { + [targetResponders addObject:(UIResponder *)unPackageValue]; + } + } + return [targetResponders copy]; +} + +- (BOOL)removeTargetResponder:(UIResponder *)targetResponder { + if (targetResponder && [self.targetResponderValues containsObject:[self packageTargetResponder:targetResponder]]) { + [self.targetResponderValues removeObject:[self packageTargetResponder:targetResponder]]; + return YES; + } + return NO; +} + +- (NSValue *)packageTargetResponder:(UIResponder *)targetResponder { + if (![targetResponder isKindOfClass:[UIResponder class]]) { + return nil; + } + return [NSValue valueWithNonretainedObject:targetResponder]; +} + +- (UIResponder *)unPackageTargetResponder:(NSValue *)value { + if (!value) { + return nil; + } + id unPackageValue = [value nonretainedObjectValue]; + if (![unPackageValue isKindOfClass:[UIResponder class]]) { + return nil; + } + return (UIResponder *)unPackageValue; +} + +- (UIResponder *)firstResponderInWindows { + UIResponder *responder = [UIApplication.sharedApplication.keyWindow qmui_findFirstResponder]; + if (!responder) { + for (UIWindow *window in UIApplication.sharedApplication.windows) { + if (window != UIApplication.sharedApplication.keyWindow) { + responder = [window qmui_findFirstResponder]; + if (responder) { + return responder; + } + } + } + } + return responder; +} + +#pragma mark - Notification + +- (void)addKeyboardNotification { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShowNotification:) name:UIKeyboardDidShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHideNotification:) name:UIKeyboardDidHideNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrameNotification:) name:UIKeyboardWillChangeFrameNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidChangeFrameNotification:) name:UIKeyboardDidChangeFrameNotification object:nil]; +} + +- (BOOL)isAppActive { + if (self.ignoreApplicationState) { + return YES; + } + if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive) { + 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 { + + if (self.debug) { + QMUILog(NSStringFromClass(self.class), @"keyboardWillShowNotification - %@", self); + } + + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { + QMUILog(NSStringFromClass(self.class), @"app is not active"); + return; + } + + if (![self shouldReceiveShowNotification]) { + return; + } + + QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; + self.lastUserInfo = userInfo; + userInfo.targetResponder = self.currentResponder ?: nil; + + if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardWillShowWithUserInfo:)]) { + [self.delegate keyboardWillShowWithUserInfo:userInfo]; + } + + // 额外处理iPad浮动键盘 + if (IS_IPAD) { + [self keyboardDidChangedFrame:[self.class keyboardView]]; + } +} + +- (void)keyboardDidShowNotification:(NSNotification *)notification { + + if (self.debug) { + QMUILog(NSStringFromClass(self.class), @"keyboardDidShowNotification - %@", self); + } + + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { + QMUILog(NSStringFromClass(self.class), @"app is not active"); + return; + } + + QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; + self.lastUserInfo = userInfo; + userInfo.targetResponder = self.currentResponder ?: nil; + + id firstResponder = [self firstResponderInWindows]; + BOOL shouldReceiveDidShowNotification = self.targetResponderValues.count <= 0 || (firstResponder && firstResponder == self.currentResponder); + + if (shouldReceiveDidShowNotification) { + if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardDidShowWithUserInfo:)]) { + [self.delegate keyboardDidShowWithUserInfo:userInfo]; + } + // 额外处理iPad浮动键盘 + if (IS_IPAD) { + [self keyboardDidChangedFrame:[self.class keyboardView]]; + } + } +} + +- (void)keyboardWillHideNotification:(NSNotification *)notification { + + if (self.debug) { + QMUILog(NSStringFromClass(self.class), @"keyboardWillHideNotification - %@", self); + } + + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { + QMUILog(NSStringFromClass(self.class), @"app is not active"); + return; + } + + if (![self shouldReceiveHideNotification]) { + return; + } + + QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; + self.lastUserInfo = userInfo; + userInfo.targetResponder = self.currentResponder ?: nil; + + if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardWillHideWithUserInfo:)]) { + [self.delegate keyboardWillHideWithUserInfo:userInfo]; + } + + // 额外处理iPad浮动键盘 + if (IS_IPAD) { + [self keyboardDidChangedFrame:[self.class keyboardView]]; + } +} + +- (void)keyboardDidHideNotification:(NSNotification *)notification { + + if (self.debug) { + QMUILog(NSStringFromClass(self.class), @"keyboardDidHideNotification - %@", self); + } + + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { + QMUILog(NSStringFromClass(self.class), @"app is not active"); + return; + } + + QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; + self.lastUserInfo = userInfo; + userInfo.targetResponder = self.currentResponder ?: nil; + + if ([self shouldReceiveHideNotification]) { + if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardDidHideWithUserInfo:)]) { + [self.delegate keyboardDidHideWithUserInfo:userInfo]; + } + } + + if (self.currentResponder && !self.currentResponder.keyboardManager_isFirstResponder && !IS_IPAD) { + // 时机最晚,设置为 nil + self.currentResponder = nil; + } + + // 额外处理iPad浮动键盘 + if (IS_IPAD) { + if (self.targetResponderValues.count <= 0 || self.currentResponder) { + [self keyboardDidChangedFrame:[self.class keyboardView]]; + } + } +} + +- (void)keyboardWillChangeFrameNotification:(NSNotification *)notification { + + if (self.debug) { + QMUILog(NSStringFromClass(self.class), @"keyboardWillChangeFrameNotification - %@", self); + } + + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { + QMUILog(NSStringFromClass(self.class), @"app is not active"); + return; + } + + QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; + self.lastUserInfo = userInfo; + + if ([self shouldReceiveShowNotification] || [self shouldReceiveHideNotification]) { + userInfo.targetResponder = self.currentResponder ?: nil; + } else { + return; + } + + if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardWillChangeFrameWithUserInfo:)]) { + [self.delegate keyboardWillChangeFrameWithUserInfo:userInfo]; + } + + // 额外处理iPad浮动键盘 + if (IS_IPAD) { + [self addFrameObserverIfNeeded]; + } +} + +- (void)keyboardDidChangeFrameNotification:(NSNotification *)notification { + + if (self.debug) { + QMUILog(NSStringFromClass(self.class), @"keyboardDidChangeFrameNotification - %@", self); + } + + if (![self isAppActive] || ![self isLocalKeyboard:notification]) { + QMUILog(NSStringFromClass(self.class), @"app is not active"); + return; + } + + QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; + self.lastUserInfo = userInfo; + + if ([self shouldReceiveShowNotification] || [self shouldReceiveHideNotification]) { + userInfo.targetResponder = self.currentResponder ?: nil; + } else { + return; + } + + if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardDidChangeFrameWithUserInfo:)]) { + [self.delegate keyboardDidChangeFrameWithUserInfo:userInfo]; + } + + // 额外处理iPad浮动键盘 + if (IS_IPAD) { + [self keyboardDidChangedFrame:[self.class keyboardView]]; + } +} + +- (QMUIKeyboardUserInfo *)newUserInfoWithNotification:(NSNotification *)notification { + QMUIKeyboardUserInfo *userInfo = [[QMUIKeyboardUserInfo alloc] init]; + userInfo.keyboardManager = self; + userInfo.notification = notification; + return userInfo; +} + +- (BOOL)shouldReceiveShowNotification { + 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 { + return self.currentResponder && [self.targetResponderValues containsObject:[self packageTargetResponder:self.currentResponder]]; + } +} + +- (BOOL)shouldReceiveHideNotification { + if (self.targetResponderValues.count <= 0) { + return YES; + } else { + if (self.currentResponder) { + return [self.targetResponderValues containsObject:[self packageTargetResponder:self.currentResponder]]; + } else { + return NO; + } + } +} + +#pragma mark - iPad浮动键盘 + +- (void)addFrameObserverIfNeeded { + if (![self.class keyboardView]) { + return; + } + UIView *keyboardView = [self.class keyboardView]; + QMUIKeyboardViewFrameObserver *observer = [QMUIKeyboardViewFrameObserver observerForView:keyboardView]; + if (!observer) { + observer = [[QMUIKeyboardViewFrameObserver alloc] init]; + observer.qmui_multipleDelegatesEnabled = YES; + [observer addToKeyboardView:keyboardView]; + } + observer.delegate = self; + [self keyboardDidChangedFrame:keyboardView]; // 手动调用第一次 +} + +- (void)keyboardDidChangedFrame:(UIView *)keyboardView { + + if (keyboardView != [self.class keyboardView]) { + return; + } + + // 也需要判断targetResponder + if (![self shouldReceiveShowNotification] && ![self shouldReceiveHideNotification]) { + return; + } + + if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardWillChangeFrameWithUserInfo:)]) { + + UIWindow *keyboardWindow = keyboardView.window; + + if (self.keyboardMoveBeginRect.size.width == 0 && self.keyboardMoveBeginRect.size.height == 0) { + // 第一次需要初始化 + self.keyboardMoveBeginRect = CGRectMake(0, keyboardWindow.bounds.size.height, keyboardWindow.bounds.size.width, 0); + } + + CGRect endFrame = CGRectZero; + if (keyboardWindow) { + endFrame = [keyboardWindow convertRect:keyboardView.frame toWindow:nil]; + } else { + endFrame = keyboardView.frame; + } + + // 自己构造一个QMUIKeyboardUserInfo,一些属性使用之前最后一个keyboardUserInfo的值 + QMUIKeyboardUserInfo *keyboardMoveUserInfo = [[QMUIKeyboardUserInfo alloc] init]; + keyboardMoveUserInfo.keyboardManager = self; + keyboardMoveUserInfo.targetResponder = self.lastUserInfo ? self.lastUserInfo.targetResponder : nil; + keyboardMoveUserInfo.animationDuration = self.lastUserInfo ? self.lastUserInfo.animationDuration : 0.25; + keyboardMoveUserInfo.animationCurve = self.lastUserInfo ? self.lastUserInfo.animationCurve : 7; + 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 - %@\n", self); + } + + [self.delegate keyboardWillChangeFrameWithUserInfo:keyboardMoveUserInfo]; + + self.keyboardMoveBeginRect = endFrame; + + if (self.currentResponder) { + UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.delegate.window; + if (mainWindow) { + CGRect keyboardRect = keyboardMoveUserInfo.endFrame; + CGFloat distanceFromBottom = [QMUIKeyboardManager distanceFromMinYToBottomInView:mainWindow keyboardRect:keyboardRect]; + if (distanceFromBottom < keyboardRect.size.height) { + if (!self.currentResponder.keyboardManager_isFirstResponder) { + // willHide + self.currentResponder = nil; + } + } else if (distanceFromBottom > keyboardRect.size.height && !self.currentResponder.isFirstResponder) { + if (!self.currentResponder.keyboardManager_isFirstResponder) { + // 浮动 + self.currentResponder = nil; + } + } + } + } + + } +} + +#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 { + if (animated) { + [UIView animateWithDuration:keyboardUserInfo.animationDuration delay:0 options:keyboardUserInfo.animationOptions|UIViewAnimationOptionBeginFromCurrentState animations:^{ + if (animations) { + animations(); + } + } completion:^(BOOL finished) { + if (completion) { + completion(finished); + } + }]; + } else { + if (animations) { + animations(); + } + if (completion) { + completion(YES); + } + } +} + ++ (void)handleKeyboardNotificationWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo showBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))showBlock hideBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))hideBlock { + // 专门处理 iPad Pro 在键盘完全不显示的情况(不会调用willShow,所以通过是否focus来判断) + // iPhoneX Max 这里键盘高度不是0,而是一个很小的值 + if (!keyboardUserInfo.isTargetResponderFocused) { + // 先判断 focus,避免 frame 变化但是此时 visibleKeyboardHeight 还不是 0 导致调用了 showBlock + if ([QMUIKeyboardManager visibleKeyboardHeight] <= 0) { + if (hideBlock) { + hideBlock(keyboardUserInfo); + } + } + } else { + if (showBlock) { + showBlock(keyboardUserInfo); + } + } +} + ++ (CGRect)convertKeyboardRect:(CGRect)rect toView:(UIView *)view { + + if (CGRectIsNull(rect) || CGRectIsInfinite(rect)) { + return rect; + } + + UIWindow *mainWindow = UIApplication.sharedApplication.keyWindow ?: UIApplication.sharedApplication.delegate.window; + if (!mainWindow) { + if (view) { + [view convertRect:rect fromView:nil]; + } else { + return rect; + } + } + + rect = [mainWindow convertRect:rect fromWindow:nil]; + if (!view) { + return [mainWindow convertRect:rect toWindow:nil]; + } + if (view == mainWindow) { + return rect; + } + + UIWindow *toWindow = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window; + if (!mainWindow || !toWindow) { + return [mainWindow convertRect:rect toView:view]; + } + if (mainWindow == toWindow) { + return [mainWindow convertRect:rect toView:view]; + } + + rect = [mainWindow convertRect:rect toView:mainWindow]; + rect = [toWindow convertRect:rect fromWindow:mainWindow]; + rect = [view convertRect:rect fromView:toWindow]; + + return rect; +} + ++ (CGFloat)distanceFromMinYToBottomInView:(UIView *)view keyboardRect:(CGRect)rect { + rect = [self convertKeyboardRect:rect toView:view]; + CGFloat distance = CGRectGetHeight(CGRectFlatted(view.bounds)) - CGRectGetMinY(rect); + 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 { + 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 (inputSetHostView) return inputSetHostView; + + 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; + + 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; + } + + window = [UIApplication.sharedApplication.windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) { + return [NSStringFromClass(item.class) isEqualToString:@"UITextEffectsWindow"]; + }]; + return window; +} + ++ (BOOL)isKeyboardVisible { + UIView *keyboardView = self.keyboardView; + UIWindow *keyboardWindow = keyboardView.window; + if (!keyboardView || !keyboardWindow) { + return NO; + } + CGRect rect = CGRectIntersection(CGRectFlatted(keyboardWindow.bounds), CGRectFlatted(keyboardView.frame)); + if (CGRectIsValidated(rect) && !CGRectIsEmpty(rect)) { + return YES; + } + return NO; +} + ++ (CGRect)currentKeyboardFrame { + UIView *keyboardView = [self keyboardView]; + if (!keyboardView) { + return CGRectNull; + } + UIWindow *keyboardWindow = keyboardView.window; + if (keyboardWindow) { + return [keyboardWindow convertRect:CGRectFlatted(keyboardView.frame) toWindow:nil]; + } else { + return CGRectFlatted(keyboardView.frame); + } +} + ++ (CGFloat)visibleKeyboardHeight { + UIView *keyboardView = [self keyboardView]; + // 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 { + // 开启了系统的“设置→辅助功能→动态效果→减弱动态效果→首选交叉淡出过渡效果”后,键盘动画不再是 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); + } + return 0; + } +} + +@end + +#pragma mark - UITextField + +@interface UITextField () + +@end + +@implementation UITextField (QMUI_KeyboardManager) + +static char kAssociatedObjectKey_keyboardWillShowNotificationBlock; +- (void)setQmui_keyboardWillShowNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillShowNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardWillShowNotificationBlock, qmui_keyboardWillShowNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardWillShowNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillShowNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardWillShowNotificationBlock); +} + +static char kAssociatedObjectKey_keyboardDidShowNotificationBlock; +- (void)setQmui_keyboardDidShowNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidShowNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardDidShowNotificationBlock, qmui_keyboardDidShowNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardDidShowNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidShowNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardDidShowNotificationBlock); +} + +static char kAssociatedObjectKey_keyboardWillHideNotificationBlock; +- (void)setQmui_keyboardWillHideNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillHideNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardWillHideNotificationBlock, qmui_keyboardWillHideNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardWillHideNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillHideNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardWillHideNotificationBlock); +} + +static char kAssociatedObjectKey_keyboardDidHideNotificationBlock; +- (void)setQmui_keyboardDidHideNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidHideNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardDidHideNotificationBlock, qmui_keyboardDidHideNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardDidHideNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidHideNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardDidHideNotificationBlock); +} + +static char kAssociatedObjectKey_keyboardWillChangeFrameNotificationBlock; +- (void)setQmui_keyboardWillChangeFrameNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillChangeFrameNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardWillChangeFrameNotificationBlock, qmui_keyboardWillChangeFrameNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardWillChangeFrameNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillChangeFrameNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardWillChangeFrameNotificationBlock); +} + +static char kAssociatedObjectKey_keyboardDidChagneFrameNotificationBlock; +- (void)setQmui_keyboardDidChangeFrameNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidChangeFrameNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardDidChagneFrameNotificationBlock, qmui_keyboardDidChangeFrameNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardDidChangeFrameNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidChangeFrameNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardDidChagneFrameNotificationBlock); +} + +- (void)initKeyboardManagerIfNeeded { + if (!self.qmui_keyboardManager) { + self.qmui_keyboardManager = [[QMUIKeyboardManager alloc] initWithDelegate:self]; + [self.qmui_keyboardManager addTargetResponder:self]; + } +} + +#pragma mark - + +- (void)keyboardWillShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardWillShowNotificationBlock) { + self.qmui_keyboardWillShowNotificationBlock(keyboardUserInfo); + } +} + +- (void)keyboardWillHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardWillHideNotificationBlock) { + self.qmui_keyboardWillHideNotificationBlock(keyboardUserInfo); + } +} + +- (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardWillChangeFrameNotificationBlock) { + self.qmui_keyboardWillChangeFrameNotificationBlock(keyboardUserInfo); + } +} + +- (void)keyboardDidShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardDidShowNotificationBlock) { + self.qmui_keyboardDidShowNotificationBlock(keyboardUserInfo); + } +} + +- (void)keyboardDidHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardDidHideNotificationBlock) { + self.qmui_keyboardDidHideNotificationBlock(keyboardUserInfo); + } +} + +- (void)keyboardDidChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardDidChangeFrameNotificationBlock) { + self.qmui_keyboardDidChangeFrameNotificationBlock(keyboardUserInfo); + } +} + +@end + +#pragma mark - UITextView + +@interface UITextView () + +@end + +@implementation UITextView (QMUI_KeyboardManager) + +static char kAssociatedObjectKey_keyboardWillShowNotificationBlock; +- (void)setQmui_keyboardWillShowNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillShowNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardWillShowNotificationBlock, qmui_keyboardWillShowNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardWillShowNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillShowNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardWillShowNotificationBlock); +} + +static char kAssociatedObjectKey_keyboardDidShowNotificationBlock; +- (void)setQmui_keyboardDidShowNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidShowNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardDidShowNotificationBlock, qmui_keyboardDidShowNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardDidShowNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidShowNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardDidShowNotificationBlock); +} + +static char kAssociatedObjectKey_keyboardWillHideNotificationBlock; +- (void)setQmui_keyboardWillHideNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillHideNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardWillHideNotificationBlock, qmui_keyboardWillHideNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardWillHideNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillHideNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardWillHideNotificationBlock); +} + +static char kAssociatedObjectKey_keyboardDidHideNotificationBlock; +- (void)setQmui_keyboardDidHideNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidHideNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardDidHideNotificationBlock, qmui_keyboardDidHideNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardDidHideNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidHideNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardDidHideNotificationBlock); +} + +static char kAssociatedObjectKey_keyboardWillChangeFrameNotificationBlock; +- (void)setQmui_keyboardWillChangeFrameNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillChangeFrameNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardWillChangeFrameNotificationBlock, qmui_keyboardWillChangeFrameNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardWillChangeFrameNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillChangeFrameNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardWillChangeFrameNotificationBlock); +} + +static char kAssociatedObjectKey_keyboardDidChagneFrameNotificationBlock; +- (void)setQmui_keyboardDidChangeFrameNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidChangeFrameNotificationBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_keyboardDidChagneFrameNotificationBlock, qmui_keyboardDidChangeFrameNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_keyboardDidChangeFrameNotificationBlock) { + [self initKeyboardManagerIfNeeded]; + } +} + +- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidChangeFrameNotificationBlock { + return (void (^)(QMUIKeyboardUserInfo *))objc_getAssociatedObject(self, &kAssociatedObjectKey_keyboardDidChagneFrameNotificationBlock); +} + +- (void)initKeyboardManagerIfNeeded { + if (!self.qmui_keyboardManager) { + self.qmui_keyboardManager = [[QMUIKeyboardManager alloc] initWithDelegate:self]; + [self.qmui_keyboardManager addTargetResponder:self]; + } +} + +#pragma mark - + +- (void)keyboardWillShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardWillShowNotificationBlock) { + self.qmui_keyboardWillShowNotificationBlock(keyboardUserInfo); + } +} + +- (void)keyboardWillHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardWillHideNotificationBlock) { + self.qmui_keyboardWillHideNotificationBlock(keyboardUserInfo); + } +} + +- (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardWillChangeFrameNotificationBlock) { + self.qmui_keyboardWillChangeFrameNotificationBlock(keyboardUserInfo); + } +} + +- (void)keyboardDidShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardDidShowNotificationBlock) { + self.qmui_keyboardDidShowNotificationBlock(keyboardUserInfo); + } +} + +- (void)keyboardDidHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardDidHideNotificationBlock) { + self.qmui_keyboardDidHideNotificationBlock(keyboardUserInfo); + } +} + +- (void)keyboardDidChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.qmui_keyboardDidChangeFrameNotificationBlock) { + self.qmui_keyboardDidChangeFrameNotificationBlock(keyboardUserInfo); + } +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILabel.h b/QMUI/QMUIKit/QMUIComponents/QMUILabel.h new file mode 100644 index 00000000..4e5f0bac --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILabel.h @@ -0,0 +1,54 @@ +/** + * 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. + */ + +// +// QMUILabel.h +// qmui +// +// Created by QMUI Team on 14-7-3. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * `QMUILabel`支持通过`contentEdgeInsets`属性来实现类似padding的效果。 + * + * 同时通过将`canPerformCopyAction`置为`YES`来开启长按复制文本的功能,复制 item 的文案可通过 menuItemTitleForCopyAction 修改,长按时label的背景色默认为`highlightedBackgroundColor` + */ +@interface QMUILabel : UILabel + +/// 控制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, nullable) IBInspectable NSString *menuItemTitleForCopyAction; + +/** + 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, 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 new file mode 100644 index 00000000..f50ddfad --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILabel.m @@ -0,0 +1,196 @@ +/** + * 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. + */ + +// +// QMUILabel.m +// qmui +// +// Created by QMUI Team on 14-7-3. +// + +#import "QMUILabel.h" +#import "QMUICore.h" +#import "UILabel+QMUI.h" + +@interface QMUILabel () + +@property(nonatomic, strong) UIColor *originalBackgroundColor; +@property(nonatomic, strong) UILongPressGestureRecognizer *longGestureRecognizer; +@end + + +@implementation QMUILabel + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)setContentEdgeInsets:(UIEdgeInsets)contentEdgeInsets { + _contentEdgeInsets = contentEdgeInsets; + [self setNeedsDisplay]; +} + +- (CGSize)sizeThatFits:(CGSize)size { + size = [super sizeThatFits:CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets), size.height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets))]; + size.width += UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets); + size.height += UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets); + return size; +} + +- (CGSize)intrinsicContentSize { + CGFloat preferredMaxLayoutWidth = self.preferredMaxLayoutWidth; + if (preferredMaxLayoutWidth <= 0) { + preferredMaxLayoutWidth = CGFLOAT_MAX; + } + 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); + + // 在某些情况下文字位置错误,因此做了如下保护 + // https://github.com/Tencent/QMUI_iOS/issues/529 + if (self.numberOfLines == 1 && (self.lineBreakMode == NSLineBreakByWordWrapping || self.lineBreakMode == NSLineBreakByCharWrapping)) { + rect = CGRectSetHeight(rect, CGRectGetHeight(rect) + self.contentEdgeInsets.top * 2); + } + + [super drawTextInRect:rect]; +} + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + + if (self.highlightedBackgroundColor) { + [super setBackgroundColor:highlighted ? self.highlightedBackgroundColor : self.originalBackgroundColor]; + } +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor { + self.originalBackgroundColor = backgroundColor; + + // 在出现 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]; + } +} + +#pragma mark - 长按复制功能 + +- (void)setCanPerformCopyAction:(BOOL)canPerformCopyAction { + _canPerformCopyAction = canPerformCopyAction; + if (_canPerformCopyAction && !self.longGestureRecognizer) { + self.userInteractionEnabled = YES; + self.longGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGestureRecognizer:)]; + [self addGestureRecognizer:self.longGestureRecognizer]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMenuWillHideNotification:) name:UIMenuControllerWillHideMenuNotification object:nil]; + } else if (!_canPerformCopyAction && self.longGestureRecognizer) { + [self removeGestureRecognizer:self.longGestureRecognizer]; + self.longGestureRecognizer = nil; + self.userInteractionEnabled = NO; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; + } +} + +- (BOOL)canBecomeFirstResponder { + return self.canPerformCopyAction; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { + if ([self canBecomeFirstResponder]) { + return action == @selector(copyString:); + } + return NO; +} + +- (void)copyString:(id)sender { + if (self.canPerformCopyAction) { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + NSString *stringToCopy = self.text; + if (stringToCopy) { + pasteboard.string = stringToCopy; + if (self.didCopyBlock) { + self.didCopyBlock(self, stringToCopy); + } + } + } +} + +- (void)handleLongPressGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer { + if (!self.canPerformCopyAction) { + return; + } + if (gestureRecognizer.state == UIGestureRecognizerStateBegan) { + [self becomeFirstResponder]; + UIMenuController *menuController = [UIMenuController sharedMenuController]; + UIMenuItem *copyMenuItem = [[UIMenuItem alloc] initWithTitle:self.menuItemTitleForCopyAction ?: @"复制" action:@selector(copyString:)]; + [[UIMenuController sharedMenuController] setMenuItems:@[copyMenuItem]]; + [menuController showMenuFromView:self.superview rect:self.frame]; + + self.highlighted = YES; + } else if (gestureRecognizer.state == UIGestureRecognizerStatePossible) { + self.highlighted = NO; + } +} + +- (void)handleMenuWillHideNotification:(NSNotification *)notification { + if (!self.canPerformCopyAction) { + return; + } + + [self setHighlighted:NO]; +} + +@end 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 new file mode 100644 index 00000000..ea46e130 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILog.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. + */ + +// +// QMUILog.h +// QMUIKit +// +// Created by QMUI Team on 2018/1/22. +// + +#import +#import "QMUILogItem.h" +#import "QMUILogNameManager.h" +#import "QMUILogger.h" +#import + +/// 以下是 QMUI 提供的用于代替 NSLog() 的打 log 的方法,可根据 logName、logLevel 两个维度来控制某些 log 是否要被打印,以便在调试时去掉不关注的 log。 + +#define QMUILog(_name, ...) [[QMUILogger sharedInstance] printLogWithFile:__FILE__ line:__LINE__ func:__FUNCTION__ logItem:[QMUILogItem logItemWithLevel:QMUILogLevelDefault name:_name logString:__VA_ARGS__]] +#define QMUILogInfo(_name, ...) [[QMUILogger sharedInstance] printLogWithFile:__FILE__ line:__LINE__ func:__FUNCTION__ logItem:[QMUILogItem logItemWithLevel:QMUILogLevelInfo name:_name logString:__VA_ARGS__]] +#define QMUILogWarn(_name, ...) [[QMUILogger sharedInstance] printLogWithFile:__FILE__ line:__LINE__ func:__FUNCTION__ logItem:[QMUILogItem logItemWithLevel:QMUILogLevelWarn name:_name logString:__VA_ARGS__]] + +//#ifdef DEBUG +// +//// iOS 11 之前用真正的方法替换去实现拦截 NSLog 的功能,iOS 11 之后这种方法失效了,所以只能用宏定义的方式覆盖 NSLog。这也就意味着在 iOS 11 下一些如果某些代码编译时机比 QMUI 早,则这些代码里的 NSLog 是无法被替换为 QMUILog 的 +//extern void _NSSetLogCStringFunction(void (*)(const char *string, unsigned length, BOOL withSyslogBanner)); +//static void PrintNSLogMessage(const char *string, unsigned length, BOOL withSyslogBanner) { +// QMUILog(@"NSLog", @"%s", string); +//} +// +//static void HackNSLog(void) __attribute__((constructor)); +//static void HackNSLog(void) { +// _NSSetLogCStringFunction(PrintNSLogMessage); +//} +// +//#define NSLog(...) QMUILog(@"NSLog", __VA_ARGS__)// iOS 11 以后真正生效的是这一句 +//#endif diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h new file mode 100644 index 00000000..3455dbf5 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h @@ -0,0 +1,45 @@ +/** + * 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. + */ + +// +// QMUILogItem.h +// QMUIKit +// +// Created by QMUI Team on 2018/1/24. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, QMUILogLevel) { + QMUILogLevelDefault, // 当使用 QMUILog() 时使用的等级 + QMUILogLevelInfo, // 当使用 QMUILogInfo() 时使用的等级,比 QMUILogLevelDefault 要轻量,适用于一些无关紧要的信息 + QMUILogLevelWarn // 当使用 QMUILogWarn() 时使用的等级,最重,适用于一些异常或者严重错误的场景 +}; + +/// 每一条 QMUILog 日志都以 QMUILogItem 的形式包装起来 +@interface QMUILogItem : NSObject + +/// 日志的等级,可通过 QMUIConfigurationTemplate 配置表控制全局每个 level 是否可用 +@property(nonatomic, assign) QMUILogLevel level; +@property(nonatomic, copy, readonly) NSString *levelDisplayString; + +/// 可利用 name 字段为日志分类,QMUILogNameManager 可全局控制某一个 name 是否可用 +@property(nullable, nonatomic, copy) NSString *name; + +/// 日志的内容 +@property(nonatomic, copy) NSString *logString; + +/// 当前 logItem 对应的 name 是否可用,可通过 QMUILogNameManager 控制,默认为 YES +@property(nonatomic, assign) BOOL enabled; + ++ (nonnull instancetype)logItemWithLevel:(QMUILogLevel)level name:(nullable NSString *)name logString:(nonnull NSString *)logString, ... NS_FORMAT_FUNCTION(3, 4); +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m new file mode 100644 index 00000000..82fa943d --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogItem.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. + */ + +// +// QMUILogItem.m +// QMUIKit +// +// Created by QMUI Team on 2018/1/24. +// + +#import "QMUILogItem.h" +#import "QMUILogger.h" +#import "QMUILogNameManager.h" + +@implementation QMUILogItem + ++ (instancetype)logItemWithLevel:(QMUILogLevel)level name:(NSString *)name logString:(NSString *)logString, ... { + QMUILogItem *logItem = [[self alloc] init]; + logItem.level = level; + logItem.name = name; + + QMUILogNameManager *logNameManager = [QMUILogger sharedInstance].logNameManager; + if ([logNameManager containsLogName:name]) { + logItem.enabled = [logNameManager enabledForLogName:name]; + } else { + [logNameManager setEnabled:YES forLogName:name]; + logItem.enabled = YES; + } + + va_list args; + va_start(args, logString); + logItem.logString = [[NSString alloc] initWithFormat:logString arguments:args]; + va_end(args); + + return logItem; +} + +- (instancetype)init { + if (self = [super init]) { + self.enabled = YES; + } + return self; +} + +- (NSString *)levelDisplayString { + switch (self.level) { + case QMUILogLevelInfo: + return @"QMUILogLevelInfo"; + case QMUILogLevelWarn: + return @"QMUILogLevelWarn"; + default: + return @"QMUILogLevelDefault"; + } +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ | %@ | %@", self.levelDisplayString, self.name.length > 0 ? self.name : @"Default", self.logString]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h new file mode 100644 index 00000000..918ee55e --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h @@ -0,0 +1,31 @@ +/** + * 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. + */ + +// +// QMUILogNameManager.h +// QMUIKit +// +// Created by QMUI Team on 2018/1/24. +// + +#import + +/// 所有 QMUILog 的 name 都会以这个 key 存储到 NSUserDefaults 里(类型为 NSDictionary *),可通过 dictionaryForKey: 获取到所有的 name 及对应的 enabled 状态。 +extern NSString * _Nonnull const QMUILoggerAllNamesKeyInUserDefaults; + +/// log.name 的管理器,由它来管理每一个 name 是否可用、以及清理不需要的 name +@interface QMUILogNameManager : NSObject + +/// 获取当前所有 logName,key 为 logName 名,value 为 name 的 enabled 状态,可通过 value.boolValue 读取它的值 +@property(nullable, nonatomic, copy, readonly) NSDictionary *allNames; +- (BOOL)containsLogName:(nullable NSString *)logName; +- (void)setEnabled:(BOOL)enabled forLogName:(nullable NSString *)logName; +- (BOOL)enabledForLogName:(nullable NSString *)logName; +- (void)removeLogName:(nullable NSString *)logName; +- (void)removeAllNames; +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m new file mode 100644 index 00000000..972c14bd --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.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. + */ + +// +// QMUILogNameManager.m +// QMUIKit +// +// Created by QMUI Team on 2018/1/24. +// + +#import "QMUILogNameManager.h" +#import "QMUILogger.h" + +NSString *const QMUILoggerAllNamesKeyInUserDefaults = @"QMUILoggerAllNamesKeyInUserDefaults"; + +@interface QMUILogNameManager () + +@property(nonatomic, strong) NSMutableDictionary *mutableAllNames; +@property(nonatomic, assign) BOOL didInitialize; +@end + +@implementation QMUILogNameManager + +- (instancetype)init { + if (self = [super init]) { + self.mutableAllNames = [[NSMutableDictionary alloc] init]; + + NSDictionary *allQMUILogNames = [[NSUserDefaults standardUserDefaults] dictionaryForKey:QMUILoggerAllNamesKeyInUserDefaults]; + for (NSString *logName in allQMUILogNames) { + [self setEnabled:allQMUILogNames[logName].boolValue forLogName:logName]; + } + + // 初始化时从 NSUserDefaults 里获取值的过程,不希望触发 delegate,所以加这个标志位 + self.didInitialize = YES; + } + return self; +} + +- (NSDictionary *)allNames { + if (self.mutableAllNames.count) { + return [self.mutableAllNames copy]; + } + return nil; +} + +- (BOOL)containsLogName:(NSString *)logName { + if (logName.length > 0) { + return !!self.mutableAllNames[logName]; + } + return NO; +} + +- (void)setEnabled:(BOOL)enabled forLogName:(NSString *)logName { + if (logName.length > 0) { + self.mutableAllNames[logName] = @(enabled); + + if (!self.didInitialize) return; + + [self synchronizeUserDefaults]; + + if ([[QMUILogger sharedInstance].delegate respondsToSelector:@selector(QMUILogName:didChangeEnabled:)]) { + [[QMUILogger sharedInstance].delegate QMUILogName:logName didChangeEnabled:enabled]; + } + } +} + +- (BOOL)enabledForLogName:(NSString *)logName { + if (logName.length > 0) { + if ([self containsLogName:logName]) { + return [self.mutableAllNames[logName] boolValue]; + } + } + return YES; +} + +- (void)removeLogName:(NSString *)logName { + if (logName.length > 0) { + [self.mutableAllNames removeObjectForKey:logName]; + + if (!self.didInitialize) return; + + [self synchronizeUserDefaults]; + + if ([[QMUILogger sharedInstance].delegate respondsToSelector:@selector(QMUILogNameDidRemove:)]) { + [[QMUILogger sharedInstance].delegate QMUILogNameDidRemove:logName]; + } + } +} + +- (void)removeAllNames { + BOOL shouldCallDelegate = self.didInitialize && [[QMUILogger sharedInstance].delegate respondsToSelector:@selector(QMUILogNameDidRemove:)]; + NSDictionary *allNames = nil; + if (shouldCallDelegate) { + allNames = self.allNames; + } + + [self.mutableAllNames removeAllObjects]; + + [self synchronizeUserDefaults]; + + if (shouldCallDelegate) { + for (NSString *logName in allNames.allKeys) { + [[QMUILogger sharedInstance].delegate QMUILogNameDidRemove:logName]; + } + } +} + +- (void)synchronizeUserDefaults { + [[NSUserDefaults standardUserDefaults] setObject:self.allNames forKey:QMUILoggerAllNamesKeyInUserDefaults]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h new file mode 100644 index 00000000..1959beb7 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.h @@ -0,0 +1,55 @@ +/** + * 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. + */ +// +// QMUILogger.h +// QMUIKit +// +// Created by QMUI Team on 2018/1/24. +// +#import + +@class QMUILogNameManager; +@class QMUILogItem; + +@protocol QMUILoggerDelegate + +@optional + +/** + * 当每一个 enabled 的 QMUILog 被使用时都会走到这里,可以由业务自行决定要如何处理这些 log,如果没实现这个方法,默认用 NSLog() 打印内容 + * @param file 当前的文件的本地完整路径,可通过 file.lastPathComponent 获取文件名 + * @param line 当前 log 命令在该文件里的代码行数 + * @param func 当前 log 命令所在的方法名 + * @param logItem 当前 log 命令对应的 QMUILogItem,可得知该 log 的 level + * @param defaultString QMUI 默认拼好的 log 内容 + */ +- (void)printQMUILogWithFile:(nonnull NSString *)file line:(int)line func:(nullable NSString *)func logItem:(nullable QMUILogItem *)logItem defaultString:(nullable NSString *)defaultString; + +/** + * 当某个 logName 的 enabled 发生变化时,通知到 delegate。注意如果是新创建某个 logName 也会走到这里。 + * @param logName 变化的 logName + * @param enabled 变化后的值 + */ +- (void)QMUILogName:(nonnull NSString *)logName didChangeEnabled:(BOOL)enabled; + +/** + * 某个 logName 被删除时通知到 delegate + * @param logName 被删除的 logName + */ +- (void)QMUILogNameDidRemove:(nonnull NSString *)logName; + +@end + +@interface QMUILogger : NSObject + +@property(nullable, nonatomic, weak) id delegate; +@property(nonnull, nonatomic, strong) QMUILogNameManager *logNameManager; + ++ (nonnull instancetype)sharedInstance; +- (void)printLogWithFile:(nullable const char *)file line:(int)line func:(nonnull const char *)func logItem:(nullable QMUILogItem *)logItem; +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m new file mode 100644 index 00000000..71e52051 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILog/QMUILogger.m @@ -0,0 +1,63 @@ +/** + * 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. + */ + +// +// QMUILogger.m +// QMUIKit +// +// Created by QMUI Team on 2018/1/24. +// + +#import "QMUILogger.h" +#import "QMUILogNameManager.h" +#import "QMUILogItem.h" + +@implementation QMUILogger + ++ (instancetype)sharedInstance { + static dispatch_once_t onceToken; + static QMUILogger *instance = nil; + dispatch_once(&onceToken,^{ + instance = [[super allocWithZone:NULL] init]; + }); + return instance; +} + ++ (id)allocWithZone:(struct _NSZone *)zone{ + return [self sharedInstance]; +} + +- (instancetype)init { + if (self = [super init]) { + self.logNameManager = [[QMUILogNameManager alloc] init]; + } + return self; +} + +- (void)printLogWithFile:(const char *)file line:(int)line func:(const char *)func logItem:(QMUILogItem *)logItem { + // 禁用了某个 name 则直接退出 + if (!logItem.enabled) return; + + NSString *fileString = [NSString stringWithFormat:@"%s", file]; + NSString *funcString = [NSString stringWithFormat:@"%s", func]; + NSString *defaultString = [NSString stringWithFormat:@"%@:%@ | %@", funcString, @(line), logItem]; + + if ([self.delegate respondsToSelector:@selector(printQMUILogWithFile:line:func:logItem:defaultString:)]) { + [self.delegate printQMUILogWithFile:fileString line:line func:funcString logItem:logItem defaultString:defaultString]; + } else { +// // iOS 11 之前用替换方法的方式替换了 NSLog,所以这里就不能继续使用 NSLog 了 +// if (IS_DEBUG && IOS_VERSION_NUMBER < 110000) { +// NSStringEncoding enc = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingUTF8); +// puts([defaultString cStringUsingEncoding:enc]); +// } else { + NSLog(@"%@", defaultString); +// } + } +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.h b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.h new file mode 100644 index 00000000..bf04276d --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.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. + */ + +// +// QMUILogManagerViewController.h +// QMUIKit +// +// Created by QMUI Team on 2018/1/24. +// + +#import "QMUICommonTableViewController.h" + +/// 用于管理 QMUILog name 的调试界面,可直接 init 使用 +@interface QMUILogManagerViewController : QMUICommonTableViewController + +/// cell 总个数大于等于这个数值时才会出搜索框和右边的 section title 索引条,方便检索。默认值为 10。 +@property(nonatomic, assign) NSUInteger rowCountWhenShowSearchBar; + +/// 一般项目的 logName 都会带有统一前缀(例如 @"QMUIImagePickerLibrary"),而在排序的时候,前缀通常是无意义的,因此这里提供一个 block 让你可以根据传进去的 logName 返回一个不带前缀的用于排序的 logName,且这个返回值的第一个字母将会作为 section 的索引显示在列表右边。若不实现这个 block 则直接拿原 logName 进行排序。 +@property(nonatomic, copy) NSString *(^formatLogNameForSortingBlock)(NSString *logName); + +/// 可自定义 cell 的文字样式,方便区分不同的 logName +@property(nonatomic, copy) NSAttributedString *(^formatCellTextBlock)(NSString *logName); +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.m new file mode 100644 index 00000000..dead13a4 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILogManagerViewController.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. + */ + +// +// QMUILogManagerViewController.m +// QMUIKit +// +// Created by QMUI Team on 2018/1/24. +// + +#import "QMUILogManagerViewController.h" +#import "QMUICore.h" +#import "QMUILog.h" +#import "QMUIStaticTableViewCellData.h" +#import "QMUIStaticTableViewCellDataSource.h" +#import "UITableView+QMUIStaticCell.h" +#import "QMUITableView.h" +#import "QMUIPopupMenuView.h" +#import "UITableView+QMUI.h" +#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; +@end + +@implementation QMUILogManagerViewController + +- (void)didInitializeWithStyle:(UITableViewStyle)style { + [super didInitializeWithStyle:style]; + self.rowCountWhenShowSearchBar = 10; +} + +- (void)initTableView { + [super initTableView]; + [self setupDataSource]; +} + +- (void)initSearchController { + [super initSearchController]; + self.searchController.qmui_preferredStatusBarStyleBlock = ^UIStatusBarStyle{ + return UIStatusBarStyleDarkContent; + }; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + [self checkEmptyView]; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + if (self.allNames.count) { + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(handleMenuItemEvent)]; + } else { + self.navigationItem.rightBarButtonItem = nil; + } +} + +- (void)setupDataSource { + self.allNames = [QMUILogger sharedInstance].logNameManager.allNames; + + NSArray *logNames = self.allNames.allKeys; + + self.sortedLogNames = [logNames sortedArrayUsingComparator:^NSComparisonResult(NSString *logName1, NSString *logName2) { + logName1 = [self formatLogNameForSorting:logName1]; + logName2 = [self formatLogNameForSorting:logName2]; + return [logName1 caseInsensitiveCompare:logName2]; + }]; + self.sectionIndexTitles = ({ + NSMutableArray *titles = [[NSMutableArray alloc] init]; + for (NSInteger i = 0; i < self.sortedLogNames.count; i++) { + NSString *logName = self.sortedLogNames[i]; + NSString *sectionIndexTitle = [[self formatLogNameForSorting:logName] substringToIndex:1]; + if (![titles containsObject:sectionIndexTitle]) { + [titles addObject:sectionIndexTitle]; + } + } + [titles copy]; + }); + + NSMutableArray *> *cellDataSections = [[NSMutableArray alloc] init]; + NSMutableArray *currentSection = nil; + for (NSInteger i = 0; i < self.sortedLogNames.count; i++) { + NSString *logName = self.sortedLogNames[i]; + NSString *formatedLogName = [self formatLogNameForSorting:logName]; + NSString *sectionIndexTitle = [formatedLogName substringToIndex:1]; + NSUInteger section = [self.sectionIndexTitles indexOfObject:sectionIndexTitle]; + if (section != NSNotFound) { + if (cellDataSections.count <= section) { + // 说明这个 section 还没被创建过 + currentSection = [[NSMutableArray alloc] init]; + [cellDataSections addObject:currentSection]; + } + [currentSection addObject:({ + QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; + d.text = logName; + d.accessoryType = QMUIStaticTableViewCellAccessoryTypeSwitch; + d.accessoryValueObject = self.allNames[logName]; + d.accessoryTarget = self; + d.accessoryAction = @selector(handleSwitchEvent:); + d; + })]; + } + } + + // 超过一定数量则出搜索框,先设置好搜索框的显隐,以便其他东西可以依赖搜索框的显隐状态来做判断 + NSInteger rowCount = logNames.count; + self.shouldShowSearchBar = rowCount >= self.rowCountWhenShowSearchBar; + + QMUIStaticTableViewCellDataSource *dataSource = [[QMUIStaticTableViewCellDataSource alloc] initWithCellDataSections:cellDataSections]; + self.tableView.qmui_staticCellDataSource = dataSource; +} + +- (void)reloadData { + [self setupDataSource]; + [self checkEmptyView]; + [self.tableView reloadData]; +} + +- (void)checkEmptyView { + if (self.allNames.count <= 0) { + [self showEmptyViewWithText:@"暂无 QMUILog 产生" detailText:nil buttonTitle:nil buttonAction:NULL]; + } else { + [self hideEmptyView]; + } + [self setupNavigationItems]; +} + +- (NSArray *)sortedLogNameArray { + NSArray *logNames = self.allNames.allKeys; + NSArray *sortedArray = [logNames sortedArrayUsingComparator:^NSComparisonResult(NSString *logName1, NSString *logName2) { + + return NSOrderedAscending; + }]; + return sortedArray; +} + +- (NSString *)formatLogNameForSorting:(NSString *)logName { + if (self.formatLogNameForSortingBlock) { + return self.formatLogNameForSortingBlock(logName); + } + return logName; +} + +- (void)handleSwitchEvent:(UISwitch *)switchControl { + UITableView *tableView = self.searchController.active ? self.searchController.tableView : self.tableView; + NSIndexPath *indexPath = [tableView qmui_indexPathForRowAtView:switchControl]; + QMUIStaticTableViewCellData *cellData = [tableView.qmui_staticCellDataSource cellDataAtIndexPath:indexPath]; + cellData.accessoryValueObject = @(switchControl.on); + [[QMUILogger sharedInstance].logNameManager setEnabled:switchControl.on forLogName:cellData.text]; +} + +- (void)handleMenuItemEvent { + QMUIPopupMenuView *menuView = [[QMUIPopupMenuView alloc] init]; + menuView.automaticallyHidesWhenUserTap = YES; + menuView.preferLayoutDirection = QMUIPopupContainerViewLayoutDirectionBelow; + menuView.maximumWidth = 124; + menuView.safetyMarginsOfSuperview = UIEdgeInsetsSetRight(menuView.safetyMarginsOfSuperview, 6); + menuView.items = @[ + [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]; +} + +#pragma mark - + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + QMUITableViewCell *cell = [tableView.qmui_staticCellDataSource cellForRowAtIndexPath:indexPath]; + QMUIStaticTableViewCellData *cellData = [tableView.qmui_staticCellDataSource cellDataAtIndexPath:indexPath]; + NSString *logName = cellData.text; + + NSAttributedString *string = nil; + if (self.formatCellTextBlock) { + string = self.formatCellTextBlock(logName); + } else { + NSString *formatedLogName = [self formatLogNameForSorting:logName]; + NSRange range = [logName rangeOfString:formatedLogName]; + NSMutableAttributedString *mutableString = [[NSMutableAttributedString alloc] initWithString:logName attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorGray}]; + [mutableString setAttributes:@{NSForegroundColorAttributeName: UIColorBlack} range:range]; + string = [mutableString copy]; + } + cell.textLabel.attributedText = string; + + if ([cell.accessoryView isKindOfClass:[UISwitch class]]) { + BOOL enabled = self.allNames[logName].boolValue; + UISwitch *switchControl = (UISwitch *)cell.accessoryView; + switchControl.on = enabled; + } + return cell; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + return tableView == self.tableView ? self.sectionIndexTitles[section] : nil; +} + +- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { + return tableView == self.tableView && self.shouldShowSearchBar ? self.sectionIndexTitles : nil; +} + +#pragma mark - + +- (void)searchController:(QMUISearchController *)searchController updateResultsForSearchString:(NSString *)searchString { + NSArray *> *dataSource = self.tableView.qmui_staticCellDataSource.cellDataSections; + NSMutableArray *resultDataSource = [[NSMutableArray alloc] init];// 搜索结果就不需要分 section 了 + for (NSInteger section = 0; section < dataSource.count; section ++) { + for (NSInteger row = 0; row < dataSource[section].count; row ++) { + QMUIStaticTableViewCellData *cellData = dataSource[section][row]; + NSString *text = cellData.text; + if ([text.lowercaseString containsString:searchString.lowercaseString]) { + [resultDataSource addObject:cellData]; + } + } + } + searchController.tableView.qmui_staticCellDataSource = [[QMUIStaticTableViewCellDataSource alloc] initWithCellDataSections:@[resultDataSource.copy]]; + + if (resultDataSource.count > 0) { + [searchController hideEmptyView]; + } else { + [searchController showEmptyViewWithText:@"无结果" detailText:nil buttonTitle:nil buttonAction:NULL]; + } +} + +- (void)willDismissSearchController:(QMUISearchController *)searchController { + // 在搜索状态里可能修改了 switch 的值,则退出时强制刷新一下默认状态的列表 + [self reloadData]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h new file mode 100644 index 00000000..8fc9b904 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h @@ -0,0 +1,21 @@ +/** + * 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. + */ + +// +// QMUILogger+QMUIConfigurationTemplate.h +// QMUIKit +// +// Created by QMUI Team on 2018/7/28. +// + +#import +#import "QMUILog.h" + +@interface QMUILogger (QMUIConfigurationTemplate) + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m new file mode 100644 index 00000000..04a54869 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m @@ -0,0 +1,40 @@ +/** + * 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. + */ + +// +// QMUILogger+QMUIConfigurationTemplate.m +// QMUIKit +// +// Created by QMUI Team on 2018/7/28. +// + +#import "QMUILogger+QMUIConfigurationTemplate.h" +#import "QMUICore.h" + +@implementation QMUILogger (QMUIConfigurationTemplate) + ++ (void)load { + static dispatch_once_t onceToken; + 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) { + // 不同级别的 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 *); + originSelectorIMP = (void (*)(id, SEL, const char *, int, const char *, QMUILogItem *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, file, line, func, logItem); + }; + }); + }); +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h b/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h new file mode 100644 index 00000000..95faf9a8 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.h @@ -0,0 +1,93 @@ +/** + * 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. + */ + +// +// QMUIMarqueeLabel.h +// qmui +// +// Created by QMUI Team on 2017/5/31. +// + +#import + +/** + * 简易的跑马灯 label 控件,在文字超过 label 可视区域时会自动开启跑马灯效果展示文字,文字滚动时是首尾连接的效果(参考播放音乐时系统锁屏界面顶部的音乐标题)。 + * @warning lineBreakMode 默认为 NSLineBreakByClipping(UILabel 默认值为 NSLineBreakByTruncatingTail)。 + * @warning textAlignment 暂不支持 NSTextAlignmentJustified 和 NSTextAlignmentNatural。 + * @warning 会忽略 numberOfLines 属性,强制以 1 来展示。 + */ +@interface QMUIMarqueeLabel : UILabel + +/// 控制滚动的速度,1 表示一帧滚动 1pt,10 表示一帧滚动 10pt,默认为 .5,与系统一致。 +@property(nonatomic, assign) IBInspectable CGFloat speed; + +/// 当文字第一次显示在界面上,以及重复滚动到开头时都要停顿一下,这个属性控制停顿的时长,默认为 2.5(也是与系统一致),单位为秒。 +@property(nonatomic, assign) IBInspectable NSTimeInterval pauseDurationWhenMoveToEdge; + +/// 用于控制首尾连接的文字之间的间距,默认为 40pt。 +@property(nonatomic, assign) IBInspectable CGFloat spacingBetweenHeadToTail; + +// 用于控制左和右边两端的渐变区域的百分比,默认为 0.2,则是 20% 宽。 +@property(nonatomic, assign) IBInspectable CGFloat fadeWidthPercent; + +/** + * 自动判断 label 的 frame 是否超出当前的 UIWindow 可视范围,超出则自动停止动画。默认为 YES。 + * @warning 某些场景并无法触发这个自动检测(例如直接调整 label.superview 的 frame 而不是 label 自身的 frame),这种情况暂不处理。 + */ +@property(nonatomic, assign) IBInspectable BOOL automaticallyValidateVisibleFrame; + +/// 在文字滚动到左右边缘时,是否要显示一个阴影渐变遮罩,默认为 YES。 +@property(nonatomic, assign) IBInspectable BOOL shouldFadeAtEdge; + +/// YES 表示文字会在打开 shouldFadeAtEdge 的情况下,从左边的渐隐区域之后显示,NO 表示不管有没有打开 shouldFadeAtEdge,都会从 label 的边缘开始显示。默认为 NO。 +/// @note 如果文字宽度本身就没超过 label 宽度(也即无需滚动),此时必定不会显示渐隐,则这个属性不会影响文字的显示位置。 +@property(nonatomic, assign) IBInspectable BOOL textStartAfterFade; +@end + + +/// 如果在可复用的 UIView 里使用(例如 UITableViewCell、UICollectionViewCell),由于 UIView 可能重复被使用,因此需要在某些显示/隐藏的时机去手动开启/关闭 label 的动画。如果在普通的 UIView 里使用则无需关注这一部分的代码。 +@interface QMUIMarqueeLabel (ReusableView) + +/** + * 尝试开启 label 的滚动动画 + * @return 是否成功开启 + */ +- (BOOL)requestToStartAnimation; + +/** + * 尝试停止 label 的滚动动画 + * @return 是否成功停止 + */ +- (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 new file mode 100644 index 00000000..c377270e --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMarqueeLabel.m @@ -0,0 +1,314 @@ +/** + * 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. + */ + +// +// QMUIMarqueeLabel.m +// qmui +// +// Created by QMUI Team on 2017/5/31. +// + +#import "QMUIMarqueeLabel.h" +#import "QMUICore.h" +#import "CALayer+QMUI.h" +#import "NSString+QMUI.h" + +@interface QMUIMarqueeLabel () + +@property(nonatomic, strong) CADisplayLink *displayLink; +@property(nonatomic, assign) CGFloat offsetX; +@property(nonatomic, assign) CGSize textSize; +@property(nonatomic, assign) CGFloat fadeStartPercent; // 渐变开始的百分比,默认为0,不建议改 +@property(nonatomic, assign) CGFloat fadeEndPercent; // 渐变结束的百分比,例如0.2,则表示 0~20% 是渐变区间 + +@property(nonatomic, assign) BOOL isFirstDisplay; + +@property(nonatomic, strong) CAGradientLayer *fadeLayer; + +/// 绘制文本时重复绘制的次数,用于实现首尾连接的滚动效果,1 表示不首尾连接,大于 1 表示首尾连接。 +@property(nonatomic, assign) NSInteger textRepeatCount; + +/// 记录上一次布局时的 bounds,如果有改变,则需要重置动画 +@property(nonatomic, assign) CGRect prevBounds; + +@end + +@implementation QMUIMarqueeLabel + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.lineBreakMode = NSLineBreakByClipping; + self.clipsToBounds = YES;// 显示非英文字符时,滚动的时候字符会稍微露出两端,所以这里直接裁剪掉 + + [self didInitialize]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self didInitialize]; + } + return self; +} + +- (void)didInitialize { + self.speed = .5; + self.fadeStartPercent = 0; + self.fadeEndPercent = .2; + self.pauseDurationWhenMoveToEdge = 2.5; + self.spacingBetweenHeadToTail = 40; + self.automaticallyValidateVisibleFrame = YES; + self.shouldFadeAtEdge = YES; + self.textStartAfterFade = NO; + + self.isFirstDisplay = YES; + self.textRepeatCount = 2; +} + +- (void)dealloc { + [self.displayLink invalidate]; + self.displayLink = nil; +} + +- (void)didMoveToWindow { + [super didMoveToWindow]; + if (self.window) { + self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)]; + [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + } else { + [self.displayLink invalidate]; + self.displayLink = nil; + } + + // 需要手动触发一下 setter,否则在 xib 赋值 text 后不生效 + self.attributedText = self.attributedText; +} + +- (void)setFadeWidthPercent:(CGFloat)fadeWidthPercent { + if (!betweenOrEqual(0.0, fadeWidthPercent, 1.0)) { + return; + } + _fadeWidthPercent = fadeWidthPercent; + + self.fadeEndPercent = fadeWidthPercent; +} + +- (void)setText:(NSString *)text { + [super setText:text]; + self.offsetX = 0; + self.textSize = [self sizeThatFits:CGSizeMax]; + self.displayLink.paused = ![self shouldPlayDisplayLink]; + [self checkIfShouldShowGradientLayer]; + [self setNeedsLayout]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText { + [super setAttributedText:attributedText]; + self.offsetX = 0; + self.textSize = [self sizeThatFits:CGSizeMax]; + self.displayLink.paused = ![self shouldPlayDisplayLink]; + [self checkIfShouldShowGradientLayer]; + [self setNeedsLayout]; +} + +- (void)drawTextInRect:(CGRect)rect { + CGFloat textInitialX = 0; + if (self.textAlignment == NSTextAlignmentLeft) { + textInitialX = 0; + } else if (self.textAlignment == NSTextAlignmentCenter) { + textInitialX = MAX(0, CGFloatGetCenter(CGRectGetWidth(self.bounds), self.textSize.width)); + } else if (self.textAlignment == NSTextAlignmentRight) { + textInitialX = MAX(0, CGRectGetWidth(self.bounds) - self.textSize.width); + } + + // 考虑渐变遮罩的偏移 + CGFloat textOffsetXByFade = 0; + BOOL shouldTextStartAfterFade = self.shouldFadeAtEdge && self.textStartAfterFade && self.textSize.width > CGRectGetWidth(self.bounds); + CGFloat fadeWidth = CGRectGetWidth(self.bounds) * .5 * MAX(0, self.fadeEndPercent - self.fadeStartPercent); + if (shouldTextStartAfterFade && textInitialX < fadeWidth) { + textOffsetXByFade = fadeWidth; + } + textInitialX += textOffsetXByFade; + + for (NSInteger i = 0; i < self.textRepeatCountConsiderTextWidth; i++) { + [self.attributedText drawInRect:CGRectMake(self.offsetX + (self.textSize.width + self.spacingBetweenHeadToTail) * i + textInitialX, CGRectGetMinY(rect) + CGFloatGetCenter(CGRectGetHeight(rect), self.textSize.height), self.textSize.width, self.textSize.height)]; + } + + // 自定义绘制就不需要调用 super +// [super drawTextInRect:rectToDrawAfterAnimated]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + if (self.fadeLayer) { + self.fadeLayer.frame = self.bounds; + } + + if (!CGSizeEqualToSize(self.prevBounds.size, self.bounds.size)) { + self.offsetX = 0; + self.displayLink.paused = ![self shouldPlayDisplayLink]; + self.prevBounds = self.bounds; + + [self checkIfShouldShowGradientLayer]; + } +} + +- (NSInteger)textRepeatCountConsiderTextWidth { + if (self.textSize.width < CGRectGetWidth(self.bounds)) { + return 1; + } + return self.textRepeatCount; +} + +- (void)handleDisplayLink:(CADisplayLink *)displayLink { + if (self.offsetX == 0) { + displayLink.paused = YES; + [self setNeedsDisplay]; + + int64_t delay = (self.isFirstDisplay || self.textRepeatCount <= 1) ? self.pauseDurationWhenMoveToEdge : 0; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + displayLink.paused = ![self shouldPlayDisplayLink]; + if (!displayLink.paused) { + self.offsetX -= self.speed; + } + }); + + if (delay > 0 && self.textRepeatCount > 1) { + self.isFirstDisplay = NO; + } + + return; + } + + self.offsetX -= self.speed; + [self setNeedsDisplay]; + + if (-self.offsetX >= self.textSize.width + (self.textRepeatCountConsiderTextWidth > 1 ? self.spacingBetweenHeadToTail : 0)) { + displayLink.paused = YES; + int64_t delay = self.textRepeatCount > 1 ? self.pauseDurationWhenMoveToEdge : 0; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + self.offsetX = 0; + [self handleDisplayLink:displayLink]; + }); + } +} + +- (BOOL)shouldPlayDisplayLink { + BOOL result = self.window && CGRectGetWidth(self.bounds) > 0 && self.textSize.width > CGRectGetWidth(self.bounds); + + // 如果 label.frame 在 window 可视区域之外,也视为不可见,暂停掉 displayLink + if (result && self.automaticallyValidateVisibleFrame) { + CGRect rectInWindow = [self.window convertRect:self.frame fromView:self.superview]; + if (!CGRectIntersectsRect(self.window.bounds, rectInWindow)) { + return NO; + } + } + + return result; +} + +- (void)setShouldFadeAtEdge:(BOOL)shouldFadeAtEdge { + _shouldFadeAtEdge = shouldFadeAtEdge; + + [self checkIfShouldShowGradientLayer]; + [self setNeedsLayout]; +} + +- (void)checkIfShouldShowGradientLayer { + BOOL shouldShowFadeLayer = self.window && self.shouldFadeAtEdge && CGRectGetWidth(self.bounds) > 0 && self.textSize.width > CGRectGetWidth(self.bounds); + + if (shouldShowFadeLayer) { + _fadeLayer = [CAGradientLayer layer]; + self.fadeLayer.locations = @[@(self.fadeStartPercent), @(self.fadeEndPercent), @(1 - self.fadeEndPercent), @(1 - self.fadeStartPercent)]; + self.fadeLayer.startPoint = CGPointMake(0, .5); + 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; + } + } +} + +#pragma mark - Superclass + +- (void)setNumberOfLines:(NSInteger)numberOfLines { + numberOfLines = 1; + [super setNumberOfLines:numberOfLines]; +} + +@end + +@implementation QMUIMarqueeLabel (ReusableView) + +- (BOOL)requestToStartAnimation { + self.automaticallyValidateVisibleFrame = NO; + BOOL shouldPlayDisplayLink = [self shouldPlayDisplayLink]; + if (shouldPlayDisplayLink) { + self.displayLink.paused = NO; + } + return shouldPlayDisplayLink; +} + +- (BOOL)requestToStopAnimation { + self.displayLink.paused = YES; + return YES; +} + +@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 new file mode 100644 index 00000000..b44011fa --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h @@ -0,0 +1,331 @@ +/** + * 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. + */ + +// +// QMUIModalPresentationViewController.h +// qmui +// +// Created by QMUI Team on 16/7/6. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIModalPresentationViewController; +@class QMUIModalPresentationWindow; + +typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) { + QMUIModalPresentationAnimationStyleFade, // 渐现渐隐,默认 + QMUIModalPresentationAnimationStylePopup, // 从中心点弹出 + QMUIModalPresentationAnimationStyleSlide // 从下往上升起 +}; + +@protocol QMUIModalPresentationContentViewControllerProtocol + +@optional + +/** + * 当浮层以 UIViewController 的形式展示(而非 UIView),并且使用 modalController 提供的默认布局时,则可通过这个方法告诉 modalController 当前浮层期望的大小。如果 modalController 实现了自己的 layoutBlock,则可不实现这个方法,实现了也不一定按照这个方法的返回值来布局,完全取决于 layoutBlock。 + * @param controller 当前的modalController + * @param keyboardHeight 当前的键盘高度,如果键盘降下,则为0 + * @param limitSize 浮层最大的宽高,由当前 modalController 的大小及 `contentViewMargins`、`maximumContentViewWidth` 和键盘高度决定 + * @return 返回浮层在 `limitSize` 限定内的大小,如果业务自身不需要限制宽度/高度,则为 width/height 返回 `CGFLOAT_MAX` 即可 + */ +- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller keyboardHeight:(CGFloat)keyboardHeight limitSize:(CGSize)limitSize; + +@end + +@protocol QMUIModalPresentationViewControllerDelegate + +@optional + +/** + * 是否应该隐藏浮层,默认为YES,会在代码主动调用隐藏,或点击背景遮罩时询问。 + * @param controller 当前的 modalController + * @return 是否允许隐藏,YES 表示允许隐藏,NO 表示不允许隐藏 + */ +- (BOOL)shouldHideModalPresentationViewController:(QMUIModalPresentationViewController *)controller; + +/** + * modalController 即将隐藏时的回调方法,在调用完这个方法后才开始做一些隐藏前的准备工作,例如恢复 window 的 dimmed 状态等。 + * @param controller 当前的modalController + */ +- (void)willHideModalPresentationViewController:(QMUIModalPresentationViewController *)controller; + +/** + * modalController隐藏后的回调方法,不管是直接调用`hideWithAnimated:completion:`,还是通过点击遮罩触发的隐藏,都会调用这个方法。 + * 如果你想区分这两种方式的隐藏回调,请直接使用hideWithAnimated方法的completion参数,以及`didHideByDimmingViewTappedBlock`属性。 + * @param controller 当前的modalController + */ +- (void)didHideModalPresentationViewController:(QMUIModalPresentationViewController *)controller; + +@end + +/** + * 一个提供通用的弹出浮层功能的控件,可以将任意`UIView`或`UIViewController`以浮层的形式显示出来并自动布局。 + * + * 支持 3 种方式显示浮层: + * + * 1. **推荐** 新起一个 `UIWindow` 盖在当前界面上,将 `QMUIModalPresentationViewController` 以 `rootViewController` 的形式显示出来,可通过 `supportedOrientationMask` 支持横竖屏,不支持在浮层不消失的情况下做界面切换(因为 window 会把背后的 controller 盖住,看不到界面切换)。 + * 可通过 shownInWindowMode 属性来判断是否在用这种方式显示。 + * @code + * [modalPresentationViewController showWithAnimated:YES completion:nil]; + * @endcode + * + * 2. 使用系统接口来显示,支持界面切换,**注意** 使用这种方法必定只能以动画的形式来显示浮层,无法以无动画的形式来显示,并且 `animated` 参数必须为 `NO`。可通过 `supportedOrientationMask` 支持横竖屏。 + * 可通过 shownInPresentedMode 属性来判断是否在用这种方式显示。 + * @code + * [self presentViewController:modalPresentationViewController animated:NO completion:nil]; + * @endcode + * + * 3. 将浮层作为一个 subview 添加到 `superview` 上,从而能够实现在浮层不消失的情况下进行界面切换,但需要 `superview` 自行管理浮层的大小和横竖屏旋转,而且 `QMUIModalPresentationViewController` 不能用局部变量来保存,会在显示后被释放,需要自行 retain。横竖屏跟随当前界面的设置。 + * 可通过 shownInSubviewMode 属性来判断是否在用这种方式显示。 + * @code + * self.modalPresentationViewController.view.frame = CGRectMake(50, 50, 100, 100); + * [self.view addSubview:self.modalPresentationViewController.view]; + * @endcode + * + * 默认的布局会将浮层居中显示,浮层的大小可通过接口控制: + * 1. 如果是用 `contentViewController`,则可通过 `preferredContentSizeInModalPresentationViewController:keyboardHeight:limitSize:` 来设置 + * 2. 如果使用 `contentView`,或者使用 `contentViewController` 但没实现 `preferredContentSizeInModalPresentationViewController:keyboardHeight:limitSize:`,则调用`contentView`的`sizeThatFits:`方法获取大小。 + * 3. 浮层大小会受 `maximumContentViewWidth` 属性的限制,以及 `contentViewMargins` 属性的影响。 + * + * 通过`layoutBlock`、`showingAnimation`、`hidingAnimation`可设置自定义的布局、打开及隐藏的动画,并允许你适配键盘升起时的场景。 + * + * 默认提供背景遮罩`dimmingView`,你也可以使用自己的遮罩 view。 + * + * 默认提供多种显示动画,可通过 `animationStyle` 来设置。 + * + * @warning 如果使用者retain了modalPresentationViewController,注意应该在`hideWithAnimated:completion:`里release + * + * @see QMUIAlertController + * @see QMUIDialogViewController + * @see QMUIMoreOperationController + */ +@interface QMUIModalPresentationViewController : UIViewController + +@property(nullable, nonatomic, weak) IBOutlet id delegate; + +/** + * 要被弹出的浮层 + * @warning 当设置了`contentView`时,不要再设置`contentViewController` + */ +@property(nullable, nonatomic, strong) IBOutlet UIView *contentView; + +/** + * 要被弹出的浮层,适用于浮层以UIViewController的形式来管理的情况。 + * @warning 当设置了`contentViewController`时,`contentViewController.view`会被当成`contentView`使用,因此不要再自行设置`contentView` + * @warning 注意`contentViewController`是强引用,容易导致循环引用,使用时请注意 + */ +@property(nullable, nonatomic, strong) IBOutlet UIViewController *contentViewController; + +/** + * 设置`contentView`布局时与外容器的间距,默认为(20, 20, 20, 20) + * @warning 当设置了`layoutBlock`属性时,此属性不生效 + */ +@property(nonatomic, assign) UIEdgeInsets contentViewMargins UI_APPEARANCE_SELECTOR; + +/** + * 限制`contentView`布局时的最大宽度,默认为 CGFLOAT_MAX,也即无限制。 + * @warning 当设置了`layoutBlock`属性时,此属性不生效 + */ +@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`的大小将会盖满整个控件。 + * + * `QMUIModalPresentationViewController`会自动给自定义的`dimmingView`添加手势以实现点击遮罩隐藏浮层。 + */ +@property(nullable, nonatomic, strong) IBOutlet UIView *dimmingView; + +/** + * 由于点击遮罩导致浮层即将被隐藏的回调 + */ +@property(nullable, nonatomic, copy) void (^willHideByDimmingViewTappedBlock)(void); + +/** + * 由于点击遮罩导致浮层被隐藏后的回调(区分于`hideWithAnimated:completion:`里的completion,这里是特地用于点击遮罩的情况) + */ +@property(nullable, nonatomic, copy) void (^didHideByDimmingViewTappedBlock)(void); + +/** + * 控制当前是否以模态的形式存在。如果以模态的形式存在,则点击空白区域不会隐藏浮层。 + * + * 默认为NO,也即点击空白区域将会自动隐藏浮层。 + */ +@property(nonatomic, assign, getter=isModal) BOOL modal; + +/** + * 标志当前浮层的显示/隐藏状态,默认为NO。 + */ +@property(nonatomic, assign, readonly, getter=isVisible) BOOL visible; + +/** + * 修改当前界面要支持的横竖屏方向,默认为 SupportedOrientationMask。 + */ +@property(nonatomic, assign) UIInterfaceOrientationMask supportedOrientationMask; + +/** + * 设置要使用的显示/隐藏动画的类型,默认为`QMUIModalPresentationAnimationStyleFade`。 + * @warning 当使用了`showingAnimation`和`hidingAnimation`时,该属性无效 + */ +@property(nonatomic, assign) QMUIModalPresentationAnimationStyle animationStyle UI_APPEARANCE_SELECTOR; + +/// 是否以 UIWindow 的方式显示,建议在显示之后才使用,否则可能不准确。 +@property(nonatomic, assign, readonly, getter=isShownInWindowMode) BOOL shownInWindowMode; + +/// 是否以系统 present 的方式显示,建议在显示之后才使用,否则可能不准确。 +@property(nonatomic, assign, readonly, getter=isShownInPresentedMode) BOOL shownInPresentedMode; + +/// 是否以 addSubview 的方式显示,建议在显示之后才使用,否则可能不准确。 +@property(nonatomic, assign, readonly, getter=isShownInSubviewMode) BOOL shownInSubviewMode; + +/// 只响应 modal.view 上的 view 所产生的键盘事件,当为 NO 时,只要有键盘事件产生,浮层都会重新计算布局。 +/// 默认为 YES,也即只响应浮层上的 view 引起的键盘位置变化。 +@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:`的影响 + * + * @see contentViewMargins + * @see maximumContentViewWidth + */ +@property(nullable, nonatomic, copy) void (^layoutBlock)(CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewDefaultFrame); + +/** + * 管理自定义的显示动画,需要管理的对象包括`contentView`和`dimmingView`,在`showingAnimation`被调用前,`contentView`已被添加到界面上。若使用了`layoutBlock`,则会先调用`layoutBlock`,再调用`showingAnimation`。在动画结束后,必须调用参数里的`completion` block。 + * @arg dimmingView 背景遮罩的View,请自行设置显示遮罩的动画 + * @arg containerBounds 浮层所在的父容器的大小,也即`self.view.bounds` + * @arg keyboardHeight 键盘在当前界面里的高度,若无键盘,则为0 + * @arg contentViewFrame 动画执行完后`contentView`的最终frame,若使用了`layoutBlock`,则也即`layoutBlock`计算完后的frame + * @arg completion 动画结束后给到modalController的回调,modalController会在这个回调里做一些状态设置,务必调用。 + */ +@property(nullable, nonatomic, copy) void (^showingAnimation)(UIView * _Nullable dimmingView, CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewFrame, void(^completion)(BOOL finished)); + +/** + * 管理自定义的隐藏动画,需要管理的对象包括`contentView`和`dimmingView`,在动画结束后,必须调用参数里的`completion` block。 + * @arg dimmingView 背景遮罩的View,请自行设置隐藏遮罩的动画 + * @arg containerBounds 浮层所在的父容器的大小,也即`self.view.bounds` + * @arg keyboardHeight 键盘在当前界面里的高度,若无键盘,则为0 + * @arg completion 动画结束后给到modalController的回调,modalController会在这个回调里做一些清理工作,务必调用 + */ +@property(nullable, nonatomic, copy) void (^hidingAnimation)(UIView * _Nullable dimmingView, CGRect containerBounds, CGFloat keyboardHeight, void(^completion)(BOOL finished)); + +/** + * 请求重新计算浮层的布局 + */ +- (void)updateLayout; + +/** + * 将浮层以 UIWindow 的方式显示出来 + * @param animated 是否以动画的形式显示 + * @param completion 显示动画结束后的回调 + */ +- (void)showWithAnimated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion; + +/** + * 将浮层隐藏掉 + * @param animated 是否以动画的形式隐藏 + * @param completion 隐藏动画结束后的回调 + * @warning 这里的`completion`只会在你显式调用`hideWithAnimated:completion:`方法来隐藏浮层时会被调用,如果你通过点击`dimmingView`来触发`hideWithAnimated:completion:`,则completion是不会被调用的,那种情况下如果你要在浮层隐藏后做一些事情,请使用`delegate`提供的`didHideModalPresentationViewController:`方法。 + */ +- (void)hideWithAnimated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion; + +/** + * 将浮层以 addSubview 的方式显示出来 + * + * @param view 要显示到哪个 view 上 + * @param animated 是否以动画的形式显示 + * @param completion 显示动画结束后的回调 + */ +- (void)showInView:(UIView *)view animated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion; + +/** + * 将某个 view 上显示的浮层隐藏掉 + * @param view 要隐藏哪个 view 上的浮层 + * @param animated 是否以动画的形式隐藏 + * @param completion 隐藏动画结束后的回调 + * @warning 这里的`completion`只会在你显式调用`hideInView:animated:completion:`方法来隐藏浮层时会被调用,如果你通过点击`dimmingView`来触发`hideInView:animated:completion:`,则completion是不会被调用的,那种情况下如果你要在浮层隐藏后做一些事情,请使用`delegate`提供的`didHideModalPresentationViewController:`方法。 + */ +- (void)hideInView:(UIView *)view animated:(BOOL)animated completion:(void (^ _Nullable)(BOOL finished))completion; + +@end + + +/** + * 如果你有一个控件,内部通过 QMUIModalPresentationViewController 实现显隐功能,那么这个控件建议实现这个协议,这样当 + [QMUIModalPresentationViewController hideAllVisibleModalPresentationViewControllerIfCan] 被调用的时候,可以通过 hideModalPresentationComponent 来隐藏你的控件,否则会直接调用 QMUIModalPresentationViewController 的 hide 方法,那样可能导致你的控件无法正确被隐藏。 + */ +@protocol QMUIModalPresentationComponentProtocol + +@required +- (void)hideModalPresentationComponent; + +@end + + +@interface QMUIModalPresentationViewController (Manager) + +/** + * 判断当前App里是否有modalViewController正在显示(存在modalViewController但不可见的时候,也视为不存在) + * @return 只要存在正在显示的浮层,则返回YES,否则返回NO + */ ++ (BOOL)isAnyModalPresentationViewControllerVisible; + +/** + * 把所有正在显示的并且允许被隐藏的modalViewController都隐藏掉 + * @return 只要遇到一个正在显示的并且不能被隐藏的浮层,就会返回NO,否则都返回YES,表示成功隐藏掉所有可视浮层 + * @see shouldHideModalPresentationViewController: + * @see QMUIModalPresentationComponentProtocol + * @warning 当要隐藏一个 modalPresentationViewController 时,如果这个 modal 有实现 QMUIModalPresentationComponentProtocol 协议,则会调用它的 hideModalPresentationComponent 方法来隐藏,否则直接用 QMUIModalPresentationViewController 的 hideWithAnimated:completion: + */ ++ (BOOL)hideAllVisibleModalPresentationViewControllerIfCan; +@end + +@interface QMUIModalPresentationViewController (UIAppearance) + ++ (instancetype)appearance; + +@end + +/// 专用于QMUIModalPresentationViewController的UIWindow,这样才能在`UIApplication.sharedApplication.windows`里方便地区分出来 +@interface QMUIModalPresentationWindow : UIWindow + +@end + + +@interface UIViewController (QMUIModalPresentationViewController) + +/** + * 获取弹出当前 vieController 的 QMUIModalPresentationViewController + */ +@property(nullable, nonatomic, weak, readonly) QMUIModalPresentationViewController *qmui_modalPresentationViewController; +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m b/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m new file mode 100644 index 00000000..5355412f --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m @@ -0,0 +1,849 @@ +/** + * 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. + */ + +// +// QMUIModalPresentationViewController.m +// qmui +// +// Created by QMUI Team on 16/7/6. +// + +#import "QMUIModalPresentationViewController.h" +#import "QMUICore.h" +#import "UIViewController+QMUI.h" +#import "UIView+QMUI.h" +#import "QMUIKeyboardManager.h" +#import "UIWindow+QMUI.h" +#import "QMUIAppearance.h" + +@interface UIViewController () + +@property(nonatomic, weak, readwrite) QMUIModalPresentationViewController *qmui_modalPresentationViewController; +@end + +@implementation QMUIModalPresentationViewController (UIAppearance) + ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self initAppearance]; + }); +} + ++ (void)initAppearance { + QMUIModalPresentationViewController *appearance = QMUIModalPresentationViewController.appearance; + appearance.animationStyle = QMUIModalPresentationAnimationStyleFade; + appearance.contentViewMargins = UIEdgeInsetsMake(20, 20, 20, 20); + appearance.maximumContentViewWidth = CGFLOAT_MAX; +} + +@end + +@interface QMUIModalPresentationViewController () + +@property(nonatomic, strong, readwrite) QMUIModalPresentationWindow *window; +@property(nonatomic, weak) UIWindow *previousKeyWindow; + +@property(nonatomic, assign, readwrite, getter=isVisible) BOOL visible; + +@property(nonatomic, assign) BOOL appearAnimated; +@property(nonatomic, copy) void (^appearCompletionBlock)(BOOL finished); + +@property(nonatomic, assign) BOOL disappearAnimated; +@property(nonatomic, copy) void (^disappearCompletionBlock)(BOOL finished); + +/// 标志 modal 本身以 present 的形式显示之后,又再继续 present 了一个子界面后从子界面回来时触发的 viewWillAppear: +@property(nonatomic, assign) BOOL viewWillAppearByPresentedViewController; + +/// 标志是否已经走过一次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 + +- (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 { + [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 { + [super awakeFromNib]; + if (self.contentViewController) { + // 在 IB 里设置了 contentViewController 的话,通过这个调用去触发 contentView 的更新 + self.contentViewController = self.contentViewController; + } +} + +- (void)dealloc { + self.window = nil; +} + +- (BOOL)shouldAutomaticallyForwardAppearanceMethods { + // 屏蔽对childViewController的生命周期函数的自动调用,改为手动控制 + return NO; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + if (self.dimmingView && !self.dimmingView.superview) { + [self.view addSubview:self.dimmingView]; + } +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + self.dimmingView.frame = self.view.bounds; + + CGRect contentViewFrame = [self contentViewFrameForShowing]; + if (self.layoutBlock) { + self.layoutBlock(self.view.bounds, self.keyboardHeight, contentViewFrame); + } else { + self.contentView.qmui_frameApplyTransform = contentViewFrame; + } +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + self.visible = YES;// present 模式没有入口 show 方法,只能加在这里 + + if (self.shownInWindowMode) { + // 只有使用showWithAnimated:completion:显示出来的浮层,才需要修改之前就记住的animated的值 + animated = self.appearAnimated; + } + + if (self.contentViewController) { + [self.contentViewController beginAppearanceTransition:YES animated:animated]; + } + + // 如果是因为 present 了新的界面再从那边回来,导致走到 viewWillAppear,则后面那些升起浮层的操作都可以不用做了,因为浮层从来没被降下去过 + self.viewWillAppearByPresentedViewController = [self isShowingPresentedViewController]; + if (self.viewWillAppearByPresentedViewController) { + return; + } + + void (^didShownCompletion)(BOOL finished) = ^(BOOL finished) { + if (self.contentViewController) { + [self.contentViewController endAppearanceTransition]; + } + + if (self.appearCompletionBlock) { + self.appearCompletionBlock(finished); + self.appearCompletionBlock = nil; + } + + self.appearAnimated = NO; + }; + + if (animated) { + [self.view addSubview:self.contentView]; + [self.view layoutIfNeeded]; + + CGRect contentViewFrame = [self contentViewFrameForShowing]; + if (self.showingAnimation) { + // 使用自定义的动画 + if (self.layoutBlock) { + self.layoutBlock(self.view.bounds, self.keyboardHeight, contentViewFrame); + 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]; + [self.contentView layoutIfNeeded]; + + [self showingAnimationWithCompletion:didShownCompletion]; + } + } else { + if (self.shouldDimmedAppAutomatically) { + [QMUIHelper dimmedApplicationWindow]; + } + CGRect contentViewFrame = [self contentViewFrameForShowing]; + self.contentView.frame = contentViewFrame; + [self.view addSubview:self.contentView]; + didShownCompletion(YES); + } +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + if (self.viewWillAppearByPresentedViewController) { + if (self.contentViewController) { + [self.contentViewController endAppearanceTransition]; + } + } +} + +- (void)viewWillDisappear:(BOOL)animated { + if (self.hasAlreadyViewWillDisappear) { + 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) { + animated = self.disappearAnimated; + } + + BOOL willDisappearByPresentedViewController = [self isShowingPresentedViewController]; + + if (!willDisappearByPresentedViewController) { + if ([self.delegate respondsToSelector:@selector(willHideModalPresentationViewController:)]) { + [self.delegate willHideModalPresentationViewController:self]; + } + } + + // 先更新标志位再 endEditing,保证键盘降下时不触发 updateLayout,从而避免影响 hidingAnimation 的动画 + self.avoidKeyboardLayout = YES; + [self.view endEditing:YES]; + + if (self.contentViewController) { + [self.contentViewController beginAppearanceTransition:NO animated:animated]; + } + + // 如果是因为 present 了新的界面导致走到 willDisappear,则后面那些降下浮层的操作都可以不用做了 + if (willDisappearByPresentedViewController) { + return; + } + + void (^didHiddenCompletion)(BOOL finished) = ^(BOOL finished) { + + if (self.shownInWindowMode) { + // 恢复 keyWindow 之前做一下检查,避免这个问题 https://github.com/Tencent/QMUI_iOS/issues/90 + 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]; + } else { + [self.previousKeyWindow makeKeyWindow]; + } + } + self.window.hidden = YES; + self.window.rootViewController = nil; + self.previousKeyWindow = nil; + [self endAppearanceTransition]; + } + + if (self.shownInSubviewMode) { + self.willHideInView = NO; + + [self.view removeFromSuperview]; + + // removeFromSuperview 在 animated:YES 时会触发第二次viewWillDisappear:,所以要搭配self.hasAlreadyViewWillDisappear使用 + // animated:NO 不会触发 + if (animated) { + self.hasAlreadyViewWillDisappear = NO; + } + } + + [self.contentView removeFromSuperview]; + if (self.contentViewController) { + [self.contentViewController endAppearanceTransition]; + } + + self.visible = NO; + self.avoidKeyboardLayout = NO; + + if ([self.delegate respondsToSelector:@selector(didHideModalPresentationViewController:)]) { + [self.delegate didHideModalPresentationViewController:self]; + } + + if (self.disappearCompletionBlock) { + self.disappearCompletionBlock(YES); + self.disappearCompletionBlock = 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); + } +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + BOOL willDisappearByPresentedViewController = [self isShowingPresentedViewController]; + if (willDisappearByPresentedViewController) { + if (self.contentViewController) { + [self.contentViewController endAppearanceTransition]; + } + } +} + +- (void)updateLayout { + if ([self isViewLoaded]) { + [self.view setNeedsLayout]; + [self.view layoutIfNeeded]; + } +} + +- (BOOL)shouldDimmedAppAutomatically { + return _shouldDimmedAppAutomatically && self.isShownInWindowMode; +} + +#pragma mark - Dimming View + +- (void)setDimmingView:(UIView *)dimmingView { + if (![self isViewLoaded]) { + _dimmingView = dimmingView; + } else { + [self.view insertSubview:dimmingView belowSubview:_dimmingView]; + [_dimmingView removeFromSuperview]; + _dimmingView = dimmingView; + [self.view setNeedsLayout]; + } + [self addTapGestureRecognizerToDimmingViewIfNeeded]; +} + +- (void)initDefaultDimmingViewWithoutAddToView { + if (!self.dimmingView) { + _dimmingView = [[UIView alloc] init]; + self.dimmingView.backgroundColor = UIColorMask; + [self addTapGestureRecognizerToDimmingViewIfNeeded]; + if ([self isViewLoaded]) { + [self.view addSubview:self.dimmingView]; + } + } +} + +// 要考虑用户可能创建了自己的dimmingView,则tap手势也要重新添加上去 +- (void)addTapGestureRecognizerToDimmingViewIfNeeded { + if (!self.dimmingView) { + return; + } + + if (self.dimmingViewTapGestureRecognizer.view == self.dimmingView) { + return; + } + + if (!self.dimmingViewTapGestureRecognizer) { + self.dimmingViewTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDimmingViewTapGestureRecognizer:)]; + } + [self.dimmingView addGestureRecognizer:self.dimmingViewTapGestureRecognizer]; + self.dimmingView.userInteractionEnabled = YES;// UIImageView默认userInteractionEnabled为NO,为了兼容UIImageView,这里必须主动设置为YES +} + +- (void)handleDimmingViewTapGestureRecognizer:(UITapGestureRecognizer *)tapGestureRecognizer { + if (self.modal) { + return; + } + + if (self.shownInWindowMode) { + __weak __typeof(self)weakSelf = self; + [self hideWithAnimated:YES completion:^(BOOL finished) { + 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(); + } + }]; + } else if (self.shownInSubviewMode) { + __weak __typeof(self)weakSelf = self; + [self hideInView:self.view.superview animated:YES completion:^(BOOL finished) { + 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; +} + +#pragma mark - Showing and Hiding + +- (void)showingAnimationWithCompletion:(void (^)(BOOL))completion { + if (self.animationStyle == QMUIModalPresentationAnimationStyleFade) { + self.dimmingView.alpha = 0.0; + self.contentView.alpha = 0.0; + [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); + } + }]; + + } else if (self.animationStyle == QMUIModalPresentationAnimationStylePopup) { + self.dimmingView.alpha = 0.0; + self.contentView.transform = CGAffineTransformMakeScale(0, 0); + [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) { + self.contentView.transform = CGAffineTransformIdentity; + if (completion) { + completion(finished); + } + }]; + + } 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:.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); + } + }]; + } +} + +- (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.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; + } +} + +- (void)hidingAnimationWithCompletion:(void (^)(BOOL))completion { + if (self.animationStyle == QMUIModalPresentationAnimationStyleFade) { + [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:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + self.dimmingView.alpha = 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:.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); + } + }]; + } +} + +- (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; + + BOOL shouldHide = YES; + if ([self.delegate respondsToSelector:@selector(shouldHideModalPresentationViewController:)]) { + shouldHide = [self.delegate shouldHideModalPresentationViewController:self]; + } + if (!shouldHide) { + return; + } + + if (sender == self.dimmingViewTapGestureRecognizer) { + if (self.willHideByDimmingViewTappedBlock) { + self.willHideByDimmingViewTappedBlock(); + } + } + + // window模式下,通过手动触发viewWillDisappear:来做界面消失的逻辑 + if (self.shownInWindowMode) { + [self beginAppearanceTransition:NO animated:animated]; + } +} + +- (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]; + [view addSubview:self.view]; + [self endAppearanceTransition]; +} + +- (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]; + if (animated) { + self.hasAlreadyViewWillDisappear = YES; + } + [self endAppearanceTransition]; +} + +- (CGRect)contentViewFrameForShowing { + CGSize contentViewContainerSize = CGSizeMake(CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(self.contentViewMargins), CGRectGetHeight(self.view.bounds) - self.keyboardHeight - UIEdgeInsetsGetVerticalValue(self.contentViewMargins)); + CGSize contentViewLimitSize = CGSizeMake(fmin(self.maximumContentViewWidth, contentViewContainerSize.width), contentViewContainerSize.height); + CGSize contentViewSize = CGSizeZero; + if ([self.contentViewController respondsToSelector:@selector(preferredContentSizeInModalPresentationViewController:keyboardHeight:limitSize:)]) { + contentViewSize = [self.contentViewController preferredContentSizeInModalPresentationViewController:self keyboardHeight:self.keyboardHeight limitSize:contentViewLimitSize]; + } else { + contentViewSize = [self.contentView sizeThatFits:contentViewLimitSize]; + } + contentViewSize.width = fmin(contentViewLimitSize.width, contentViewSize.width); + contentViewSize.height = fmin(contentViewLimitSize.height, contentViewSize.height); + CGRect contentViewFrame = CGRectMake(CGFloatGetCenter(contentViewContainerSize.width, contentViewSize.width) + self.contentViewMargins.left, CGFloatGetCenter(contentViewContainerSize.height, contentViewSize.height) + self.contentViewMargins.top, contentViewSize.width, contentViewSize.height); + return contentViewFrame; +} + +- (BOOL)isShownInWindowMode { + return !!self.window; +} + +- (BOOL)isShownInPresentedMode { + return !self.shownInWindowMode && self.presentingViewController && self.presentingViewController.presentedViewController == self; +} + +- (BOOL)isShownInSubviewMode { + return !self.shownInWindowMode && !self.shownInPresentedMode && self.view.superview; +} + +- (BOOL)isShowingPresentedViewController { + return self.shownInPresentedMode && self.presentedViewController && self.presentedViewController.presentingViewController == self; +} + +#pragma mark - + +- (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (self.onlyRespondsToKeyboardEventFromDescendantViews) { + UIResponder *firstResponder = keyboardUserInfo.targetResponder; + if (!firstResponder || !([firstResponder isKindOfClass:[UIView class]] && [(UIView *)firstResponder isDescendantOfView:self.view])) { + return; + } + } + CGFloat keyboardHeight = [keyboardUserInfo heightInView:self.view]; + if (self.keyboardHeight != keyboardHeight) { + self.keyboardHeight = keyboardHeight; + if (!self.avoidKeyboardLayout) { + [self updateLayout]; + } + } +} + +#pragma mark - 屏幕旋转 + +- (BOOL)shouldAutorotate { + UIViewController *visibleViewController = [QMUIHelper visibleViewController]; + if (visibleViewController != self && [visibleViewController respondsToSelector:@selector(shouldAutorotate)]) { + return [visibleViewController shouldAutorotate]; + } + return YES; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + UIViewController *visibleViewController = [QMUIHelper visibleViewController]; + if (visibleViewController != self && [visibleViewController respondsToSelector:@selector(supportedInterfaceOrientations)]) { + return [visibleViewController supportedInterfaceOrientations]; + } + return self.supportedOrientationMask; +} + +- (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]; + } +} + +// 当以 present 方式显示浮层时,状态栏允许由 contentViewController 控制,但 QMUIModalPresentationViewController 的 qmui_prefersStatusBarHiddenBlock/qmui_preferredStatusBarStyleBlock 优先级会更高 +- (UIViewController *)childViewControllerForStatusBarHidden { + 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; + } + return [super childViewControllerForHomeIndicatorAutoHidden]; +} + +@end + +@implementation QMUIModalPresentationViewController (Manager) + ++ (BOOL)isAnyModalPresentationViewControllerVisible { + for (UIWindow *window in UIApplication.sharedApplication.windows) { + if ([window isKindOfClass:[QMUIModalPresentationWindow class]] && !window.hidden) { + return YES; + } + } + return NO; +} + ++ (BOOL)hideAllVisibleModalPresentationViewControllerIfCan { + + BOOL hideAllFinally = YES; + + for (UIWindow *window in UIApplication.sharedApplication.windows) { + if (![window isKindOfClass:[QMUIModalPresentationWindow class]]) { + continue; + } + + // 存在modalViewController,但并没有显示出来,所以不用处理 + if (window.hidden) { + continue; + } + + // 存在window,但不存在modalViewController,则直接把这个window移除 + if (!window.rootViewController) { + window.hidden = YES; + continue; + } + + QMUIModalPresentationViewController *modalViewController = (QMUIModalPresentationViewController *)window.rootViewController; + BOOL canHide = YES; + if ([modalViewController.delegate respondsToSelector:@selector(shouldHideModalPresentationViewController:)]) { + canHide = [modalViewController.delegate shouldHideModalPresentationViewController:modalViewController]; + } + if (canHide) { + // 如果某些控件的显隐能力是通过 QMUIModalPresentationViewController 实现的,那么隐藏它们时,应该用它们自己的 hide 方法,而不是 QMUIModalPresentationViewController 自带的 hideWithAnimated:completion: + id modalPresentationComponent = nil; + if ([modalViewController.contentViewController conformsToProtocol:@protocol(QMUIModalPresentationComponentProtocol)]) { + modalPresentationComponent = (id)modalViewController.contentViewController; + } else if ([modalViewController.contentView conformsToProtocol:@protocol(QMUIModalPresentationComponentProtocol)]) { + modalPresentationComponent = (id)modalViewController.contentView; + } + if (modalPresentationComponent) { + [modalPresentationComponent hideModalPresentationComponent]; + } else { + [modalViewController hideWithAnimated:NO completion:nil]; + } + } else { + // 只要有一个modalViewController正在显示但却无法被隐藏,就返回NO + hideAllFinally = NO; + } + } + + return hideAllFinally; +} + +@end + +@implementation QMUIModalPresentationWindow + +@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 new file mode 100644 index 00000000..f84addaa --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.h @@ -0,0 +1,156 @@ +/** + * 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. + */ + +// +// QMUIMoreOperationController.h +// qmui +// +// Created by QMUI Team on 17/11/15. +// + +#import +#import +#import "QMUIModalPresentationViewController.h" +#import "QMUIButton.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIMoreOperationController; +@class QMUIMoreOperationItemView; + +@protocol QMUIMoreOperationControllerDelegate + +@optional + +/// 即将显示操作面板 +- (void)willPresentMoreOperationController:(QMUIMoreOperationController *)moreOperationController; + +/// 已经显示操作面板 +- (void)didPresentMoreOperationController:(QMUIMoreOperationController *)moreOperationController; + +/// 即将降下操作面板,cancelled参数是用来区分是否触发了maskView或者cancelButton按钮降下面板还是手动调用hide方法来降下面板。 +- (void)willDismissMoreOperationController:(QMUIMoreOperationController *)moreOperationController cancelled:(BOOL)cancelled; + +/// 已经降下操作面板,cancelled参数是用来区分是否触发了maskView或者cancelButton按钮降下面板还是手动调用hide方法来降下面板。 +- (void)didDismissMoreOperationController:(QMUIMoreOperationController *)moreOperationController cancelled:(BOOL)cancelled; + +/// itemView 点击事件,可以与 itemView.handler 共存,可通过 itemView.tag 或者 itemView.indexPath 来区分不同的 itemView +- (void)moreOperationController:(QMUIMoreOperationController *)moreOperationController didSelectItemView:(QMUIMoreOperationItemView *)itemView; +@end + + +/** + * 更多操作面板控件,类似系统的相册分享面板,以及微信的 webview 分享面板。功能特性包括: + * 1. 支持多行,每行支持多个 item,由二维数组 items 控制。 + * 2. 默认自带取消按钮,也可自行隐藏。 + * 3. 支持以 UIAppearance 的方式配置样式皮肤。 + */ +@interface QMUIMoreOperationController : UIViewController + +@property(nullable, nonatomic, strong) UIColor *contentBackgroundColor 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 之后的区域 + +@property(nullable, nonatomic, strong) UIColor *scrollViewSeparatorColor UI_APPEARANCE_SELECTOR;// 每一行之间的顶部分隔线,对第一行无效 +@property(nonatomic, assign) UIEdgeInsets scrollViewContentInsets UI_APPEARANCE_SELECTOR;// 每一行内部的 padding + +@property(nullable, nonatomic, strong) UIColor *itemBackgroundColor UI_APPEARANCE_SELECTOR;// 按钮的背景色 +@property(nullable, nonatomic, strong) UIColor *itemTitleColor UI_APPEARANCE_SELECTOR;// 按钮的标题颜色 +@property(nullable, nonatomic, strong) UIFont *itemTitleFont UI_APPEARANCE_SELECTOR;// 按钮的标题字体 +@property(nonatomic, assign) CGFloat itemPaddingHorizontal UI_APPEARANCE_SELECTOR;// 按钮内 imageView 的左右间距(按钮宽度 = 图片宽度 + 左右间距 * 2),通常用来调整文字的宽度 +@property(nonatomic, assign) CGFloat itemTitleMarginTop UI_APPEARANCE_SELECTOR;// 按钮标题距离文字之间的间距 +@property(nonatomic, assign) CGFloat itemMinimumMarginHorizontal UI_APPEARANCE_SELECTOR;// 按钮与按钮之间的最小间距 +@property(nonatomic, assign) BOOL automaticallyAdjustItemMargins UI_APPEARANCE_SELECTOR;// 是否要自动计算默认一行展示多少个 item,YES 表示尽量让每一行末尾露出半个 item 暗示后面还有内容,NO 表示直接根据 itemMinimumMarginHorizontal 来计算布局。默认为 YES。 + +@property(nullable, nonatomic, strong) UIColor *cancelButtonBackgroundColor UI_APPEARANCE_SELECTOR;// 取消按钮的背景色 +@property(nullable, nonatomic, strong) UIColor *cancelButtonTitleColor UI_APPEARANCE_SELECTOR;// 取消按钮的标题颜色 +@property(nullable, nonatomic, strong) UIColor *cancelButtonSeparatorColor UI_APPEARANCE_SELECTOR;// 取消按钮的顶部分隔线颜色 +@property(nullable, nonatomic, strong) UIFont *cancelButtonFont UI_APPEARANCE_SELECTOR;// 取消按钮的字体 +@property(nonatomic, assign) CGFloat cancelButtonHeight UI_APPEARANCE_SELECTOR;// 取消按钮的高度 +@property(nonatomic, assign) CGFloat cancelButtonMarginTop UI_APPEARANCE_SELECTOR;// 取消按钮距离内容面板的间距 + +@property(nullable, nonatomic, weak) id delegate; + +@property(nullable, nonatomic, strong, readonly) UIView *contentView;// 放 UIScrollView 的容器,与 cancelButton 区分开 +@property(nullable, nonatomic, copy, readonly) NSArray *scrollViews;// 获取当前的所有 UIScrollView + +/// 取消按钮,如果不需要,则自行设置其 hidden 为 YES +@property(nullable, nonatomic, strong, readonly) QMUIButton *cancelButton; + +/// 在 iPhoneX 机器上是否延伸底部背景色。因为在 iPhoneX 上我们会把整个面板往上移动 safeArea 的距离,如果你的面板本来就配置成撑满全屏的样式,那么就会露出底部的空隙,isExtendBottomLayout 可以帮助你把空暇填补上。默认为NO。 +@property(nonatomic, assign) BOOL isExtendBottomLayout UI_APPEARANCE_SELECTOR; + +@property(nullable, nonatomic, copy) NSArray *> *items; + +/// 添加一个 itemView 到指定 section 的末尾 +- (void)addItemView:(QMUIMoreOperationItemView *)itemView inSection:(NSInteger)section; + +/// 插入一个 itemView 到指定的位置,NSIndexPath 请使用 section-item 组合,其中 section 表示行,item 表示 section 里的元素序号 +- (void)insertItemView:(QMUIMoreOperationItemView *)itemView atIndexPath:(NSIndexPath *)indexPath; + +/// 移除指定位置的 itemView,NSIndexPath 请使用 section-item 组合,其中 section 表示行,item 表示 section 里的元素序号 +- (void)removeItemViewAtIndexPath:(NSIndexPath *)indexPath; + +/// 获取指定 tag 的 itemView,如果不存在则返回 nil +- (QMUIMoreOperationItemView * _Nullable)itemViewWithTag:(NSInteger)tag; + +/// 获取指定 itemView 在当前控件里的 indexPath,如果不存在则返回 nil +- (NSIndexPath * _Nullable)indexPathWithItemView:(QMUIMoreOperationItemView *)itemView; + +/// 弹出面板,一般在 init 完并且设置好 items 之后就调用这个接口来显示面板 +- (void)showFromBottom; + +/// 隐藏面板 +- (void)hideToBottom; + +/// 更多操作面板是否正在显示 +@property(nonatomic, assign, getter=isShowing, readonly) BOOL showing; +@property(nonatomic, assign, getter=isAnimating, readonly) BOOL animating; + +@end + + +@interface QMUIMoreOperationController (UIAppearance) + ++ (instancetype)appearance; + +@end + + +@interface QMUIMoreOperationItemView : QMUIButton + +@property(nullable, nonatomic, strong, readonly) NSIndexPath *indexPath; +@property(nonatomic, assign) NSInteger tag; + ++ (instancetype)itemViewWithImage:(UIImage * _Nullable)image + title:(NSString * _Nullable)title + handler:(void (^ _Nullable)(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView))handler; + ++ (instancetype)itemViewWithImage:(UIImage * _Nullable)image + selectedImage:(UIImage * _Nullable)selectedImage + title:(NSString * _Nullable)title + selectedTitle:(NSString * _Nullable)selectedTitle + handler:(void (^ _Nullable)(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView))handler; + ++ (instancetype)itemViewWithImage:(UIImage * _Nullable)image + title:(NSString * _Nullable)title + tag:(NSInteger)tag + handler:(void (^ _Nullable)(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView))handler; + ++ (instancetype)itemViewWithImage:(UIImage * _Nullable)image + selectedImage:(UIImage * _Nullable)selectedImage + title:(NSString * _Nullable)title + selectedTitle:(NSString * _Nullable)selectedTitle + tag:(NSInteger)tag + handler:(void (^ _Nullable)(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView))handler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.m b/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.m new file mode 100644 index 00000000..269b7a46 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMoreOperationController.m @@ -0,0 +1,742 @@ +/** + * 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. + */ + +// +// QMUIMoreOperationController.m +// qmui +// +// Created by QMUI Team on 17/11/15. +// + +#import "QMUIMoreOperationController.h" +#import "QMUICore.h" +#import "CALayer+QMUI.h" +#import "UIControl+QMUI.h" +#import "UIView+QMUI.h" +#import "NSArray+QMUI.h" +#import "UIScrollView+QMUI.h" +#import "QMUILog.h" +#import "QMUIAppearance.h" + +static NSInteger const kQMUIMoreOperationItemViewTagOffset = 999; + +@interface QMUIMoreOperationItemView () { + NSInteger _tag; +} + +@property(nonatomic, weak) QMUIMoreOperationController *moreOperationController; +@property(nonatomic, copy) void (^handler)(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView); + +// 被添加到某个 QMUIMoreOperationController 时要调用,用于更新 itemView 的样式,以及 moreOperationController 属性的指针 +// @param moreOperationController 如果为空,则会自动使用 [QMUIMoreOperationController appearance] +- (void)formatItemViewStyleWithMoreOperationController:(QMUIMoreOperationController *)moreOperationController; +@end + +@implementation QMUIMoreOperationController (UIAppearance) + ++ (instancetype)appearance { + return [QMUIAppearance appearanceForClass:self]; +} + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self initAppearance]; + }); +} + ++ (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 + +@interface QMUIMoreOperationController () + +@property(nonatomic, strong) NSMutableArray *mutableScrollViews; +@property(nonatomic, strong) NSMutableArray *> *mutableItems; +@property(nonatomic, strong) CALayer *extendLayer; + +@property(nonatomic, assign, getter=isShowing, readwrite) BOOL showing; +@property(nonatomic, assign, getter=isAnimating, readwrite) BOOL animating; +@property(nonatomic, assign) BOOL hideByCancel; // 是否通过点击取消按钮或者遮罩来隐藏面板,默认为 NO + +@end + +@implementation QMUIMoreOperationController + +- (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 { + [self qmui_applyAppearance]; + + 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]; + + [self.view addSubview:self.contentView]; + [self.view addSubview:self.cancelButton]; + + self.extendLayer = [CALayer layer]; + self.extendLayer.hidden = !self.isExtendBottomLayout; + [self.extendLayer qmui_removeDefaultAnimations]; + [self.view.layer addSublayer:self.extendLayer]; + [self updateExtendLayerAppearance]; + + [self updateCornerRadius]; +} + +- (NSArray *)scrollViews { + return [self.mutableScrollViews copy]; +} + +#pragma mark - Layout + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + __block CGFloat layoutY = CGRectGetHeight(self.view.bounds); + + if (!self.extendLayer.hidden) { + self.extendLayer.frame = CGRectMake(0, layoutY, CGRectGetWidth(self.view.bounds), SafeAreaInsetsConstantForDeviceWithNotch.bottom); + if (self.view.clipsToBounds) { + QMUILog(@"QMUIMoreOperationController", @"%@ 需要显示 extendLayer,但却被父级 clip 掉了,可能看不到", NSStringFromClass(self.class)); + } + } + + BOOL isCancelButtonShowing = !self.cancelButton.hidden; + if (isCancelButtonShowing) { + self.cancelButton.frame = CGRectMake(0, layoutY - self.cancelButtonHeight, CGRectGetWidth(self.view.bounds), self.cancelButtonHeight); + [self.cancelButton setNeedsLayout]; + layoutY = CGRectGetMinY(self.cancelButton.frame) - self.cancelButtonMarginTop; + } + + self.contentView.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), layoutY); + layoutY = self.contentPaddings.top; + CGFloat contentWidth = CGRectGetWidth(self.contentView.bounds) - UIEdgeInsetsGetHorizontalValue(self.contentPaddings); + + [self.mutableScrollViews enumerateObjectsUsingBlock:^(UIScrollView * _Nonnull scrollView, NSUInteger idx, BOOL * _Nonnull stop) { + scrollView.frame = CGRectMake(self.contentPaddings.left, layoutY, contentWidth, CGRectGetHeight(scrollView.frame)); + + // 要保护 safeAreaInsets 的区域,而这里不使用 scrollView.safeAreaInsets 是因为此时 scrollView 的 safeAreaInsets 仍然为 0,但 scrollView.superview.safeAreaInsets 已经正确了,所以使用 scrollView.superview 也即 self.view 的 + // 底部的 insets 暂不考虑 +// 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; + CGFloat exampleItemWidth = exampleItemView.imageView.image.size.width + self.itemPaddingHorizontal * 2; + CGFloat scrollViewVisibleWidth = contentWidth - scrollView.contentInset.left - scrollViewSafeAreaInsets.left;// 注意计算列数时不需要考虑 contentInset.right 的 + CGFloat columnCount = (scrollViewVisibleWidth + self.itemMinimumMarginHorizontal) / (exampleItemWidth + self.itemMinimumMarginHorizontal); + + // 让初始状态下在 scrollView 右边露出半个 item + if (self.automaticallyAdjustItemMargins) { + columnCount = [self suitableColumnCountWithCount:columnCount]; + } + + CGFloat finalItemMarginHorizontal = flat((scrollViewVisibleWidth - exampleItemWidth * columnCount) / columnCount); + + __block CGFloat maximumItemHeight = 0; + __block CGFloat itemViewMinX = scrollViewSafeAreaInsets.left; + [itemSection enumerateObjectsUsingBlock:^(QMUIMoreOperationItemView * _Nonnull itemView, NSUInteger idx, BOOL * _Nonnull stop) { + CGSize itemSize = CGSizeFlatted([itemView sizeThatFits:CGSizeMake(exampleItemWidth, CGFLOAT_MAX)]); + maximumItemHeight = fmax(maximumItemHeight, itemSize.height); + itemView.frame = CGRectMake(itemViewMinX, 0, exampleItemWidth, itemSize.height); + itemViewMinX = CGRectGetMaxX(itemView.frame) + finalItemMarginHorizontal; + }]; + scrollView.contentSize = CGSizeMake(itemViewMinX - finalItemMarginHorizontal + scrollViewSafeAreaInsets.right, maximumItemHeight); + scrollView.frame = CGRectSetHeight(scrollView.frame, scrollView.contentSize.height + UIEdgeInsetsGetVerticalValue(scrollView.contentInset)); + layoutY = CGRectGetMaxY(scrollView.frame); + }]; +} + +- (CGFloat)suitableColumnCountWithCount:(CGFloat)columnCount { + // 根据精准的列数,找到一个合适的、能让半个 item 刚好露出来的列数。例如 3.6 会被转换成 3.5,3.2 会被转换成 2.5。 + CGFloat result = round(columnCount) - .5;; + return result; +} + +- (void)showFromBottom { + + if (self.showing || self.animating) { + return; + } + + self.hideByCancel = YES; + + __weak __typeof(self)weakSelf = self; + + QMUIModalPresentationViewController *modalPresentationViewController = [[QMUIModalPresentationViewController alloc] init]; + modalPresentationViewController.delegate = self; + modalPresentationViewController.maximumContentViewWidth = self.contentMaximumWidth; + modalPresentationViewController.contentViewMargins = self.contentEdgeMargins; + modalPresentationViewController.contentViewController = self; + + __weak __typeof(modalPresentationViewController)weakModalController = modalPresentationViewController; + modalPresentationViewController.layoutBlock = ^(CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewDefaultFrame) { + 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)) { + + if ([weakSelf.delegate respondsToSelector:@selector(willPresentMoreOperationController:)]) { + [weakSelf.delegate willPresentMoreOperationController:weakSelf]; + } + + dimmingView.alpha = 0; + weakModalController.contentView.frame = CGRectSetY(contentViewFrame, CGRectGetHeight(containerBounds)); + [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^(void) { + dimmingView.alpha = 1; + weakModalController.contentView.frame = contentViewFrame; + } completion:^(BOOL finished) { + weakSelf.showing = YES; + weakSelf.animating = NO; + if ([weakSelf.delegate respondsToSelector:@selector(didPresentMoreOperationController:)]) { + [weakSelf.delegate didPresentMoreOperationController:weakSelf]; + } + if (completion) { + completion(finished); + } + }]; + }; + + modalPresentationViewController.hidingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, void(^completion)(BOOL finished)) { + [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^(void) { + dimmingView.alpha = 0; + weakModalController.contentView.frame = CGRectSetY(weakModalController.contentView.frame, CGRectGetHeight(containerBounds)); + } completion:^(BOOL finished) { + if (completion) { + completion(finished); + } + }]; + }; + + self.animating = YES; + [modalPresentationViewController showWithAnimated:YES completion:NULL]; +} + +- (void)hideToBottom { + if (!self.showing || self.animating) { + return; + } + self.hideByCancel = NO; + [self.qmui_modalPresentationViewController hideWithAnimated:YES completion:NULL]; +} + +#pragma mark - Item + +- (void)setItems:(NSArray *> *)items { + [self.mutableItems qmui_enumerateNestedArrayWithBlock:^(QMUIMoreOperationItemView *itemView, BOOL *stop) { + [itemView removeFromSuperview]; + }]; + [self.mutableItems removeAllObjects]; + + self.mutableItems = [items qmui_mutableCopyNestedArray]; + + [self.mutableScrollViews enumerateObjectsUsingBlock:^(UIScrollView * _Nonnull scrollView, NSUInteger idx, BOOL * _Nonnull stop) { + [scrollView removeFromSuperview]; + }]; + [self.mutableScrollViews removeAllObjects]; + [self.mutableItems enumerateObjectsUsingBlock:^(NSArray * _Nonnull itemViewSection, NSUInteger scrollViewIndex, BOOL * _Nonnull stop) { + UIScrollView *scrollView = [self addScrollViewAtIndex:scrollViewIndex]; + [itemViewSection enumerateObjectsUsingBlock:^(QMUIMoreOperationItemView * _Nonnull itemView, NSUInteger itemViewIndex, BOOL * _Nonnull stop) { + [self addItemView:itemView toScrollView:scrollView]; + }]; + }]; + [self setViewNeedsLayoutIfLoaded]; +} + +- (NSArray *> *)items { + return [self.mutableItems copy]; +} + +- (void)addItemView:(QMUIMoreOperationItemView *)itemView inSection:(NSInteger)section { + if (section == self.mutableItems.count) { + // 创建新的 itemView section + [self.mutableItems addObject:[@[itemView] mutableCopy]]; + } else { + [self.mutableItems[section] addObject:itemView]; + } + itemView.moreOperationController = self; + + if (section == self.mutableScrollViews.count) { + // 创建新的 section + [self addScrollViewAtIndex:section]; + } + if (section < self.mutableScrollViews.count) { + [self addItemView:itemView toScrollView:self.mutableScrollViews[section]]; + } + + [self setViewNeedsLayoutIfLoaded]; +} + +- (void)insertItemView:(QMUIMoreOperationItemView *)itemView atIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == self.mutableItems.count) { + // 创建新的 itemView section + [self.mutableItems addObject:[@[itemView] mutableCopy]]; + } else { + [self.mutableItems[indexPath.section] insertObject:itemView atIndex:indexPath.item]; + } + itemView.moreOperationController = self; + + if (indexPath.section == self.mutableScrollViews.count) { + // 创建新的 section + [self addScrollViewAtIndex:indexPath.section]; + } + if (indexPath.section < self.mutableScrollViews.count) { + [itemView formatItemViewStyleWithMoreOperationController:self]; + [self.mutableScrollViews[indexPath.section] insertSubview:itemView atIndex:indexPath.item]; + } + + [self setViewNeedsLayoutIfLoaded]; +} + +- (void)removeItemViewAtIndexPath:(NSIndexPath *)indexPath { + QMUIMoreOperationItemView *itemView = self.mutableScrollViews[indexPath.section].subviews[indexPath.item]; + itemView.moreOperationController = nil; + [itemView removeFromSuperview]; + NSMutableArray *itemViewSection = self.mutableItems[indexPath.section]; + [itemViewSection removeObject:itemView]; + if (itemViewSection.count == 0) { + [self.mutableItems removeObject:itemViewSection]; + [self.mutableScrollViews[indexPath.section] removeFromSuperview]; + [self.mutableScrollViews removeObjectAtIndex:indexPath.section]; + [self updateScrollViewsBorderStyle]; + } + [self setViewNeedsLayoutIfLoaded]; +} + +- (QMUIMoreOperationItemView *)itemViewWithTag:(NSInteger)tag { + __block QMUIMoreOperationItemView *result = nil; + [self.mutableItems qmui_enumerateNestedArrayWithBlock:^(QMUIMoreOperationItemView *itemView, BOOL *stop) { + if (itemView.tag == tag) { + result = itemView; + *stop = YES; + } + }]; + return result; +} + +- (NSIndexPath *)indexPathWithItemView:(QMUIMoreOperationItemView *)itemView { + for (NSInteger section = 0; section < self.mutableItems.count; section++) { + NSInteger index = [self.mutableItems[section] indexOfObject:itemView]; + if (index != NSNotFound) { + return [NSIndexPath indexPathForItem:index inSection:section]; + } + } + return nil; +} + +- (UIScrollView *)addScrollViewAtIndex:(NSInteger)index { + UIScrollView *scrollView = [self generateScrollViewWithIndex:index]; + [self.contentView addSubview:scrollView]; + [self.mutableScrollViews addObject:scrollView]; + [self updateScrollViewsBorderStyle]; + return scrollView; +} + +- (void)addItemView:(QMUIMoreOperationItemView *)itemView toScrollView:(UIScrollView *)scrollView { + [itemView formatItemViewStyleWithMoreOperationController:self]; + [scrollView addSubview:itemView]; +} + +- (UIScrollView *)generateScrollViewWithIndex:(NSInteger)index { + UIScrollView *scrollView = [[UIScrollView alloc] init]; + scrollView.showsHorizontalScrollIndicator = NO; + scrollView.showsVerticalScrollIndicator = NO; + scrollView.alwaysBounceHorizontal = YES; + scrollView.qmui_borderColor = self.scrollViewSeparatorColor; + scrollView.qmui_borderPosition = (self.scrollViewSeparatorColor && index != 0) ? QMUIViewBorderPositionTop : QMUIViewBorderPositionNone; + scrollView.scrollsToTop = NO; + scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + scrollView.contentInset = self.scrollViewContentInsets; + [scrollView qmui_scrollToTopForce:YES animated:NO]; + return scrollView; +} + +- (void)updateScrollViewsBorderStyle { + [self.mutableScrollViews enumerateObjectsUsingBlock:^(UIScrollView * _Nonnull scrollView, NSUInteger idx, BOOL * _Nonnull stop) { + scrollView.qmui_borderColor = self.scrollViewSeparatorColor; + scrollView.qmui_borderPosition = idx != 0 ? QMUIViewBorderPositionTop : QMUIViewBorderPositionNone; + }]; +} + +#pragma mark - Event + +- (void)handleCancelButtonEvent:(id)sender { + if (!self.showing || self.animating) { + return; + } + [self.qmui_modalPresentationViewController hideWithAnimated:YES completion:NULL]; +} + +- (void)handleItemViewEvent:(QMUIMoreOperationItemView *)itemView { + if ([self.delegate respondsToSelector:@selector(moreOperationController:didSelectItemView:)]) { + [self.delegate moreOperationController:self didSelectItemView:itemView]; + } + if (itemView.handler) { + itemView.handler(self, itemView); + } +} + +#pragma mark - Property setter + +- (void)setContentBackgroundColor:(UIColor *)contentBackgroundColor { + _contentBackgroundColor = contentBackgroundColor; + _contentView.backgroundColor = contentBackgroundColor; +} + +- (void)setScrollViewSeparatorColor:(UIColor *)scrollViewSeparatorColor { + _scrollViewSeparatorColor = scrollViewSeparatorColor; + [self updateScrollViewsBorderStyle]; +} + +- (void)setScrollViewContentInsets:(UIEdgeInsets)scrollViewContentInsets { + _scrollViewContentInsets = scrollViewContentInsets; + if (self.mutableScrollViews) { + for (UIScrollView *scrollView in self.mutableScrollViews) { + scrollView.contentInset = scrollViewContentInsets; + } + [self setViewNeedsLayoutIfLoaded]; + } +} + +- (void)setCancelButtonBackgroundColor:(UIColor *)cancelButtonBackgroundColor { + _cancelButtonBackgroundColor = cancelButtonBackgroundColor; + _cancelButton.backgroundColor = cancelButtonBackgroundColor; + [self updateExtendLayerAppearance]; +} + +- (void)setCancelButtonTitleColor:(UIColor *)cancelButtonTitleColor { + _cancelButtonTitleColor = cancelButtonTitleColor; + if (_cancelButton) { + [_cancelButton setTitleColor:cancelButtonTitleColor forState:UIControlStateNormal]; + [_cancelButton setTitleColor:[cancelButtonTitleColor colorWithAlphaComponent:ButtonHighlightedAlpha] forState:UIControlStateHighlighted]; + } +} + +- (void)setCancelButtonSeparatorColor:(UIColor *)cancelButtonSeparatorColor { + _cancelButtonSeparatorColor = cancelButtonSeparatorColor; + _cancelButton.qmui_borderColor = cancelButtonSeparatorColor; +} + +- (void)setItemBackgroundColor:(UIColor *)itemBackgroundColor { + _itemBackgroundColor = itemBackgroundColor; + [self.mutableItems qmui_enumerateNestedArrayWithBlock:^(QMUIMoreOperationItemView *itemView, BOOL *stop) { + itemView.imageView.backgroundColor = itemBackgroundColor; + }]; +} + +- (void)setItemTitleColor:(UIColor *)itemTitleColor { + _itemTitleColor = itemTitleColor; + [self.mutableItems qmui_enumerateNestedArrayWithBlock:^(QMUIMoreOperationItemView *itemView, BOOL *stop) { + [itemView setTitleColor:itemTitleColor forState:UIControlStateNormal]; + }]; +} + +- (void)setItemTitleFont:(UIFont *)itemTitleFont { + _itemTitleFont = itemTitleFont; + [self.mutableItems qmui_enumerateNestedArrayWithBlock:^(QMUIMoreOperationItemView *itemView, BOOL *stop) { + itemView.titleLabel.font = itemTitleFont; + [itemView setNeedsLayout]; + }]; +} + +- (void)setItemPaddingHorizontal:(CGFloat)itemPaddingHorizontal { + _itemPaddingHorizontal = itemPaddingHorizontal; + [self setViewNeedsLayoutIfLoaded]; +} + +- (void)setItemTitleMarginTop:(CGFloat)itemTitleMarginTop { + _itemTitleMarginTop = itemTitleMarginTop; + [self.mutableItems qmui_enumerateNestedArrayWithBlock:^(QMUIMoreOperationItemView *itemView, BOOL *stop) { + itemView.titleEdgeInsets = UIEdgeInsetsMake(itemTitleMarginTop, 0, 0, 0); + [itemView setNeedsLayout]; + }]; +} + +- (void)setItemMinimumMarginHorizontal:(CGFloat)itemMinimumMarginHorizontal { + _itemMinimumMarginHorizontal = itemMinimumMarginHorizontal; + [self setViewNeedsLayoutIfLoaded]; +} + +- (void)setAutomaticallyAdjustItemMargins:(BOOL)automaticallyAdjustItemMargins { + _automaticallyAdjustItemMargins = automaticallyAdjustItemMargins; + [self setViewNeedsLayoutIfLoaded]; +} + +- (void)setCancelButtonFont:(UIFont *)cancelButtonFont { + _cancelButtonFont = cancelButtonFont; + _cancelButton.titleLabel.font = cancelButtonFont; + [_cancelButton setNeedsLayout]; +} + +- (void)setContentCornerRadius:(CGFloat)contentCornerRadius { + _contentCornerRadius = contentCornerRadius; + [self updateCornerRadius]; +} + +- (void)setCancelButtonMarginTop:(CGFloat)cancelButtonMarginTop { + _cancelButtonMarginTop = cancelButtonMarginTop; + _cancelButton.qmui_borderPosition = cancelButtonMarginTop > 0 ? QMUIViewBorderPositionNone : QMUIViewBorderPositionTop; + [self updateCornerRadius]; + [self setViewNeedsLayoutIfLoaded]; +} + +- (void)setIsExtendBottomLayout:(BOOL)isExtendBottomLayout { + _isExtendBottomLayout = isExtendBottomLayout; + if (isExtendBottomLayout) { + self.extendLayer.hidden = NO; + [self updateExtendLayerAppearance]; + } else { + self.extendLayer.hidden = YES; + } +} + +- (void)setViewNeedsLayoutIfLoaded { + if (self.isShowing) { + [self.qmui_modalPresentationViewController updateLayout]; + [self.view setNeedsLayout]; + } else if ([self isViewLoaded]) { + [self.view setNeedsLayout]; + } +} + +- (void)updateExtendLayerAppearance { + self.extendLayer.backgroundColor = self.cancelButtonBackgroundColor.CGColor; +} + +- (void)updateCornerRadius { + if (self.cancelButtonMarginTop > 0) { + if (self.isViewLoaded) { + self.view.layer.cornerRadius = 0; + self.view.clipsToBounds = NO; + } + + _contentView.layer.cornerRadius = self.contentCornerRadius; + _cancelButton.layer.cornerRadius = self.contentCornerRadius; + } else { + 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; + } +} + +#pragma mark - + +- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller keyboardHeight:(CGFloat)keyboardHeight limitSize:(CGSize)limitSize { + __block CGFloat contentHeight = (self.cancelButton.hidden ? 0 : self.cancelButtonHeight + self.cancelButtonMarginTop); + [self.mutableScrollViews enumerateObjectsUsingBlock:^(UIScrollView * _Nonnull scrollView, NSUInteger idx, BOOL * _Nonnull stop) { + NSArray *itemSection = self.mutableItems[idx]; + QMUIMoreOperationItemView *exampleItemView = itemSection.firstObject; + CGFloat exampleItemWidth = exampleItemView.imageView.image.size.width + self.itemPaddingHorizontal * 2; + __block CGFloat maximumItemHeight = 0; + [itemSection enumerateObjectsUsingBlock:^(QMUIMoreOperationItemView * _Nonnull itemView, NSUInteger idx, BOOL * _Nonnull stop) { + CGSize itemSize = CGSizeFlatted([itemView sizeThatFits:CGSizeMake(exampleItemWidth, CGFLOAT_MAX)]); + maximumItemHeight = fmax(maximumItemHeight, itemSize.height); + }]; + contentHeight += maximumItemHeight + UIEdgeInsetsGetVerticalValue(scrollView.contentInset); + }]; + if (self.mutableScrollViews.count) { + contentHeight += UIEdgeInsetsGetVerticalValue(self.contentPaddings); + } + limitSize.height = contentHeight; + return limitSize; +} + +#pragma mark - + +- (void)willHideModalPresentationViewController:(QMUIModalPresentationViewController *)controller { + self.animating = YES; + if ([self.delegate respondsToSelector:@selector(willDismissMoreOperationController:cancelled:)]) { + [self.delegate willDismissMoreOperationController:self cancelled:self.hideByCancel]; + } +} + +- (void)didHideModalPresentationViewController:(QMUIModalPresentationViewController *)controller { + self.showing = NO; + self.animating = NO; + if ([self.delegate respondsToSelector:@selector(didDismissMoreOperationController:cancelled:)]) { + [self.delegate didDismissMoreOperationController:self cancelled:self.hideByCancel]; + } +} + +#pragma mark - + +- (void)hideModalPresentationComponent { + [self hideToBottom]; +} + +@end + +@implementation QMUIMoreOperationItemView + +@dynamic tag; + ++ (instancetype)itemViewWithImage:(UIImage *)image selectedImage:(UIImage *)selectedImage title:(NSString *)title selectedTitle:(NSString *)selectedTitle handler:(void (^)(QMUIMoreOperationController *, QMUIMoreOperationItemView *))handler { + QMUIMoreOperationItemView *itemView = [[self alloc] init]; + [itemView setImage:image forState:UIControlStateNormal]; + [itemView setImage:selectedImage forState:UIControlStateSelected]; + [itemView setImage:selectedImage forState:UIControlStateHighlighted|UIControlStateSelected]; + [itemView setTitle:title forState:UIControlStateNormal]; + [itemView setTitle:selectedTitle forState:UIControlStateHighlighted|UIControlStateSelected]; + [itemView setTitle:selectedTitle forState:UIControlStateSelected]; + itemView.handler = handler; + [itemView formatItemViewStyleWithMoreOperationController:nil]; + return itemView; +} + ++ (instancetype)itemViewWithImage:(UIImage *)image title:(NSString *)title handler:(void (^)(QMUIMoreOperationController *, QMUIMoreOperationItemView *))handler { + return [self itemViewWithImage:image selectedImage:nil title:title selectedTitle:nil handler:handler]; +} + ++ (instancetype)itemViewWithImage:(UIImage *)image + title:(NSString *)title + tag:(NSInteger)tag + handler:(void (^)(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView))handler { + QMUIMoreOperationItemView *itemView = [self itemViewWithImage:image title:title handler:handler]; + itemView.tag = tag; + return itemView; +} + ++ (instancetype)itemViewWithImage:(UIImage *)image + selectedImage:(UIImage *)selectedImage + title:(NSString *)title + selectedTitle:(NSString *)selectedTitle + tag:(NSInteger)tag + handler:(void (^)(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView))handler { + QMUIMoreOperationItemView *itemView = [self itemViewWithImage:image selectedImage:selectedImage title:title selectedTitle:selectedTitle handler:handler]; + itemView.tag = tag; + return itemView; +} + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.imagePosition = QMUIButtonImagePositionTop; + self.adjustsButtonWhenHighlighted = NO; + self.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + self.titleLabel.numberOfLines = 0; + self.titleLabel.textAlignment = NSTextAlignmentCenter; + self.imageView.contentMode = UIViewContentModeCenter; + } + return self; +} + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + self.imageView.alpha = highlighted ? ButtonHighlightedAlpha : 1; +} + +// 从某个指定的 QMUIMoreOperationController 里取 itemView 的样式,应用到当前 itemView 里 +- (void)formatItemViewStyleWithMoreOperationController:(QMUIMoreOperationController *)moreOperationController { + if (moreOperationController) { + // 将事件放到 controller 级别去做,以便实现 delegate 功能 + [self addTarget:moreOperationController action:@selector(handleItemViewEvent:) forControlEvents:UIControlEventTouchUpInside]; + } else { + // 参数 nil 则默认使用 appearance 的样式 + moreOperationController = [QMUIMoreOperationController appearance]; + } + self.titleLabel.font = moreOperationController.itemTitleFont; + self.titleEdgeInsets = UIEdgeInsetsMake(moreOperationController.itemTitleMarginTop, 0, 0, 0); + [self setTitleColor:moreOperationController.itemTitleColor forState:UIControlStateNormal]; + self.imageView.backgroundColor = moreOperationController.itemBackgroundColor; + +} + +- (void)setTag:(NSInteger)tag { + _tag = tag + kQMUIMoreOperationItemViewTagOffset; +} + +- (NSInteger)tag { + return MAX(-1, _tag - kQMUIMoreOperationItemViewTagOffset);// 为什么这里用-1而不是0:如果一个 itemView 通过带 tag: 参数初始化,那么 itemView.tag 最小值为 0,而如果一个 itemView 不通过带 tag: 的参数初始化,那么 itemView.tag 固定为 0,可见 tag 为 0 代表的意义不唯一,为了消除歧义,这里用 -1 代表那种不使用 tag: 参数初始化的 itemView +} + +- (NSIndexPath *)indexPath { + if (self.moreOperationController) { + return [self.moreOperationController indexPathWithItemView:self]; + } + return nil; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@:\t%p\nimage:\t\t\t%@\nselectedImage:\t%@\ntitle:\t\t\t%@\nselectedTitle:\t%@\nindexPath:\t\t%@\ntag:\t\t\t\t%@", NSStringFromClass(self.class), self, [self imageForState:UIControlStateNormal], [self imageForState:UIControlStateSelected] == [self imageForState:UIControlStateNormal] ? nil : [self imageForState:UIControlStateSelected], [self titleForState:UIControlStateNormal], [self titleForState:UIControlStateSelected] == [self titleForState:UIControlStateNormal] ? nil : [self titleForState:UIControlStateSelected], ({self.indexPath ? [NSString stringWithFormat:@"%@ - %@", @(self.indexPath.section), @(self.indexPath.item)] : nil;}), @(self.tag)]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h new file mode 100644 index 00000000..e891a641 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h @@ -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. + */ + +// +// NSObject+MultipleDelegates.h +// QMUIKit +// +// Created by QMUI Team on 2018/3/27. +// + +#import + +@class QMUIMultipleDelegates; + +/** + * 让所有 NSObject 都支持多个 delegate,默认只支持属性名为 delegate 的 delegate(特别地,UITableView 和 UICollectionView 额外默认支持 dataSource)。 + * 使用方式:将 qmui_multipleDelegatesEnabled 置为 YES 后像平时一样 self.delegate = xxx 即可。 + * 如果你要清掉所有的 delegate,则像平时一样 self.delegate = nil 即可。 + * 如果你把 delegate 同时赋值给 objA 和 objB,而你只要移除 objB,则可:[self qmui_removeDelegate:objB] + * + * 如果你要让其他命名的 delegate 属性也支持多 delegate,则可调用 qmui_registerDelegateSelector: 方法将该属性的 getter 传进去,再进行实际的 delegate 赋值,例如你的 delegate 命名为 abcDelegate,则你可以这么写: + * [self qmui_registerDelegateSelector:@selector(abcDelegate)]; + * self.abcDelegate = delegateA; + * self.abcDelegate = delegateB; + * + * @warning 不支持 self.delegate = self 的写法,会引发死循环,有这种需求的场景建议在 self 内部创建一个对象专门用于 delegate 的响应,参考 _QMUITextViewDelegator。 + */ +@interface NSObject (QMUIMultipleDelegates) + +/// 当你需要当前的 class 支持多个 delegate,请将此属性置为 YES。默认为 NO。 +@property(nonatomic, assign) BOOL qmui_multipleDelegatesEnabled; + +/// 让某个 delegate 属性也支持多 delegate 模式(默认只帮你加了 @selector(delegate) 的支持,如果有其他命名的 property 就需要自己用这个方法添加) +- (void)qmui_registerDelegateSelector:(SEL)getter; + +/// 移除某个特定的 delegate 对象,例如假设你把 delegate 同时赋值给 objA 和 objB,而你只要移除 objB,则可:[self qmui_removeDelegate:objB]。但如果你想同时移除 objA 和 objB(也即全部 delegate),则像往常一样直接 self.delegate = nil 即可。 +- (void)qmui_removeDelegate:(id)delegate; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m new file mode 100644 index 00000000..9b4b9531 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m @@ -0,0 +1,161 @@ +/** + * 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+MultipleDelegates.m +// QMUIKit +// +// Created by QMUI Team on 2018/3/27. +// + +#import "NSObject+QMUIMultipleDelegates.h" +#import "QMUIMultipleDelegates.h" +#import "QMUICore.h" +#import "NSPointerArray+QMUI.h" +#import "NSString+QMUI.h" + +@interface NSObject () + +@property(nonatomic, strong) NSMutableDictionary *qmuimd_delegates; +@end + +@implementation NSObject (QMUIMultipleDelegates) + +QMUISynthesizeIdStrongProperty(qmuimd_delegates, setQmuimd_delegates) + +static char kAssociatedObjectKey_qmuiMultipleDelegatesEnabled; +- (void)setQmui_multipleDelegatesEnabled:(BOOL)qmui_multipleDelegatesEnabled { + objc_setAssociatedObject(self, &kAssociatedObjectKey_qmuiMultipleDelegatesEnabled, @(qmui_multipleDelegatesEnabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_multipleDelegatesEnabled) { + if (!self.qmuimd_delegates) { + self.qmuimd_delegates = [NSMutableDictionary dictionary]; + } + [self qmui_registerDelegateSelector:@selector(delegate)]; + if ([self isKindOfClass:[UITableView class]] || [self isKindOfClass:[UICollectionView class]]) { + [self qmui_registerDelegateSelector:@selector(dataSource)]; + } + } +} + +- (BOOL)qmui_multipleDelegatesEnabled { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_qmuiMultipleDelegatesEnabled)) boolValue]; +} + +- (void)qmui_registerDelegateSelector:(SEL)getter { + if (!self.qmui_multipleDelegatesEnabled) { + return; + } + + Class targetClass = [self class]; + SEL originDelegateSetter = setterWithGetter(getter); + SEL newDelegateSetter = [self newSetterWithGetter:getter]; + Method originMethod = class_getInstanceMethod(targetClass, originDelegateSetter); + if (!originMethod) { + return; + } + + NSString *delegateGetterKey = NSStringFromSelector(getter); + + [QMUIHelper executeBlock:^{ + IMP originIMP = method_getImplementation(originMethod); + void (*originSelectorIMP)(id, SEL, id); + originSelectorIMP = (void (*)(id, SEL, id))originIMP; + + BOOL isAddedMethod = class_addMethod(targetClass, newDelegateSetter, imp_implementationWithBlock(^(NSObject *selfObject, id aDelegate){ + + // 这一段保护的原因请查看 https://github.com/Tencent/QMUI_iOS/issues/292 + if (!selfObject.qmui_multipleDelegatesEnabled || selfObject.class != targetClass) { + originSelectorIMP(selfObject, originDelegateSetter, aDelegate); + return; + } + + // 为这个 selector 创建一个 QMUIMultipleDelegates 容器 + QMUIMultipleDelegates *delegates = selfObject.qmuimd_delegates[delegateGetterKey]; + if (!aDelegate) { + // 对应 setDelegate:nil,表示清理所有的 delegate + 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]; + } + + originSelectorIMP(selfObject, originDelegateSetter, nil);// 先置为 nil 再设置 delegates,从而避免这个问题 https://github.com/Tencent/QMUI_iOS/issues/305 + originSelectorIMP(selfObject, originDelegateSetter, delegates);// 不管外面将什么 object 传给 setDelegate:,最终实际上传进去的都是 QMUIMultipleDelegates 容器 + + }), method_getTypeEncoding(originMethod)); + if (isAddedMethod) { + 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 + BeginIgnorePerformSelectorLeaksWarning + id originDelegate = [self performSelector:getter]; + if (originDelegate && originDelegate != self.qmuimd_delegates[delegateGetterKey]) { + [self performSelector:originDelegateSetter withObject:originDelegate]; + } + EndIgnorePerformSelectorLeaksWarning +} + +- (void)qmui_removeDelegate:(id)delegate { + if (!self.qmuimd_delegates) { + return; + } + NSMutableArray *delegateGetters = [[NSMutableArray alloc] init]; + [self.qmuimd_delegates enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, QMUIMultipleDelegates * _Nonnull obj, BOOL * _Nonnull stop) { + BOOL removeSucceed = [obj removeDelegate:delegate]; + if (removeSucceed) { + [delegateGetters addObject:key]; + } + }]; + if (delegateGetters.count > 0) { + for (NSString *getterString in delegateGetters) { + [self refreshDelegateWithGetter:NSSelectorFromString(getterString)]; + } + } +} + +- (void)refreshDelegateWithGetter:(SEL)getter { + SEL originSetterSEL = [self newSetterWithGetter:getter]; + BeginIgnorePerformSelectorLeaksWarning + id originDelegate = [self performSelector:getter]; + [self performSelector:originSetterSEL withObject:nil];// 先置为 nil 再设置 delegates,从而避免这个问题 https://github.com/Tencent/QMUI_iOS/issues/305 + [self performSelector:originSetterSEL withObject:originDelegate]; + EndIgnorePerformSelectorLeaksWarning +} + +// 根据 delegate property 的 getter,得到 QMUIMultipleDelegates 为它的 setter 创建的新 setter 方法,最终交换原方法,因此利用这个方法返回的 SEL,可以调用到原来的 delegate property setter 的实现 +- (SEL)newSetterWithGetter:(SEL)getter { + return NSSelectorFromString([NSString stringWithFormat:@"qmuimd_%@", NSStringFromSelector(setterWithGetter(getter))]); +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h new file mode 100644 index 00000000..c6c22146 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.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. + */ + +// +// QMUIMultipleDelegates.h +// QMUIKit +// +// Created by QMUI Team on 2018/3/27. +// + +#import +#import +#import "NSObject+QMUIMultipleDelegates.h" + +/// 存放多个 delegate 指针的容器,必须搭配其他控件使用,一般不需要你自己 init。作用是让某个 class 支持同时存在多个 delegate。更多说明请查看 NSObject (QMUIMultipleDelegates) 的注释。 +@interface QMUIMultipleDelegates : NSObject + ++ (instancetype)weakDelegates; ++ (instancetype)strongDelegates; + +@property(nonatomic, strong, readonly) NSPointerArray *delegates; +@property(nonatomic, weak) NSObject *parentObject; + +- (void)addDelegate:(id)delegate; +- (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 new file mode 100644 index 00000000..d825d4fd --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m @@ -0,0 +1,234 @@ +/** + * 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. + */ + +// +// QMUIMultipleDelegates.m +// QMUIKit +// +// Created by QMUI Team on 2018/3/27. +// + +#import "QMUIMultipleDelegates.h" +#import "NSPointerArray+QMUI.h" +#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 = [[self alloc] init]; + delegates.delegates = [NSPointerArray weakObjectsPointerArray]; + return delegates; +} + ++ (instancetype)strongDelegates { + 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]; + } +} + +- (BOOL)removeDelegate:(id)delegate { + NSUInteger index = [self.delegates qmui_indexOfPointer:(__bridge void *)delegate]; + if (index != NSNotFound) { + [self.delegates removePointerAtIndex:index]; + return YES; + } + return NO; +} + +- (void)removeAllDelegates { + for (NSInteger i = self.delegates.count - 1; i >= 0; i--) { + [self.delegates removePointerAtIndex:i]; + } +} + +- (BOOL)containsDelegate:(id)delegate { + return [self.delegates qmui_containsPointer:(__bridge void *)delegate]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { + NSMethodSignature *result = nil; + NSPointerArray *delegates = [self.delegates copy]; + for (id delegate in delegates) { + result = [delegate methodSignatureForSelector:aSelector]; + if (result && [delegate respondsToSelector:aSelector]) { + return result; + } + } + + return NSMethodSignature.qmui_avoidExceptionSignature; +} + +- (void)forwardInvocation:(NSInvocation *)anInvocation { + SEL selector = anInvocation.selector; + + // 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; + } + + NSPointerArray *delegates = [self.delegates copy]; + for (id delegate in delegates) { + if (class_respondsToSelector(self.class, aSelector)) { + return YES; + } + + // 对 QMUIMultipleDelegates 额外处理的解释在这里:https://github.com/Tencent/QMUI_iOS/issues/357 + 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 new file mode 100644 index 00000000..180f9163 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.h @@ -0,0 +1,192 @@ +/** + * 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. + */ + +// +// QMUINavigationTitleView.h +// qmui +// +// Created by QMUI Team on 14-7-2. +// + +#import +#import "QMUIButton.h" + +@class QMUINavigationTitleView; + +@protocol QMUINavigationTitleViewDelegate + +@optional + +/** + 点击 titleView 后的回调,只需设置 titleView.userInteractionEnabled = YES 后即可使用。不过一般都用于配合 QMUINavigationTitleViewAccessoryTypeDisclosureIndicator。 + + @param titleView 被点击的 titleView + @param isActive titleView 是否处于活跃状态(所谓的活跃,对应右边的箭头而言,就是点击后箭头向上的状态) + */ +- (void)didTouchTitleView:(QMUINavigationTitleView *)titleView isActive:(BOOL)isActive; + +/** + titleView 的活跃状态发生变化时会被调用,也即 [titleView setActive:] 被调用时。 + + @param active 是否处于活跃状态 + @param titleView 变换状态的 titleView + */ +- (void)didChangedActive:(BOOL)active forTitleView:(QMUINavigationTitleView *)titleView; + +@end + +/// 设置title和subTitle的布局方式,默认是水平布局。 +typedef NS_ENUM(NSInteger, QMUINavigationTitleViewStyle) { + QMUINavigationTitleViewStyleDefault, // 水平 + QMUINavigationTitleViewStyleSubTitleVertical // 垂直 +}; + +/// 设置titleView的样式,默认没有任何修饰 +typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) { + QMUINavigationTitleViewAccessoryTypeNone, // 默认 + QMUINavigationTitleViewAccessoryTypeDisclosureIndicator // 有下拉箭头 +}; + + +/** + * 可作为 UIViewController 顶部导航栏里的标题控件,通过 self.navigationItem.titleView 来设置。当调用 -[UIViewController setTitle:] 或 -[UINavigationItem setTitle:] 时,会自动更新 QMUINavigationTitleView 的内容。 + * + * 也可以当成单独的组件,脱离 UIViewController 使用,就跟普通组件一样。 + * + * 支持主副标题,且可控制主副标题的布局方式(水平或垂直);支持在左边显示loading,在右边显示accessoryView(如箭头)。 + * + * 默认情况下 titleView 是不支持点击的,需要支持点击的情况下,请把 `userInteractionEnabled` 设为 `YES`。 + * + * 若要监听 titleView 的点击事件,有两种方法: + * + * 1. 使用 UIControl 默认的 addTarget:action:forControlEvents: 方式。这种适用于单纯的点击,不需要涉及到状态切换等。 + * 2. 使用 QMUINavigationTitleViewDelegate 提供的接口。这种一般配合 titleView.accessoryType 来使用,这样就不用自己去做 accessoryView 的旋转、active 状态的维护等。 + */ +@interface QMUINavigationTitleView : UIControl + +@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 + +@property(nonatomic, strong, readonly) UILabel *titleLabel; +@property(nonatomic, copy) NSString *title; + +@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; + +/// 水平布局下的副标题的字体,默认为 NavBarTitleFont +@property(nonatomic, strong) UIFont *horizontalSubtitleFont UI_APPEARANCE_SELECTOR; + +/// 垂直布局下的标题字体,默认为 UIFontMake(15) +@property(nonatomic, strong) UIFont *verticalTitleFont UI_APPEARANCE_SELECTOR; + +/// 垂直布局下的副标题字体,默认为 UIFontLightMake(12) +@property(nonatomic, strong) UIFont *verticalSubtitleFont UI_APPEARANCE_SELECTOR; + +/// 标题的上下左右间距,当标题不显示时,计算大小及布局时也不考虑这个间距,默认为 UIEdgeInsetsZero +@property(nonatomic, assign) UIEdgeInsets titleEdgeInsets UI_APPEARANCE_SELECTOR; + +/// 副标题的上下左右间距,当副标题不显示时,计算大小及布局时也不考虑这个间距,默认为 UIEdgeInsetsZero +@property(nonatomic, assign) UIEdgeInsets subtitleEdgeInsets UI_APPEARANCE_SELECTOR; + +#pragma mark - Loading + +@property(nonatomic, strong, readonly) UIActivityIndicatorView *loadingView; + +/* + * 设置是否需要loading,只有开启了这个属性,loading才有可能显示出来。默认值为NO。 + */ +@property(nonatomic, assign) BOOL needsLoadingView; + +/* + * `needsLoadingView`开启之后,通过这个属性来控制loading的显示和隐藏,默认值为YES + * + * @see needsLoadingView + */ +@property(nonatomic, assign) BOOL loadingViewHidden; + +/* + * 如果为YES则title居中,loading放在title的左边,title右边有一个跟左边loading一样大的占位空间(目的是为了让切换 loading 时文字不跳动);如果为NO,loading和title整体居中。默认值为YES。 + */ +@property(nonatomic, assign) BOOL needsLoadingPlaceholderSpace; + +@property(nonatomic, assign) CGSize loadingViewSize UI_APPEARANCE_SELECTOR; + +/* + * 控制loading距离右边的距离 + */ +@property(nonatomic, assign) CGFloat loadingViewMarginRight UI_APPEARANCE_SELECTOR; + +#pragma mark - Accessory + +/* + * 当accessoryView不为空时,QMUINavigationTitleViewAccessoryType设置无效,一直都是None + */ +@property(nonatomic, strong) UIView *accessoryView; + +/* + * 只有当accessoryView为空时才有效 + */ +@property(nonatomic, assign) QMUINavigationTitleViewAccessoryType accessoryType; + +/* + * 用于微调accessoryView的位置 + */ +@property(nonatomic, assign) CGPoint accessoryViewOffset UI_APPEARANCE_SELECTOR; + +/* + * 如果为YES则title居中,`accessoryView`放在title的左边或右边;如果为NO,`accessoryView`和title整体居中。默认值为NO。 + */ +@property(nonatomic, assign) BOOL needsAccessoryPlaceholderSpace; + +/* + * 同 accessoryView,用于 subtitle 的 AccessoryView + * @warn 为了美观考虑,该属性只对 QMUINavigationTitleViewStyleSubTitleVertical 有效 + */ +@property(nonatomic, strong) UIView *subAccessoryView; + +/* + * 用于微调 subAccessoryView 的位置 + */ +@property(nonatomic, assign) CGPoint subAccessoryViewOffset UI_APPEARANCE_SELECTOR; + +/* + * 同 needsAccessoryPlaceholderSpace,用于 subtitle + */ +@property(nonatomic, assign) BOOL needsSubAccessoryPlaceholderSpace; + +/* + * 初始化方法 + */ +- (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 new file mode 100644 index 00000000..fb4a4bcd --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUINavigationTitleView.m @@ -0,0 +1,858 @@ +/** + * 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. + */ + +// +// QMUINavigationTitleView.m +// qmui +// +// Created by QMUI Team on 14-7-2. +// + +#import "QMUINavigationTitleView.h" +#import "QMUICore.h" +#import "UIFont+QMUI.h" +#import "UIImage+QMUI.h" +#import "UILabel+QMUI.h" +#import "UIActivityIndicatorView+QMUI.h" +#import "UIViewController+QMUI.h" +#import "UIView+QMUI.h" +#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; + +@end + +@implementation QMUINavigationTitleView + +#pragma mark - 初始化 + +- (instancetype)initWithFrame:(CGRect)frame { + return [self initWithStyle:QMUINavigationTitleViewStyleDefault frame:frame]; +} + +- (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style { + return [self initWithStyle:style frame:CGRectZero]; +} + +- (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.titleLabel.accessibilityTraits |= UIAccessibilityTraitHeader; + [self.contentView addSubview:self.titleLabel]; + + _subtitleLabel = [[UILabel alloc] init]; + self.subtitleLabel.textAlignment = NSTextAlignmentCenter; + self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingTail; + self.subtitleLabel.accessibilityTraits |= UIAccessibilityTraitHeader; + [self.contentView addSubview:self.subtitleLabel]; + + self.userInteractionEnabled = NO; + self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter; + self.style = style; + self.needsLoadingView = NO; + self.loadingViewHidden = YES; + self.needsAccessoryPlaceholderSpace = NO; + self.needsSubAccessoryPlaceholderSpace = NO; + self.needsLoadingPlaceholderSpace = YES; + self.accessoryType = QMUINavigationTitleViewAccessoryTypeNone; + + [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; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@, title = %@, subtitle = %@", [super description], self.title, self.subtitle]; +} + +#pragma mark - 布局 + +- (void)refreshLayout { + UINavigationBar *navigationBar = [self navigationBarSuperviewForSubview:self]; + if (navigationBar) { + [navigationBar setNeedsLayout]; + } + [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) { + return nil; + } + + if ([subview.superview isKindOfClass:[UINavigationBar class]]) { + return (UINavigationBar *)subview.superview; + } + + return [self navigationBarSuperviewForSubview:subview.superview]; +} + +- (void)updateTitleLabelSize { + if (self.titleLabel.text.length > 0) { + // 这里用 CGSizeCeil 是特地保证 titleView 的 sizeThatFits 计算出来宽度是 pt 取整,这样在 layoutSubviews 我们以 px 取整时,才能保证不会出现水平居中时出现半像素的问题,然后由于我们对半像素会认为一像素,所以导致总体宽度多了一像素,从而导致文字布局可能出现缩略... + self.titleLabelSize = CGSizeCeil([self.titleLabel sizeThatFits:CGSizeMax]); + } else { + self.titleLabelSize = CGSizeZero; + } +} + +- (void)updateSubtitleLabelSize { + if (self.subtitleLabel.text.length > 0) { + // 这里用 CGSizeCeil 是特地保证 titleView 的 sizeThatFits 计算出来宽度是 pt 取整,这样在 layoutSubviews 我们以 px 取整时,才能保证不会出现水平居中时出现半像素的问题,然后由于我们对半像素会认为一像素,所以导致总体宽度多了一像素,从而导致文字布局可能出现缩略... + self.subtitleLabelSize = CGSizeCeil([self.subtitleLabel sizeThatFits:CGSizeMax]); + } else { + self.subtitleLabelSize = CGSizeZero; + } +} + +- (CGSize)loadingViewSpacingSize { + 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 { + CGSize size = CGSizeMake([self loadingViewSpacingSize].width * (self.needsLoadingPlaceholderSpace ? 2 : 1), [self loadingViewSpacingSize].height); + return size; +} + +- (CGSize)accessorySpacingSize { + if (self.accessoryView || self.accessoryTypeView) { + UIView *view = self.accessoryView ?: self.accessoryTypeView; + return CGSizeMake(CGRectGetWidth(view.bounds) + self.accessoryViewOffset.x, CGRectGetHeight(view.bounds)); + } + return CGSizeZero; +} + +- (CGSize)subAccessorySpacingSize { + if (self.subAccessoryView) { + UIView *view = self.subAccessoryView; + return CGSizeMake(CGRectGetWidth(view.frame) + self.subAccessoryViewOffset.x, CGRectGetHeight(view.frame)); + } + return CGSizeZero; +} + +- (CGSize)accessorySpacingSizeIfNeedesPlaceholder { + return CGSizeMake([self accessorySpacingSize].width * (self.needsAccessoryPlaceholderSpace ? 2 : 1), [self accessorySpacingSize].height); +} + +- (CGSize)subAccessorySpacingSizeIfNeedesPlaceholder { + return CGSizeMake([self subAccessorySpacingSize].width * (self.needsSubAccessoryPlaceholderSpace ? 2 : 1), [self subAccessorySpacingSize].height); +} + +- (UIEdgeInsets)titleEdgeInsetsIfShowingTitleLabel { + return CGSizeIsEmpty(self.titleLabelSize) ? UIEdgeInsetsZero : self.titleEdgeInsets; +} + +- (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; + 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);// 虽然 accessoryView、loadingView 的高度都可能超过文字本身高度,但为了方便,这里就只考虑文字高度,其他 subview 高度均不考虑,布局时都相对于文字垂直居中(可能会溢出 titleView 的上下边缘) + return CGSizeFlatted(size); + } else { + CGSize size = CGSizeZero; + size.width = self.titleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsetsIfShowingTitleLabel) + self.subtitleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel); + size.width += [self loadingViewSpacingSizeIfNeedsPlaceholder].width + [self accessorySpacingSizeIfNeedesPlaceholder].width; + size.height = MAX(self.titleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsetsIfShowingTitleLabel), self.subtitleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel)); + size.height = MAX(size.height, [self loadingViewSpacingSizeIfNeedsPlaceholder].height); + size.height = MAX(size.height, [self accessorySpacingSizeIfNeedesPlaceholder].height); + return CGSizeFlatted(size); + } +} + +- (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; +} + +- (void)layoutSubviews { + if (CGSizeIsEmpty(self.bounds.size)) { + return; + } + + [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.contentView.bounds.size; + + // 实际内容的size,小于等于maxSize + CGSize contentSize = [self contentSize]; + contentSize.width = MIN(maxSize.width, contentSize.width); + contentSize.height = MIN(maxSize.height, contentSize.height); + + // 整个 titleView 居中,但内部的 subviews 可以在内容区域根据 contentHorizontalAlignment 的值做不一样的对齐布局 + CGFloat contentOffsetLeft = floorInPixel((maxSize.width - contentSize.width) / 2.0); + CGFloat contentOffsetRight = contentOffsetLeft; + + // 计算loading占的单边宽度 + CGFloat loadingViewSpace = [self loadingViewSpacingSize].width; + + // 获取当前 accessoryView + UIView *accessoryView = self.accessoryView ?: self.accessoryTypeView; + + // 计算 accessoryView 占的单边宽度 + CGFloat accessoryViewSpace = [self accessorySpacingSize].width; + + BOOL isTitleLabelShowing = self.titleLabel.text.length > 0; + BOOL isSubtitleLabelShowing = self.subtitleLabel.text.length > 0; + BOOL isSubAccessoryViewShowing = isSubtitleLabelShowing && self.subAccessoryView && !self.subAccessoryView.hidden; + UIEdgeInsets titleEdgeInsets = self.titleEdgeInsetsIfShowingTitleLabel; + UIEdgeInsets subtitleEdgeInsets = self.subtitleEdgeInsetsIfShowingSubtitleLabel; + + 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) { + 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, 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) { + 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) { + 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)); + } + secondLineMaxX = secondLineMinX + MIN(secondLineWidth, contentSize.width); + secondLineMinX += self.needsSubAccessoryPlaceholderSpace ? [self subAccessorySpacingSize].width : 0; + if (isSubAccessoryViewShowing) { + 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) { + 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); + maxX = CGRectGetMinX(accessoryView.frame) - self.accessoryViewOffset.x; + } + if (isSubtitleLabelShowing) { + maxX -= subtitleEdgeInsets.right; + // 如果当前的 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(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; + } + if (isTitleLabelShowing) { + minX += titleEdgeInsets.left; + maxX -= titleEdgeInsets.right; + // 如果当前的 contentSize 就是以这个 label 的最大占位计算出来的,那么就不应该先计算 center 再计算偏移 + BOOL shouldTitleLabelCenterVertically = self.titleLabelSize.height + UIEdgeInsetsGetVerticalValue(titleEdgeInsets) < contentSize.height; + CGFloat titleLabelMinY = shouldTitleLabelCenterVertically ? CGFloatGetCenter(maxSize.height, self.titleLabelSize.height) + titleEdgeInsets.top - titleEdgeInsets.bottom : titleEdgeInsets.top; + self.titleLabel.frame = CGRectFlatMake(minX, titleLabelMinY, maxX - minX, self.titleLabelSize.height); + } else { + self.titleLabel.frame = CGRectZero; + } + } + + // 上面的布局都是按 UIControlContentVerticalAlignmentTop 来计算的,所以这里根据实际的 contentVerticalAlignment 进行偏移 + // 不支持 UIControlContentVerticalAlignmentFill + CGFloat offsetY = CGFloatGetCenter(maxSize.height, contentSize.height); + if (self.contentVerticalAlignment == UIControlContentVerticalAlignmentTop) { + offsetY = 0; + } else if (self.contentVerticalAlignment == UIControlContentVerticalAlignmentBottom) { + offsetY = maxSize.height - contentSize.height; + } + [self.subviews enumerateObjectsUsingBlock:^(UIView *obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (!CGRectIsEmpty(obj.frame)) { + obj.frame = CGRectSetY(obj.frame, CGRectGetMinY(obj.frame) + offsetY); + } + }]; +} + + +#pragma mark - setter / getter + +- (void)setMaximumWidth:(CGFloat)maximumWidth { + _maximumWidth = maximumWidth; + [self refreshLayout]; +} + +- (void)setPadding:(UIEdgeInsets)padding { + _padding = padding; + [self refreshLayout]; +} + +- (void)setContentHorizontalAlignment:(UIControlContentHorizontalAlignment)contentHorizontalAlignment { + [super setContentHorizontalAlignment:contentHorizontalAlignment]; + [self refreshLayout]; +} + +- (void)setNeedsLoadingPlaceholderSpace:(BOOL)needsLoadingPlaceholderSpace { + _needsLoadingPlaceholderSpace = needsLoadingPlaceholderSpace; + [self refreshLayout]; +} + +- (void)setNeedsAccessoryPlaceholderSpace:(BOOL)needsAccessoryPlaceholderSpace { + _needsAccessoryPlaceholderSpace = needsAccessoryPlaceholderSpace; + [self refreshLayout]; +} + +- (void)setAccessoryViewOffset:(CGPoint)accessoryViewOffset { + _accessoryViewOffset = accessoryViewOffset; + [self refreshLayout]; +} + +- (void)setNeedsSubAccessoryPlaceholderSpace:(BOOL)needsSubAccessoryPlaceholderSpace { + _needsSubAccessoryPlaceholderSpace = needsSubAccessoryPlaceholderSpace; + [self refreshLayout]; +} + +- (void)setSubAccessoryViewOffset:(CGPoint)subAccessoryViewOffset { + _subAccessoryViewOffset = subAccessoryViewOffset; + [self refreshLayout]; +} + +- (void)setLoadingViewMarginRight:(CGFloat)loadingViewMarginRight { + _loadingViewMarginRight = loadingViewMarginRight; + [self refreshLayout]; +} + +- (void)setHorizontalTitleFont:(UIFont *)horizontalTitleFont { + _horizontalTitleFont = horizontalTitleFont; + if (self.style == QMUINavigationTitleViewStyleDefault) { + self.titleLabel.font = horizontalTitleFont; + [self refreshLayout]; + } +} + +- (void)setHorizontalSubtitleFont:(UIFont *)horizontalSubtitleFont { + _horizontalSubtitleFont = horizontalSubtitleFont; + if (self.style == QMUINavigationTitleViewStyleDefault) { + self.subtitleLabel.font = horizontalSubtitleFont; + [self refreshLayout]; + } +} + +- (void)setVerticalTitleFont:(UIFont *)verticalTitleFont { + _verticalTitleFont = verticalTitleFont; + if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) { + self.titleLabel.font = verticalTitleFont; + [self refreshLayout]; + } +} + +- (void)setVerticalSubtitleFont:(UIFont *)verticalSubtitleFont { + _verticalSubtitleFont = verticalSubtitleFont; + if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) { + self.subtitleLabel.font = verticalSubtitleFont; + [self refreshLayout]; + } +} + +- (void)setTitleEdgeInsets:(UIEdgeInsets)titleEdgeInsets { + _titleEdgeInsets = titleEdgeInsets; + [self refreshLayout]; +} + +- (void)setSubtitleEdgeInsets:(UIEdgeInsets)subtitleEdgeInsets { + _subtitleEdgeInsets = subtitleEdgeInsets; + [self refreshLayout]; +} + +- (void)setTitle:(NSString *)title { + _title = title; + self.titleLabel.text = title; + [self refreshLayout]; +} + +- (void)setSubtitle:(NSString *)subtitle { + _subtitle = subtitle; + self.subtitleLabel.text = subtitle; + [self refreshLayout]; +} + +- (void)setAccessoryType:(QMUINavigationTitleViewAccessoryType)accessoryType { + + // 如果已设置了accessoryView,则accessoryType不生效 + if (self.accessoryView) { + accessoryType = QMUINavigationTitleViewAccessoryTypeNone; + } + + _accessoryType = accessoryType; + + if (accessoryType == QMUINavigationTitleViewAccessoryTypeNone) { + [self.accessoryTypeView removeFromSuperview]; + self.accessoryTypeView = nil; + [self refreshLayout]; + return; + } + + if (!self.accessoryTypeView) { + self.accessoryTypeView = [[UIImageView alloc] init]; + self.accessoryTypeView.contentMode = UIViewContentModeCenter; + } + + UIImage *accessoryImage = nil; + if (accessoryType == QMUINavigationTitleViewAccessoryTypeDisclosureIndicator) { + accessoryImage = [NavBarAccessoryViewTypeDisclosureIndicatorImage qmui_imageWithOrientation:UIImageOrientationUp]; + } + + self.accessoryTypeView.image = accessoryImage; + [self.accessoryTypeView sizeToFit]; + + // 经过上面的 setImage 和 sizeToFit 之后再 addSubview,因为 addSubview 会触发系统来询问你的 sizeThatFits: + if (self.accessoryTypeView.superview != self) { + [self.contentView addSubview:self.accessoryTypeView]; + } + + [self refreshLayout]; +} + +- (void)setAccessoryView:(UIView *)accessoryView { + if (_accessoryView != accessoryView) { + [_accessoryView removeFromSuperview]; + _accessoryView = nil; + } + if (accessoryView) { + _accessoryView = accessoryView; + self.accessoryType = QMUINavigationTitleViewAccessoryTypeNone; + [self.accessoryView sizeToFit]; + [self.contentView addSubview:self.accessoryView]; + } + [self refreshLayout]; +} + +- (void)setSubAccessoryView:(UIView *)subAccessoryView { + if (_subAccessoryView != subAccessoryView) { + [_subAccessoryView removeFromSuperview]; + _subAccessoryView = nil; + } + if (subAccessoryView) { + _subAccessoryView = subAccessoryView; + [self.subAccessoryView sizeToFit]; + [self.contentView addSubview:self.subAccessoryView]; + } + + [self refreshLayout]; +} + +- (void)updateSubAccessoryViewHidden { + if (self.subAccessoryView && self.subtitleLabel.text.length && self.style == QMUINavigationTitleViewStyleSubTitleVertical) { + self.subAccessoryView.hidden = NO; + } else { + self.subAccessoryView.hidden = YES; + } +} + +- (void)setNeedsLoadingView:(BOOL)needsLoadingView { + _needsLoadingView = needsLoadingView; + if (needsLoadingView) { + if (!self.loadingView) { + _loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:NavBarActivityIndicatorViewStyle]; + self.loadingView.qmui_size = self.loadingViewSize; + self.loadingView.color = self.tintColor; + [self.loadingView stopAnimating]; + [self.contentView addSubview:self.loadingView]; + } + } else { + if (self.loadingView) { + [self.loadingView stopAnimating]; + [self.loadingView removeFromSuperview]; + _loadingView = nil; + } + } + [self refreshLayout]; +} + +- (void)setLoadingViewHidden:(BOOL)loadingViewHidden { + _loadingViewHidden = loadingViewHidden; + if (self.needsLoadingView) { + loadingViewHidden ? [self.loadingView stopAnimating] : [self.loadingView startAnimating]; + } + [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:)]) { + [self.delegate didChangedActive:active forTitleView:self]; + } + if (self.accessoryType == QMUINavigationTitleViewAccessoryTypeDisclosureIndicator) { + if (active) { + [UIView animateWithDuration:.25f delay:0 options:QMUIViewAnimationOptionsCurveIn animations:^(void){ + self.accessoryTypeView.transform = CGAffineTransformMakeRotation(AngleWithDegrees(-180)); + } completion:^(BOOL finished) { + }]; + } else { + [UIView animateWithDuration:.25f delay:0 options:QMUIViewAnimationOptionsCurveIn animations:^(void){ + self.accessoryTypeView.transform = CGAffineTransformMakeRotation(AngleWithDegrees(0.1)); + } completion:^(BOOL finished) { + }]; + } + } +} + +#pragma mark - Style & Type + +- (void)setStyle:(QMUINavigationTitleViewStyle)style { + _style = style; + if (style == QMUINavigationTitleViewStyleSubTitleVertical) { + self.titleLabel.font = self.verticalTitleFont; + self.subtitleLabel.font = self.verticalSubtitleFont; + } else { + self.titleLabel.font = self.horizontalTitleFont; + self.subtitleLabel.font = self.horizontalSubtitleFont; + } + + [self refreshLayout]; +} + +- (void)tintColorDidChange { + [super tintColorDidChange]; + + 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]; + 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 { + BOOL active = !self.active; + if ([self.delegate respondsToSelector:@selector(didTouchTitleView:isActive:)]) { + [self.delegate didTouchTitleView:self isActive:active]; + } + self.active = active; + [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) + +@end + +@implementation QMUINavigationTitleView (UIAppearance) + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self setDefaultAppearance]; + }); +} + ++ (void)setDefaultAppearance { + QMUINavigationTitleView *appearance = [QMUINavigationTitleView appearance]; + appearance.adjustsSubviewsTintColorAutomatically = YES; + appearance.adjustsSubviewsWhenHighlighted = YES; + appearance.maximumWidth = CGFLOAT_MAX; + appearance.loadingViewSize = CGSizeMake(18, 18); + appearance.loadingViewMarginRight = 3; + appearance.horizontalTitleFont = NavBarTitleFont; + appearance.horizontalSubtitleFont = NavBarTitleFont; + appearance.verticalTitleFont = UIFontMake(15); + appearance.verticalSubtitleFont = UIFontLightMake(12); + appearance.accessoryViewOffset = CGPointMake(3, 0); + appearance.subAccessoryViewOffset = CGPointMake(3, 0); + appearance.titleEdgeInsets = UIEdgeInsetsZero; + appearance.subtitleEdgeInsets = UIEdgeInsetsZero; +} + +@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 new file mode 100644 index 00000000..09f30fdf --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.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. + */ + +// +// QMUIOrderedDictionary.h +// qmui +// +// Created by QMUI Team on 16/7/21. +// + +#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; +- (void)insertObject:(ObjectType)object forKey:(KeyType)key atIndex:(NSInteger)index; +- (void)insertObjects:(NSArray *)objects forKeys:(NSArray *)keys atIndex:(NSInteger)index; +- (void)removeObject:(ObjectType)object forKey:(KeyType)key; +- (void)removeObject:(ObjectType)object atIndex:(NSInteger)index; +- (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 new file mode 100644 index 00000000..c17c093c --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIOrderedDictionary.m @@ -0,0 +1,155 @@ +/** + * 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. + */ + +// +// QMUIOrderedDictionary.m +// qmui +// +// Created by QMUI Team on 16/7/21. +// + +#import "QMUIOrderedDictionary.h" + +@interface QMUIOrderedDictionary () + +@property(nonatomic, strong) NSMutableArray *mutableAllKeys; +@property(nonatomic, strong) NSMutableArray *mutableAllValues; +@property(nonatomic, strong) NSMutableDictionary *mutableDictionary; +@end + +@implementation QMUIOrderedDictionary + +- (instancetype)initWithKeysAndObjects:(id)firstKey, ... { + if (self = [self init]) { + + if (firstKey) { + [self.mutableAllKeys addObject:firstKey]; + + va_list argumentList; + va_start(argumentList, firstKey); + id argument; + NSInteger i = 1; + while ((argument = va_arg(argumentList, id))) { + if (i % 2 == 0) { + [self.mutableAllKeys addObject:argument]; + } else { + [self.mutableAllValues addObject:argument]; + } + i++; + } + va_end(argumentList); + + [self.mutableAllKeys enumerateObjectsUsingBlock:^(id _Nonnull key, NSUInteger idx, BOOL * _Nonnull stop) { + [self.mutableDictionary setObject:self.mutableAllValues[idx] forKey:key]; + }]; + } + } + return self; +} + +- (instancetype)init { + if (self = [super init]) { + self.mutableAllKeys = [[NSMutableArray alloc] init]; + self.mutableAllValues = [[NSMutableArray alloc] init]; + self.mutableDictionary = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (NSUInteger)count { + return self.mutableDictionary.count; +} + +- (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]; + [self.mutableAllValues replaceObjectAtIndex:index withObject:object]; + [self.mutableDictionary setObject:object forKey:key]; + } else { + [self addObject:object forKey:key]; + } +} + +- (void)addObject:(id)object forKey:(id)key { + if (![self.mutableAllKeys containsObject:key]) { + [self.mutableAllKeys addObject:key]; + [self.mutableAllValues addObject:object]; + [self.mutableDictionary setObject:object forKey:key]; + } +} + +- (void)addObjects:(NSArray *)objects forKeys:(NSArray *)keys { + if (objects.count == keys.count) { + [keys enumerateObjectsUsingBlock:^(id _Nonnull key, NSUInteger idx, BOOL * _Nonnull stop) { + [self addObject:objects[idx] forKey:key]; + }]; + } +} + +- (void)insertObject:(id)object forKey:(id)key atIndex:(NSInteger)index { + if (![self.mutableAllKeys containsObject:key]) { + [self.mutableAllKeys insertObject:key atIndex:index]; + [self.mutableAllValues insertObject:object atIndex:index]; + [self.mutableDictionary setObject:object forKey:key]; + } +} + +- (void)insertObjects:(NSArray *)objects forKeys:(NSArray *)keys atIndex:(NSInteger)index { + if (objects.count == keys.count) { + __block NSInteger nextIndex = index; + [keys enumerateObjectsUsingBlock:^(id _Nonnull key, NSUInteger idx, BOOL * _Nonnull stop) { + [self insertObject:objects[idx] forKey:key atIndex:nextIndex]; + nextIndex++; + }]; + } +} + +- (void)removeObject:(id)object forKey:(id)key { + if ([self.mutableAllKeys containsObject:key]) { + NSInteger index = [self.mutableAllKeys indexOfObject:key]; + [self removeObject:object atIndex:index]; + } +} + +- (void)removeObject:(id)object atIndex:(NSInteger)index { + if (index < self.allKeys.count) { + [self.mutableDictionary removeObjectForKey:self.mutableAllKeys[index]]; + [self.mutableAllKeys removeObjectAtIndex:index]; + [self.mutableAllValues removeObjectAtIndex:index]; + } +} + +- (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]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.h b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.h new file mode 100644 index 00000000..2ba4fae5 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.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. + */ + +// +// QMUIPieProgressView.h +// qmui +// +// Created by QMUI Team on 15/9/8. +// + +#import + +/** + * 饼状进度条控件 + * + * 使用 `tintColor` 更改进度条饼状部分和边框部分的颜色 + * + * 使用 `backgroundColor` 更改圆形背景色 + * + * 通过 `UIControlEventValueChanged` 来监听进度变化 + */ + +typedef NS_ENUM(NSUInteger, QMUIPieProgressViewShape) { + QMUIPieProgressViewShapeSector, // 扇形,默认 + QMUIPieProgressViewShapeRing // 环形 +}; + +@interface QMUIPieProgressView : UIControl + +/** + 进度动画的时长,默认为 0.5 + */ +@property(nonatomic, assign) IBInspectable CFTimeInterval progressAnimationDuration; + +/** + 当前进度值,默认为 0.0。调用 `setProgress:` 相当于调用 `setProgress:animated:NO` + */ +@property(nonatomic, assign) IBInspectable float progress; + +/** + 外边框的大小,默认为 1。 + */ +@property(nonatomic, assign) IBInspectable CGFloat borderWidth; + +/** + 外边框与内部扇形之间的间隙,默认为 0。 + */ +@property(nonatomic, assign) IBInspectable CGFloat borderInset; + +/** + 线宽,用于环形绘制,默认为 0。 + */ +@property(nonatomic, assign) IBInspectable CGFloat lineWidth; + +/** + 绘制形状,默认是扇形。 + */ +@property(nonatomic, assign) IBInspectable QMUIPieProgressViewShape shape; + +/** + 修改当前的进度,会触发 UIControlEventValueChanged 事件 + + @param progress 当前的进度,取值范围 [0.0-1.0] + @param animated 是否以动画来表现 + */ +- (void)setProgress:(float)progress animated:(BOOL)animated; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.m b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.m new file mode 100644 index 00000000..605bf521 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPieProgressView.m @@ -0,0 +1,190 @@ +/** + * 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. + */ + +// +// QMUIPieProgressView.m +// qmui +// +// Created by QMUI Team on 15/9/8. +// + +#import "QMUIPieProgressView.h" +#import "QMUICore.h" + +@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]) { + self.shouldChangeProgressWithAnimation = YES; + } + return self; +} + ++ (BOOL)needsDisplayForKey:(NSString *)key { + return [key isEqualToString:@"progress"] || [super needsDisplayForKey:key]; +} + +- (id)actionForKey:(NSString *)event { + if ([event isEqualToString:@"progress"] && self.shouldChangeProgressWithAnimation) { + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:event]; + animation.fromValue = [self.presentationLayer valueForKey:event]; + animation.duration = self.progressAnimationDuration; + return animation; + } + return [super actionForKey:event]; +} + +- (void)drawInContext:(CGContextRef)context { + if (CGRectIsEmpty(self.bounds)) { + 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; + + 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)layoutSublayers { + [super layoutSublayers]; + self.cornerRadius = CGRectGetHeight(self.bounds) / 2; +} + +@end + +@implementation QMUIPieProgressView + ++ (Class)layerClass { + return [QMUIPieProgressLayer class]; +} + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.backgroundColor = UIColorClear; + self.tintColor = UIColorBlue; + self.borderWidth = 1; + self.borderInset = 0; + + [self didInitialize]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self didInitialize]; + // 从 xib 初始化的话,在 IB 里设置了 tintColor 也不会触发 tintColorDidChange,所以这里手动调用一下 + [self tintColorDidChange]; + } + return self; +} + +- (void)didInitialize { + self.progress = 0.0; + self.progressAnimationDuration = 0.5; + + self.layer.contentsScale = ScreenScale;// 要显示指定一个倍数 + [self.layer setNeedsDisplay]; +} + +- (void)setProgress:(float)progress { + [self setProgress:progress animated:NO]; +} + +- (void)setProgress:(float)progress animated:(BOOL)animated { + _progress = fmax(0.0, fmin(1.0, progress)); + self.progressLayer.shouldChangeProgressWithAnimation = animated; + self.progressLayer.progress = _progress; + + [self sendActionsForControlEvents:UIControlEventValueChanged]; +} + +- (void)setProgressAnimationDuration:(CFTimeInterval)progressAnimationDuration { + _progressAnimationDuration = progressAnimationDuration; + self.progressLayer.progressAnimationDuration = progressAnimationDuration; +} + +- (void)setBorderWidth:(CGFloat)borderWidth { + _borderWidth = borderWidth; + self.progressLayer.borderWidth = borderWidth; +} + +- (void)setBorderInset:(CGFloat)borderInset { + _borderInset = 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; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.h b/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.h new file mode 100644 index 00000000..2da5a690 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.h @@ -0,0 +1,236 @@ +/** + * 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. + */ + +// +// QMUIPopupContainerView.h +// qmui +// +// Created by QMUI Team on 15/12/17. +// + +#import +#import "UIControl+QMUI.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) { + QMUIPopupContainerViewLayoutDirectionAbove, + QMUIPopupContainerViewLayoutDirectionBelow, + QMUIPopupContainerViewLayoutDirectionLeft, + QMUIPopupContainerViewLayoutDirectionRight +}; + +typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutAlignment) { + QMUIPopupContainerViewLayoutAlignmentCenter, + QMUIPopupContainerViewLayoutAlignmentLeading, + QMUIPopupContainerViewLayoutAlignmentTrailing, +}; + +/** + * 带箭头的小tips浮层,自带 imageView 和 textLabel,可展示简单的图文信息,支持 UIViewContentModeTop/UIViewContentModeBottom/UIViewContentModeCenter 三种布局方式。 + * QMUIPopupContainerView 支持以两种方式显示在界面上: + * 1. 添加到某个 UIView 上(适合于 viewController 切换时浮层跟着一起切换的场景),这种场景只能手动隐藏浮层。 + * 2. 在 QMUIPopupContainerView 自带的 UIWindow 里显示(适合于用完就消失的场景,不要涉及界面切换),这种场景支持点击空白地方自动隐藏浮层。 + * + * 使用步骤: + * 1. 调用 init 方法初始化。 + * 2. 选择一种显示方式: + * 2.1 如果要添加到某个 UIView 上,则先设置浮层 hidden = YES,然后调用 addSubview: 把浮层添加到目标 UIView 上。 + * 2.2 如果是轻量的场景用完即走,则 init 完浮层即可,无需设置 hidden,也无需调用 addSubview:,在后面第 4 步里会自动把浮层添加到 UIWindow 上显示出来。 + * 3. 通过为 sourceBarItem/sourceView/sourceRect 三者中的一个赋值,来决定浮层布局的位置。 + * 4. 调用 showWithAnimated: 或 showWithAnimated:completion: 显示浮层。 + * 5. 调用 hideWithAnimated: 或 hideWithAnimated:completion: 隐藏浮层。 + * + * @warning 如果使用方法 2.2,并且没有打开 automaticallyHidesWhenUserTap 属性,则记得在适当的时机(例如 viewWillDisappear:)隐藏浮层。 + * + * 如果默认功能无法满足需求,可继承它重写一个子类,继承要点: + * 1. 初始化时要做的事情请放在 didInitialize 里。 + * 2. 所有 subviews 请加到 contentView 上。 + * 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; + +/// 在浮层显示时,点击空白地方是否要自动隐藏浮层,仅在用方法 2 显示时有效。 +/// 默认为 NO,也即需要手动调用代码去隐藏浮层。 +@property(nonatomic, assign) BOOL automaticallyHidesWhenUserTap; + +/// 所有 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, nullable) UIImageView *imageView; + +/// 预提供的UILabel,默认为nil,调用到的时候才初始化。默认支持多行。 +@property(nonatomic, strong, readonly, nullable) UILabel *textLabel; + +/// 圆角矩形气泡内的padding(不包括三角箭头),默认是(8, 8, 8, 8) +@property(nonatomic, assign) UIEdgeInsets contentEdgeInsets UI_APPEARANCE_SELECTOR; + +/// 调整imageView的位置,默认为UIEdgeInsetsZero。top/left正值表示往下/右方偏移,bottom/right仅在对应位置存在下一个子View时生效(例如只有同时存在imageView和textLabel时,imageEdgeInsets.right才会生效)。 +@property(nonatomic, assign) UIEdgeInsets imageEdgeInsets UI_APPEARANCE_SELECTOR; + +/// 调整textLabel的位置,默认为UIEdgeInsetsZero。top/left/bottom/right的作用同imageEdgeInsets +@property(nonatomic, assign) UIEdgeInsets textEdgeInsets UI_APPEARANCE_SELECTOR; + +/// 三角箭头的大小,默认为 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,会在布局时被动态修改。 +@property(nonatomic, assign) CGFloat maximumHeight UI_APPEARANCE_SELECTOR; + +/// 最小高度(指整个控件的高度,而不是contentView部分),默认为0 +@property(nonatomic, assign) CGFloat minimumHeight UI_APPEARANCE_SELECTOR; + +/// 计算布局时期望的默认位置,默认为QMUIPopupContainerViewLayoutDirectionAbove,也即在目标的上方 +@property(nonatomic, assign) QMUIPopupContainerViewLayoutDirection preferLayoutDirection UI_APPEARANCE_SELECTOR; + +/// 最终的布局方向(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),注意这里的值不需要由业务考虑 safeAreaInsets,内部会自己叠加。 +@property(nonatomic, assign) UIEdgeInsets safetyMarginsOfSuperview 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, 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, nullable) __kindof UIBarItem *sourceBarItem; + +@property(nonatomic, weak, nullable) __kindof UIView *sourceView; + +/// rect 需要处于 QMUIPopupContainerView 所在的坐标系内,例如如果 popup 使用 addSubview: 的方式添加到界面,则 sourceRect 应该是 superview 坐标系内的;如果 popup 使用 window 的方式展示,则 sourceRect 需要转换为 window 坐标系内。 +@property(nonatomic, assign) CGRect sourceRect; + +/// 标记为需要更新布局,会在下一次 runloop 里统一调用 updateLayout。一般情况请用这个方法,避免直接用 updateLayout,从而获取更佳的性能。 +- (void)setNeedsUpdateLayout; + +/// 立即刷新当前 popup 的布局,前提是 popup.isShowing 为 YES。 +- (void)updateLayout; + +- (void)showWithAnimated:(BOOL)animated; +- (void)showWithAnimated:(BOOL)animated completion:(void (^ __nullable)(BOOL finished))completion; +- (void)hideWithAnimated:(BOOL)animated; +- (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, nullable) void (^willShowBlock)(BOOL animated); + +/** + * 即将隐藏时的回调 + * @argv hidesByUserTap 用于区分此次隐藏是否因为用户手动点击空白区域导致浮层被隐藏 + * @argv animated 是否需要动画 + */ +@property(nonatomic, copy, nullable) void (^willHideBlock)(BOOL hidesByUserTap, BOOL animated); + +/** + * 已经隐藏后的回调 + * @argv hidesByUserTap 用于区分此次隐藏是否因为用户手动点击空白区域导致浮层被隐藏 + */ +@property(nonatomic, copy, nullable) void (^didHideBlock)(BOOL hidesByUserTap); + +@end + +@interface QMUIPopupContainerView (UISubclassingHooks) + +/// 子类重写,在初始化时做一些操作 +- (void)didInitialize NS_REQUIRES_SUPER; + +/** + 子类重写,用于告诉父类添加到 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 new file mode 100644 index 00000000..dd76738c --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupContainerView.m @@ -0,0 +1,1106 @@ +/** + * 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. + */ + +// +// QMUIPopupContainerView.m +// qmui +// +// Created by QMUI Team on 15/12/17. +// + +#import "QMUIPopupContainerView.h" +#import "QMUICore.h" +#import "QMUICommonViewController.h" +#import "UIViewController+QMUI.h" +#import "QMUILog.h" +#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 + +@end + +@interface QMUIPopContainerViewController : QMUICommonViewController + +@end + +@interface QMUIPopContainerMaskControl : UIControl + +@property(nonatomic, weak) QMUIPopupContainerView *popupContainerView; +@end + +@interface QMUIPopupContainerView () { + UIImageView *_imageView; + UILabel *_textLabel; + + CALayer *_backgroundViewMaskLayer; + CAShapeLayer *_copiedBackgroundLayer; + CALayer *_copiedArrowImageLayer; +} + +@property(nonatomic, strong) QMUIPopupContainerViewWindow *popupWindow; +@property(nonatomic, weak) UIWindow *previousKeyWindow; +@property(nonatomic, assign) BOOL hidesByUserTap; +@end + +@implementation QMUIPopupContainerView + +- (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)dealloc { + _sourceView.qmui_frameDidChangeBlock = nil; +} + +- (UIImageView *)imageView { + if (!_imageView) { + _imageView = [[UIImageView alloc] init]; + _imageView.contentMode = UIViewContentModeCenter; + [self.contentView addSubview:_imageView]; + } + return _imageView; +} + +- (UILabel *)textLabel { + if (!_textLabel) { + _textLabel = [[UILabel alloc] init]; + _textLabel.font = UIFontMake(12); + _textLabel.textColor = UIColorBlack; + _textLabel.numberOfLines = 0; + [self.contentView addSubview:_textLabel]; + } + return _textLabel; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + UIView *result = [super hitTest:point withEvent:event]; + if (result == self.contentView) { + return self; + } + 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 { + _maskViewBackgroundColor = maskViewBackgroundColor; + if (self.popupWindow) { + self.popupWindow.rootViewController.view.backgroundColor = maskViewBackgroundColor; + } +} + +- (void)setShadow:(NSShadow *)shadow { + _shadow = shadow; + _borderLayer.qmui_shadow = shadow; +} + +- (void)setBorderColor:(UIColor *)borderColor { + _borderColor = borderColor; + _borderLayer.strokeColor = borderColor.CGColor; +} + +- (void)setBorderWidth:(CGFloat)borderWidth { + _borderWidth = borderWidth; + _borderLayer.lineWidth = _borderWidth; +} + +- (void)setCornerRadius:(CGFloat)cornerRadius { + _cornerRadius = cornerRadius; + [self setNeedsLayout]; +} + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + if (self.highlightedBackgroundColor) { + 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 { + CGSize contentLimitSize = [self contentSizeInSize:size]; + 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]; + 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) { + // 箭头向下 + 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) { + // 箭头向上 + 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]; + 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; + // 每次都要重新定义 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; + 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; + }; + sourceView.qmui_frameDidChangeBlock(sourceView, sourceView.frame);// update layout immediately +} + +- (void)setSourceRect:(CGRect)sourceRect { + _sourceRect = sourceRect; + if (self.isShowing) { + [self layoutWithTargetRect: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) { + self.sourceBarItem = self.sourceBarItem; + } else if (self.sourceView) { + self.sourceView = self.sourceView; + } else { + self.sourceRect = self.sourceRect; + } + _shouldInvalidateLayout = NO; +} + +// 参数 targetRect 在 window 模式下是 window 的坐标系内的,如果是 subview 模式下则是 superview 坐标系内的 +- (void)layoutWithTargetRect:(CGRect)targetRect { + UIView *superview = self.superview; + if (!superview) { + return; + } + + _currentLayoutDirection = self.preferLayoutDirection; + targetRect = self.popupWindow ? [self.popupWindow convertRect:targetRect toView:superview] : targetRect; + CGRect containerRect = superview.bounds; + + 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; + }; + + + CGSize tipSize = [self sizeThatFits:sizeToFitBlock()]; + CGFloat preferredTipWidth = tipSize.width; + CGFloat preferredTipHeight = tipSize.height; + CGFloat tipMinX = 0; + CGFloat tipMinY = 0; + + 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); + + 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); + } + } + + // 经过上面一番调整,可能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 setNeedsLayout]; + + if (self.debug) { + self.contentView.backgroundColor = UIColorTestGreen; + self.borderColor = UIColorRed; + self.borderWidth = PixelOne; + _imageView.backgroundColor = UIColorTestRed; + _textLabel.backgroundColor = UIColorTestBlue; + } +} + +- (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 tipOrigin; +} + +- (BOOL)canTipShowAtSpecifiedLayoutDirect:(QMUIPopupContainerViewLayoutDirection)direction targetRect:(CGRect)itemRect tipSize:(CGSize)tipSize { + BOOL canShow = NO; + 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; +} + +- (void)showWithAnimated:(BOOL)animated { + [self showWithAnimated:animated completion:nil]; +} + +- (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { + + BOOL isShowingByWindowMode = NO; + if (!self.superview) { + [self initPopupContainerViewWindowIfNeeded]; + + QMUICommonViewController *viewController = (QMUICommonViewController *)self.popupWindow.rootViewController; + viewController.supportedOrientationMask = [QMUIHelper visibleViewController].supportedInterfaceOrientations; + + self.previousKeyWindow = UIApplication.sharedApplication.keyWindow; + [self.popupWindow makeKeyAndVisible]; + + isShowingByWindowMode = YES; + } else { + self.hidden = NO; + } + + [self updateLayout]; + + if (self.willShowBlock) { + self.willShowBlock(animated); + } + + if (animated) { + if (isShowingByWindowMode) { + 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); + 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.rootViewController.view.alpha = 1; + } else { + self.alpha = 1; + } + if (completion) { + completion(YES); + } + } +} + +- (void)hideWithAnimated:(BOOL)animated { + [self hideWithAnimated:animated completion:nil]; +} + +- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { + if (self.willHideBlock) { + self.willHideBlock(self.hidesByUserTap, animated); + } + + BOOL isShowingByWindowMode = !!self.popupWindow; + + if (animated) { + void (^a)(void) = ^void(void) { + if (isShowingByWindowMode) { + self.popupWindow.rootViewController.view.alpha = 0; + } else { + self.alpha = 0; + } + }; + 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]; + } +} + +- (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) { + [self.previousKeyWindow makeKeyWindow]; + } + + // iOS 9 下(iOS 8 和 10 都没问题)需要主动移除,才能令 rootViewController 和 popupWindow 立即释放,不影响后续的 layout 判断,如果不加这两句,虽然 popupWindow 指针被置为 nil,但其实对象还存在,View 层级关系也还在 + // https://github.com/Tencent/QMUI_iOS/issues/75 + [self removeFromSuperview]; + self.popupWindow.rootViewController = nil; + + self.popupWindow.hidden = YES; + self.popupWindow = nil; + } else { + self.hidden = YES; + } + if (completion) { + completion(YES); + } + if (self.didHideBlock) { + self.didHideBlock(self.hidesByUserTap); + } + self.hidesByUserTap = NO; +} + +- (BOOL)isShowing { + BOOL isShowingIfAddedToView = self.superview && !self.hidden && !self.popupWindow; + BOOL isShowingIfInWindow = self.superview && self.popupWindow && !self.popupWindow.hidden; + return isShowingIfAddedToView || isShowingIfInWindow; +} + +#pragma mark - Private Tools + +- (BOOL)isSubviewShowing:(UIView *)subview { + return subview && !subview.hidden && subview.superview; +} + +- (void)initPopupContainerViewWindowIfNeeded { + if (!self.popupWindow) { + self.popupWindow = [[QMUIPopupContainerViewWindow alloc] init]; + self.popupWindow.qmui_capturesStatusBarAppearance = NO; + self.popupWindow.backgroundColor = UIColorClear; + self.popupWindow.windowLevel = UIWindowLevelQMUIAlertView; + QMUIPopContainerViewController *viewController = [[QMUIPopContainerViewController alloc] init]; + ((QMUIPopContainerMaskControl *)viewController.view).popupContainerView = self; + if (self.automaticallyHidesWhenUserTap) { + viewController.view.backgroundColor = self.maskViewBackgroundColor; + } else { + viewController.view.backgroundColor = UIColorClear; + } + viewController.supportedOrientationMask = [QMUIHelper visibleViewController].supportedInterfaceOrientations; + self.popupWindow.rootViewController = viewController;// 利用 rootViewController 来管理横竖屏 + [self.popupWindow.rootViewController.view addSubview:self]; + } +} + +/// 根据一个给定的大小(包含箭头,不含 distanceBetweenSource ),计算出符合这个大小的内容大小(去掉箭头和白色内部的 contentEdgeInsets 后) +- (CGSize)contentSizeInSize:(CGSize)size { + 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 + self.arrowSpacingInHorizontal + (self.arrowSpacingInHorizontal > 0 ? 0 : self.borderWidth);// 当存在箭头时,箭头会叠在 border 上,所以这里只加一条边 + resultWidth = MAX(MIN(resultWidth, self.maximumWidth), self.minimumWidth);// 宽度必须在最小值和最大值之间 + resultWidth = flat(resultWidth); + + 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 = 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 qmui_removeDefaultAnimations]; + [self.layer addSublayer:_backgroundLayer]; + + _contentView = [[UIView alloc] init]; + self.contentView.clipsToBounds = YES; + [self addSubview:self.contentView]; + + // 由于浮层是在调用 showWithAnimated: 时才会被添加到 window 上,所以 appearance 也是在 showWithAnimated: 后才生效,这太晚了,会导致 showWithAnimated: 之前用到那些支持 appearance 的属性值都不准确,所以这里手动提前触发。 + [self qmui_applyAppearance]; +} + +- (CGSize)sizeThatFitsInContentView:(CGSize)size { + // 如果没内容则返回自身大小 + if (![self isSubviewShowing:_imageView] && ![self isSubviewShowing:_textLabel]) { + CGSize selfSize = [self contentSizeInSize:self.bounds.size]; + return selfSize; + } + + CGSize resultSize = CGSizeZero; + + BOOL isImageViewShowing = [self isSubviewShowing:_imageView]; + if (isImageViewShowing) { + CGSize imageViewSize = [_imageView sizeThatFits:size]; + 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 - UIEdgeInsetsGetHorizontalValue(self.textEdgeInsets), size.height); + CGSize textLabelSize = [_textLabel sizeThatFits:textLabelLimitSize]; + resultSize.width += ceil(textLabelSize.width) + UIEdgeInsetsGetHorizontalValue(self.textEdgeInsets); + resultSize.height = MAX(resultSize.height, ceil(textLabelSize.height) + UIEdgeInsetsGetVerticalValue(self.textEdgeInsets)); + } + return resultSize; +} + +@end + +@implementation QMUIPopupContainerView (UIAppearance) + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self setDefaultAppearance]; + }); +} + ++ (void)setDefaultAppearance { + QMUIPopupContainerView *appearance = [QMUIPopupContainerView appearance]; + appearance.contentEdgeInsets = UIEdgeInsetsMake(8, 8, 8, 8); + appearance.arrowSize = CGSizeMake(18, 9); + appearance.maximumWidth = CGFLOAT_MAX; + appearance.minimumWidth = 0; + 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;// 如果先设置了 UIView.appearance.backgroundColor,再使用最传统的 method_exchangeImplementations 交换 UIView.setBackgroundColor 方法,则会 crash。QMUI 这里是在 +initialize 时设置的,业务如果要 hook -[UIView setBackgroundColor:] 则需要比 +initialize 更早才行 + appearance.maskViewBackgroundColor = UIColorMask; + appearance.highlightedBackgroundColor = nil; + 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; + appearance.qmui_outsideEdge = UIEdgeInsetsZero; + +} + +@end + +@implementation QMUIPopContainerViewController + +- (void)loadView { + QMUIPopContainerMaskControl *maskControl = [[QMUIPopContainerMaskControl alloc] init]; + self.view = maskControl; +} + +@end + +@implementation QMUIPopContainerMaskControl + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + [self addTarget:self action:@selector(handleMaskEvent:) forControlEvents:UIControlEventTouchDown]; + } + return self; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + UIView *result = [super hitTest:point withEvent:event]; + if (result == self) { + if (!self.popupContainerView.automaticallyHidesWhenUserTap) { + return nil; + } + } + return result; +} + +// 把点击遮罩的事件放在 addTarget: 里而不直接在 hitTest:withEvent: 里处理是因为 hitTest:withEvent: 总是会走两遍 +- (void)handleMaskEvent:(id)sender { + if (self.popupContainerView.automaticallyHidesWhenUserTap) { + self.popupContainerView.hidesByUserTap = YES; + [self.popupContainerView hideWithAnimated:YES]; + } +} + +- (void)layoutSubviews { + [super layoutSubviews]; + [self.popupContainerView updateLayout];// 横竖屏旋转时,可能 sourceView window 已经旋转,但 popupWindow 尚未旋转,所以在 popupWindow 布局更新完成后再刷新一次 popup 的布局 +} + +@end + +@implementation QMUIPopupContainerViewWindow + +// 避免 UIWindow 拦截掉事件,保证让事件继续往背后传递 +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + UIView *result = [super hitTest:point withEvent:event]; + if (result == self) { + return nil; + } + return result; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.rootViewController.view.frame = self.bounds;// 保证来电模式下也是撑满全屏 +} + +@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/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 new file mode 100644 index 00000000..94538d7d --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h @@ -0,0 +1,162 @@ +/** + * 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. + */ + +// +// QMUIPopupMenuView.h +// qmui +// +// Created by QMUI Team on 2017/2/24. +// + +#import +#import "QMUIPopupContainerView.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 添加菜单项,并在 item 点击事件里调用 hideWithAnimated: 隐藏浮层。 + * 4. 通过为 sourceBarItem/sourceView/sourceRect 三者中的一个赋值,来决定浮层布局的位置(参考父类)。 + * 5. 调用 showWithAnimated: 即可显示(参考父类)。 + * + * 注意,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 + +/// contentView 里的 scrollView,所有 itemButton 都是放在这里面的。 +@property(nonatomic, strong, readonly) QMUITableView *tableView; + +/// 是否需要显示每个 item 之间的分隔线,默认为 NO,当为 YES 时,每个 section 除了最后一个 item 外其他 item 底部都会显示分隔线。分隔线显示在当前 item 上方,不占位。 +@property(nonatomic, assign) BOOL shouldShowItemSeparator UI_APPEARANCE_SELECTOR; + +/// item 分隔线的颜色,默认为 UIColorSeparator。 +@property(nonatomic, strong, nullable) UIColor *itemSeparatorColor UI_APPEARANCE_SELECTOR; + +/// item 分隔线的位置偏移,默认为 UIEdgeInsetsZero。item 分隔线的默认布局是 menuView 宽度减去左右 padding,如果你希望分隔线左右贴边则可为这个属性设置一个负值的 left/right。 +@property(nonatomic, assign) UIEdgeInsets itemSeparatorInset UI_APPEARANCE_SELECTOR; + +/// 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。分隔线拥有自己的占位,不与 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; + +/// 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; + +/// 批量设置 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; + +/// 默认 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 QMUIPopupMenuItem *> *items; + +/// 设置多个 section 的多个 item +@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 + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.m b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.m new file mode 100644 index 00000000..9e158517 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.m @@ -0,0 +1,609 @@ +/** + * 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. + */ + +// +// QMUIPopupMenuView.m +// qmui +// +// Created by QMUI Team on 2017/2/24. +// + +#import "QMUIPopupMenuView.h" +#import "QMUICore.h" +#import "UIView+QMUI.h" +#import "CALayer+QMUI.h" +#import "NSArray+QMUI.h" +#import "UIFont+QMUI.h" +#import "UITableViewCell+QMUI.h" + +@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]; +} + +- (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) + +- (void)updateAppearanceForPopupMenuView; +@end + +@implementation QMUIPopupMenuView + +- (void)setItems:(NSArray<__kindof QMUIPopupMenuItem *> *)items { + _items = items; + self.itemSections = items ? @[_items] : nil; +} + +- (void)setItemSections:(NSArray *> *)itemSections { + [_itemSections qmui_enumerateNestedArrayWithBlock:^(__kindof QMUIPopupMenuItem *item, BOOL *stop) { + item.menuView = nil; + }]; + _itemSections = itemSections; + [_itemSections qmui_enumerateNestedArrayWithBlock:^(__kindof QMUIPopupMenuItem *item, BOOL * _Nonnull stop) { + item.menuView = self; + }]; + [self reload];// 涉及到数据的必须立即刷新,否则容易因为异步导致 cell 里的 view 和当前的 item 不匹配的 bug +} + +- (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]; +} + +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<__kindof QMUIPopupMenuItem *> *items = self.itemSections[section]; + for (NSInteger row = 0, rowCount = items.count; row < rowCount; row ++) { + QMUIPopupMenuItem *item = items[row]; + if (item == aItem) { + return [NSIndexPath indexPathForRow:row inSection:section]; + } + } + } + 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 { + [selectedIndexPaths addObject:indexPath]; + } + } else { + // 单选,得把其他选中都清除 + [selectedIndexPaths removeAllObjects]; + if (!itemView.selected) { + [selectedIndexPaths addObject:indexPath]; + } + } + self.selectedItemIndexPaths = selectedIndexPaths.copy; + } + } + + if (itemView.item.handler) { + itemView.item.handler(itemView.item, itemView, indexPath.section, indexPath.row); + } +} + +- (void)setBottomAccessoryView:(__kindof UIView *)bottomAccessoryView { + if (bottomAccessoryView != _bottomAccessoryView) { + [_bottomAccessoryView removeFromSuperview]; + } + _bottomAccessoryView = bottomAccessoryView; + [self.contentView addSubview:_bottomAccessoryView]; + [self setNeedsUpdateLayout]; +} + +- (void)tintColorDidChange { + [super tintColorDidChange]; + [self setNeedsReload]; +} + +- (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; +} + +#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; + + _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 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]; + 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)); + } + self.tableView.frame = contentRect; +} + +@end + +@implementation QMUIPopupMenuView (UIAppearance) + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self setDefaultAppearanceForPopupMenuView]; + }); +} + ++ (void)setDefaultAppearanceForPopupMenuView { + QMUIPopupMenuView *appearance = [QMUIPopupMenuView appearance]; + appearance.shouldShowItemSeparator = YES; + appearance.itemSeparatorColor = UIColorSeparator; + appearance.itemSeparatorInset = UIEdgeInsetsZero; + appearance.itemSeparatorHeight = PixelOne; + appearance.shouldShowSectionSeparator = YES; + appearance.sectionSeparatorColor = UIColorSeparator; + appearance.sectionSeparatorInset = UIEdgeInsetsZero; + 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; +} + +- (void)updateAppearanceForPopupMenuView { + QMUIPopupMenuView *appearance = [QMUIPopupMenuView appearance]; + 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.sectionSeparatorHeight = appearance.sectionSeparatorHeight; + self.sectionSpacing = appearance.sectionSpacing; + self.sectionSpacingColor = appearance.sectionSpacingColor; + self.padding = appearance.padding; + self.itemHeight = appearance.itemHeight; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.h b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.h new file mode 100644 index 00000000..28f78f25 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.h @@ -0,0 +1,114 @@ +/** + * 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. + */ + +// +// QMUINavigationBarScrollingAnimator.h +// QMUIKit +// +// Created by QMUI Team on 2018/O/16. +// + +#import "QMUIScrollAnimator.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + 实现通过界面上的 UIScrollView 滚动来控制顶部导航栏外观的类,导航栏外观会跟随滚动距离的变化而变化。 + + 使用方式: + + 1. 用 init 方法初始化。 + 2. 通过 scrollView 属性关联一个 UIScrollView。 + 3. 修改 offsetYToStartAnimation 调整动画触发的滚动位置。 + 4. 修改 distanceToStopAnimation 调整动画触发后滚动多久到达终点。 + + @note 注意,由于在同个 UINavigationController 里的所有 viewController 的 navigationBar 都是共享的,所以如果修改了 navigationBar 的样式,需要自行处理界面切换时 navigationBar 的样式恢复。 + @note 注意,为了性能考虑,在 progress 达到 0 后再往上滚,或者 progress 达到 1 后再往下滚,都不会再触发那一系列 animationBlock。 + */ +@interface QMUINavigationBarScrollingAnimator : QMUIScrollAnimator + +/// 指定要关联的 UINavigationBar,若不指定,会自动寻找当前 App 可视界面上的 navigationBar +@property(nullable, nonatomic, weak) UINavigationBar *navigationBar; + +/** + contentOffset.y 到达哪个值即开始动画,默认为 0 + + @note 注意,如果 adjustsOffsetYWithInsetTopAutomatically 为 YES,则实际计算时的值为 (-contentInset.top + offsetYToStartAnimation),这时候 offsetYToStartAnimation = 0 则表示在列表默认的停靠位置往下拉就会触发临界点。 + */ +@property(nonatomic, assign) CGFloat offsetYToStartAnimation; + +/// 控制从 offsetYToStartAnimation 开始,要滚动多长的距离就打到动画结束的位置,默认为 44 +@property(nonatomic, assign) CGFloat distanceToStopAnimation; + +/// 传给 offsetYToStartAnimation 的值是否要自动叠加上 -contentInset.top,默认为 YES。 +@property(nonatomic, assign) BOOL adjustsOffsetYWithInsetTopAutomatically; + +/// 当前滚动位置对应的进度 +@property(nonatomic, assign, readonly) float progress; + +/** + 如果为 NO,则当 progress 的值不再变化(例如达到 0 后继续往上滚动,或者达到 1 后继续往下滚动)时,就不会再触发动画,从而提升性能。 + + 如果为 YES,则任何时候只要有滚动产生,动画就会被触发,适合运用到类似 Plain Style 的 UITableView 里在滚动时也要适配停靠的 sectionHeader 的场景(因为需要不断计算当前正在停靠的 sectionHeader 是哪一个)。 + + 默认为 NO + */ +@property(nonatomic, assign) BOOL continuous; + +/** + 用于控制不同滚动位置下的表现,总的动画 block,如果定义了这个,则滚动时不会再调用后面那几个 block + @param animator 当前的 animator 对象 + @param progress 当前滚动位置处于 offsetYToStartAnimation 到 (offsetYToStartAnimation + distanceToStopAnimation) 之间的哪个进度 + */ +@property(nullable, nonatomic, copy) void (^animationBlock)(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress); + +/** + 返回不同滚动位置下对应的背景图 + @param animator 当前的 animator 对象 + @param progress 当前滚动位置处于 offsetYToStartAnimation 到 (offsetYToStartAnimation + distanceToStopAnimation) 之间的哪个进度 + */ +@property(nullable, nonatomic, copy) UIImage * (^backgroundImageBlock)(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress); + +/** + 返回不同滚动位置下对应的导航栏底部分隔线的图片 + @param animator 当前的 animator 对象 + @param progress 当前滚动位置处于 offsetYToStartAnimation 到 (offsetYToStartAnimation + distanceToStopAnimation) 之间的哪个进度 + */ +@property(nullable, nonatomic, copy) UIImage * (^shadowImageBlock)(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress); + +/** + 返回不同滚动位置下对应的导航栏的 tintColor + @param animator 当前的 animator 对象 + @param progress 当前滚动位置处于 offsetYToStartAnimation 到 (offsetYToStartAnimation + distanceToStopAnimation) 之间的哪个进度 + */ +@property(nullable, nonatomic, copy) UIColor * (^tintColorBlock)(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress); + +/** + 返回不同滚动位置下对应的导航栏的 titleView tintColor,注意只能对使用了 navigationItem.titleView 生效(QMUICommonViewController 的子类默认用了 QMUINavigationTitleView,所以也可以生效)。 + @param animator 当前的 animator 对象 + @param progress 当前滚动位置处于 offsetYToStartAnimation 到 (offsetYToStartAnimation + distanceToStopAnimation) 之间的哪个进度 + */ +@property(nullable, nonatomic, copy) UIColor * (^titleViewTintColorBlock)(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress); + +/** + 返回不同滚动位置下对应的状态栏样式 + @param animator 当前的 animator 对象 + @param progress 当前滚动位置处于 offsetYToStartAnimation 到 (offsetYToStartAnimation + distanceToStopAnimation) 之间的哪个进度 + @warning 需在项目的 Info.plist 文件内设置字段 “View controller-based status bar appearance” 的值为 NO 才能生效,如果不设置,或者值为 YES,则请自行通过系统提供的 - preferredStatusBarStyle 方法来实现,statusbarStyleBlock 无效 + */ +@property(nullable, nonatomic, copy) UIStatusBarStyle (^statusbarStyleBlock)(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress); + +/** + 返回不同滚动位置下对应的导航栏的 barTintColor + @param animator 当前的 animator 对象 + @param progress 当前滚动位置处于 offsetYToStartAnimation 到 (offsetYToStartAnimation + distanceToStopAnimation) 之间的哪个进度 + */ +@property(nonatomic, copy) UIColor * (^barTintColorBlock)(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress); +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.m b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.m new file mode 100644 index 00000000..832dc7b5 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.m @@ -0,0 +1,130 @@ +/** + * 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. + */ + +// +// QMUINavigationBarScrollingAnimator.m +// QMUIKit +// +// Created by QMUI Team on 2018/O/16. +// + +#import "QMUINavigationBarScrollingAnimator.h" +#import "UIViewController+QMUI.h" +#import "UIScrollView+QMUI.h" +#import "UIView+QMUI.h" + +@interface QMUINavigationBarScrollingAnimator () + +@property(nonatomic, assign) BOOL progressZeroReached; +@property(nonatomic, assign) BOOL progressOneReached; +@end + +@implementation QMUINavigationBarScrollingAnimator + +- (instancetype)init { + self = [super init]; + if (self) { + + self.adjustsOffsetYWithInsetTopAutomatically = YES; + + self.distanceToStopAnimation = 44; + + self.didScrollBlock = ^(QMUINavigationBarScrollingAnimator * _Nonnull animator) { + UINavigationBar *navigationBar = animator.navigationBar; + if (!navigationBar) { + navigationBar = animator.scrollView.qmui_viewController.navigationController.navigationBar; + if (!navigationBar) { + NSLog(@"无法自动找到 UINavigationBar,或许此时 scrollView 所在的 viewController 已经不存在于 UINavigationController 里。"); + return; + } + } + + CGFloat progress = animator.progress; + + if (!animator.continuous && ((progress <= 0 && animator.progressZeroReached) || (progress >= 1 && animator.progressOneReached))) { + return; + } + animator.progressZeroReached = progress <= 0; + animator.progressOneReached = progress >= 1; + + if (animator.animationBlock) { + animator.animationBlock(animator, progress); + } else { + if (animator.backgroundImageBlock) { + UIImage *backgroundImage = animator.backgroundImageBlock(animator, progress); + [navigationBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault]; + } + if (animator.shadowImageBlock) { + UIImage *shadowImage = animator.shadowImageBlock(animator, progress); + navigationBar.shadowImage = shadowImage; + } + if (animator.tintColorBlock) { + UIColor *tintColor = animator.tintColorBlock(animator, progress); + navigationBar.tintColor = tintColor; + } + if (animator.titleViewTintColorBlock) { + UIColor *tintColor = animator.titleViewTintColorBlock(animator, progress); + navigationBar.topItem.titleView.tintColor = tintColor; + } + if (animator.barTintColorBlock) { + 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]; + } else { + [UIApplication.sharedApplication setStatusBarStyle:UIStatusBarStyleDefault]; + } + EndIgnoreDeprecatedWarning + } + } + }; + } + return self; +} + +- (float)progress { + UIScrollView *scrollView = self.scrollView; + CGFloat contentOffsetY = flat(scrollView.contentOffset.y); + CGFloat offsetYToStartAnimation = flat(self.offsetYToStartAnimation + (self.adjustsOffsetYWithInsetTopAutomatically ? -scrollView.adjustedContentInset.top : 0)); + if (contentOffsetY < offsetYToStartAnimation) { + return 0; + } + if (contentOffsetY > offsetYToStartAnimation + self.distanceToStopAnimation) { + return 1; + } + return (contentOffsetY - offsetYToStartAnimation) / self.distanceToStopAnimation; +} + +- (void)setOffsetYToStartAnimation:(CGFloat)offsetYToStartAnimation { + BOOL valueChanged = _offsetYToStartAnimation != offsetYToStartAnimation; + _offsetYToStartAnimation = offsetYToStartAnimation; + if (valueChanged) { + [self resetState]; + } +} + +- (void)setScrollView:(__kindof UIScrollView *)scrollView { + BOOL scrollViewChanged = self.scrollView != scrollView; + [super setScrollView:scrollView]; + if (scrollViewChanged) { + [self resetState]; + } +} + +- (void)resetState { + self.progressZeroReached = NO; + self.progressOneReached = NO; + [self updateScroll]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.h b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.h new file mode 100644 index 00000000..afa8d4f7 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.h @@ -0,0 +1,69 @@ +/** + * 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. + */ + +// +// QMUINavigationBarScrollingSnapAnimator.h +// QMUIKit +// +// Created by QMUI Team on 2018/S/30. +// + +#import "QMUIScrollAnimator.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + 实现通过界面上的 UIScrollView 滚动来控制顶部导航栏外观的类,当滚动到某个位置时,即触发导航栏外观的变化。 + + 使用方式: + + 1. 用 init 方法初始化。 + 2. 通过 scrollView 属性关联一个 UIScrollView。 + 3. 修改 offsetYToStartAnimation 调整动画触发的滚动位置。 + + @note 注意,由于在同个 UINavigationController 里的所有 viewController 的 navigationBar 都是共享的,所以如果修改了 navigationBar 的样式,需要自行处理界面切换时 navigationBar 的样式恢复。 + */ +@interface QMUINavigationBarScrollingSnapAnimator : QMUIScrollAnimator + +/// 指定要关联的 UINavigationBar,若不指定,会自动寻找当前 App 可视界面上的 navigationBar +@property(nonatomic, weak) UINavigationBar *navigationBar; + +/** + contentOffset.y 到达哪个值即开始动画,默认为 0。 + + @note 注意,如果 adjustsOffsetYWithInsetTopAutomatically 为 YES,则实际计算时的值为 (-contentInset.top + offsetYToStartAnimation),这时候 offsetYToStartAnimation = 0 则表示在列表默认的停靠位置往下拉就会触发临界点。 + */ +@property(nonatomic, assign) CGFloat offsetYToStartAnimation; + +/// 传给 offsetYToStartAnimation 的值是否要自动叠加上 -contentInset.top,默认为 YES。 +@property(nonatomic, assign) BOOL adjustsOffsetYWithInsetTopAutomatically; + +/** + 当滚动到触发位置时,可在 block 里执行动画 + @param animator 当前的 animator 对象 + @param offsetYReached 是否已经过了临界点(也即 offsetYToStartAnimation) + */ +@property(nonatomic, copy) void (^animationBlock)(QMUINavigationBarScrollingSnapAnimator * _Nonnull animator, BOOL offsetYReached); + +/** + 是否已经过了临界点(也即 offsetYToStartAnimation) + */ +@property(nonatomic, assign, readonly) BOOL offsetYReached; + +/** + 如果为 NO,则当 offsetYReached 的值不再变化(例如 YES 后继续往下滚动,或者 NO 后继续往上滚动)时,就不会再触发动画,从而提升性能。 + + 如果为 YES,则任何时候只要有滚动产生,动画就会被触发,适合运用到类似 Plain Style 的 UITableView 里在滚动时也要适配停靠的 sectionHeader 的场景(因为需要不断计算当前正在停靠的 sectionHeader 是哪一个)。 + + 默认为 NO + */ +@property(nonatomic, assign) BOOL continuous; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.m b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.m new file mode 100644 index 00000000..a79c42d6 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.m @@ -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. + */ + +// +// QMUINavigationBarScrollingSnapAnimator.m +// QMUIKit +// +// Created by QMUI Team on 2018/S/30. +// + +#import "QMUINavigationBarScrollingSnapAnimator.h" +#import "UIViewController+QMUI.h" +#import "UIScrollView+QMUI.h" +#import "UIView+QMUI.h" + +@interface QMUINavigationBarScrollingSnapAnimator () + +@property(nonatomic, assign) BOOL alreadyCalledScrollDownAnimation; +@property(nonatomic, assign) BOOL alreadyCalledScrollUpAnimation; +@end + +@implementation QMUINavigationBarScrollingSnapAnimator + +- (instancetype)init { + self = [super init]; + if (self) { + + self.adjustsOffsetYWithInsetTopAutomatically = YES; + + self.didScrollBlock = ^(QMUINavigationBarScrollingSnapAnimator * _Nonnull animator) { + UINavigationBar *navigationBar = animator.navigationBar; + if (!navigationBar) { + navigationBar = animator.scrollView.qmui_viewController.navigationController.navigationBar; + if (!navigationBar) { + NSLog(@"无法自动找到 UINavigationBar,或许此时 scrollView 所在的 viewController 已经不存在于 UINavigationController 里。"); + return; + } + } + + if (animator.animationBlock) { + if (animator.offsetYReached) { + if (animator.continuous || !animator.alreadyCalledScrollDownAnimation) { + animator.animationBlock(animator, YES); + animator.alreadyCalledScrollDownAnimation = YES; + animator.alreadyCalledScrollUpAnimation = NO; + } + } else { + if (animator.continuous || !animator.alreadyCalledScrollUpAnimation) { + animator.animationBlock(animator, NO); + animator.alreadyCalledScrollUpAnimation = YES; + animator.alreadyCalledScrollDownAnimation = NO; + } + } + } + }; + } + return self; +} + +- (BOOL)offsetYReached { + UIScrollView *scrollView = self.scrollView; + CGFloat contentOffsetY = flat(scrollView.contentOffset.y); + CGFloat offsetYToStartAnimation = flat(self.offsetYToStartAnimation + (self.adjustsOffsetYWithInsetTopAutomatically ? -scrollView.adjustedContentInset.top : 0)); + return contentOffsetY > offsetYToStartAnimation; +} + +- (void)setOffsetYToStartAnimation:(CGFloat)offsetYToStartAnimation { + BOOL valueChanged = _offsetYToStartAnimation != offsetYToStartAnimation; + _offsetYToStartAnimation = offsetYToStartAnimation; + if (valueChanged) { + [self resetState]; + } +} + +- (void)setScrollView:(__kindof UIScrollView *)scrollView { + BOOL scrollViewChanged = self.scrollView != scrollView; + [super setScrollView:scrollView]; + if (scrollViewChanged) { + [self resetState]; + } +} + +- (void)resetState { + self.alreadyCalledScrollUpAnimation = NO; + self.alreadyCalledScrollDownAnimation = NO; + [self updateScroll]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.h b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.h new file mode 100644 index 00000000..ab340d66 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.h @@ -0,0 +1,45 @@ +/** + * 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. + */ + +// +// QMUIScrollAnimator.h +// QMUIKit +// +// Created by QMUI Team on 2018/S/30. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + 一个方便地监控 UIScrollView 滚动的类,可在 didScrollBlock 里做一些与滚动位置相关的事情。 + + 使用方式: + 1. 用 init 初始化。 + 2. 通过 scrollView 绑定一个 UIScrollView。 + 3. 在 didScrollBlock 里做一些与滚动位置相关的事情。 + */ +@interface QMUIScrollAnimator : NSObject + +/// 绑定的 UIScrollView +@property(nullable, nonatomic, weak) __kindof UIScrollView *scrollView; + +/// UIScrollView 滚动时会调用这个 block +@property(nonatomic, copy) void (^didScrollBlock)(__kindof QMUIScrollAnimator *animator); + +/// 当 enabled 为 NO 时,即便 scrollView 滚动,didScrollBlock 也不会被调用。默认为 YES。 +@property(nonatomic, assign) BOOL enabled; + +/// 立即根据当前的滚动位置更新状态 +- (void)updateScroll; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.m b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.m new file mode 100644 index 00000000..d3cbc73f --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.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. + */ + +// +// QMUIScrollAnimator.m +// QMUIKit +// +// Created by QMUI Team on 2018/S/30. +// + +#import "QMUIScrollAnimator.h" +#import "QMUIMultipleDelegates.h" +#import "UIScrollView+QMUI.h" +#import "UIView+QMUI.h" + +@interface QMUIScrollAnimator () + +@property(nonatomic, assign) BOOL scrollViewMultipleDelegatesEnabledBeforeSet; +@property(nonatomic, weak) id scrollViewDelegateBeforeSet; +@end + +@implementation QMUIScrollAnimator + +- (instancetype)init { + if (self = [super init]) { + self.enabled = YES; + } + return self; +} + +- (void)setScrollView:(__kindof UIScrollView *)scrollView { + if (scrollView) { + self.scrollViewMultipleDelegatesEnabledBeforeSet = scrollView.qmui_multipleDelegatesEnabled; + self.scrollViewDelegateBeforeSet = scrollView.delegate; + scrollView.qmui_multipleDelegatesEnabled = YES; + scrollView.delegate = self; + } else { + _scrollView.qmui_multipleDelegatesEnabled = self.scrollViewMultipleDelegatesEnabledBeforeSet; + if (_scrollView.qmui_multipleDelegatesEnabled) { + [((QMUIMultipleDelegates *)_scrollView.delegate) removeDelegate:self]; + } else { + _scrollView.delegate = self.scrollViewDelegateBeforeSet; + } + } + _scrollView = scrollView; +} + +- (void)updateScroll { + [self scrollViewDidScroll:self.scrollView]; +} + +#pragma mark - + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + if (self.enabled && scrollView == self.scrollView && self.didScrollBlock && scrollView.qmui_visible) { + self.didScrollBlock(self); + } +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.h b/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.h new file mode 100644 index 00000000..d6f6518a --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.h @@ -0,0 +1,20 @@ +/** + * 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. + */ + +// +// QMUISearchBar.h +// qmui +// +// Created by QMUI Team on 14-7-2. +// + +#import + +@interface QMUISearchBar : UISearchBar + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.m b/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.m new file mode 100644 index 00000000..3b36769b --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUISearchBar.m @@ -0,0 +1,40 @@ +/** + * 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. + */ + +// +// QMUISearchBar.m +// qmui +// +// Created by QMUI Team on 14-7-2. +// + +#import "QMUISearchBar.h" +#import "UISearchBar+QMUI.h" + +@implementation QMUISearchBar + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + [self didInitialize]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + [self didInitialize]; + } + return self; +} + +- (void)didInitialize { + [self qmui_styledAsQMUISearchBar]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISearchController.h b/QMUI/QMUIKit/QMUIComponents/QMUISearchController.h new file mode 100644 index 00000000..cc579338 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUISearchController.h @@ -0,0 +1,168 @@ +/** + * 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. + */ + +// +// QMUISearchController.h +// +// Created by QMUI Team on 16/5/25. +// + +#import +#import "QMUICommonViewController.h" +#import "QMUICommonTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIEmptyView; +@class QMUISearchController; + +/** + * 配合 QMUISearchController 使用的 protocol,主要负责两件事情: + * + * 1. 响应用户的输入,在搜索框内的文字发生变化后被调用,可在 searchController:updateResultsForSearchString: 方法内更新搜索结果的数据集,在里面请自行调用 [searchController.tableView reloadData] + * 2. 渲染最终用于显示搜索结果的 UITableView 的数据,该 tableView 的 delegate、dataSource 均包含在这个 protocol 里 + */ +@protocol QMUISearchControllerDelegate + +@required +/** + * 搜索框文字发生变化时的回调,请自行调用 `[tableView reloadData]` 来更新界面。 + * @warning 搜索框文字为空(例如第一次点击搜索框进入搜索状态时,或者文字全被删掉了,或者点击搜索框的×)也会走进来,此时参数searchString为@"",这是为了和系统的UISearchController保持一致 + */ +- (void)searchController:(QMUISearchController *)searchController updateResultsForSearchString:(nullable NSString *)searchString; + +@optional +- (void)willPresentSearchController:(QMUISearchController *)searchController; +- (void)didPresentSearchController:(QMUISearchController *)searchController; +- (void)willDismissSearchController:(QMUISearchController *)searchController; +- (void)didDismissSearchController:(QMUISearchController *)searchController; +- (void)searchController:(QMUISearchController *)searchController didLoadSearchResultsTableView:(UITableView *)tableView; +- (void)searchController:(QMUISearchController *)searchController willShowEmptyView:(QMUIEmptyView *)emptyView; + +@end + +/** + * 支持在搜索文字为空时(注意并非“搜索结果为空”)显示一个界面,例如常见的“最近搜索”功能,具体请查看属性 launchView。 + * 使用方法: + * 1. 使用 initWithContentsViewController: 初始化 + * 2. 通过 searchBar 属性得到搜索框的引用并直接使用,例如 `tableHeaderView = searchController.searchBar` + * 3. 指定 searchResultsDelegate 属性并在其中实现 searchController:updateResultsForSearchString: 方法以更新搜索结果数据集 + * 4. 如果需要,可通过 @c qmui_preferredStatusBarStyleBlock 来控制搜索状态下的状态栏样式。 + * + * @note QMUICommonTableViewController 内部自带 QMUISearchController,只需将属性 shouldShowSearchBar 置为 YES 即可,无需自行初始化 QMUISearchController。 + */ +@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 + * @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; + +/// 搜索结果列表,仅当通过 initWithContentsViewController: 或 initWithContentsViewController:resultsTableViewStyle: 初始化时才有效。 +@property(nonatomic, strong, readonly, nullable) QMUITableView *tableView; + +/// 在搜索文字为空时会展示的一个 view,通常用于实现“最近搜索”之类的功能。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; + +/** + * 控制进入/退出搜索状态 + * @param active YES 表示进入搜索状态,NO 表示退出搜索状态 + * @param animated 是否要以动画的形式展示状态切换 + */ +- (void)setActive:(BOOL)active animated:(BOOL)animated; + +/// 进入搜索状态时是否要把原界面的 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 + + + +@interface QMUICommonTableViewController (Search) + +/** + * 控制列表里是否需要搜索框,如果为 YES,则会在 viewDidLoad 之后创建一个 searchBar 作为 tableHeaderView;如果为 NO,则会移除已有的 searchBar 和 searchController。 + * 默认为 NO。 + * @note 若在 viewDidLoad 之前设置为 YES,也会等到 viewDidLoad 时才去创建搜索框。 + */ +@property(nonatomic, assign) BOOL shouldShowSearchBar; + +/** + * 获取当前的 searchController,注意只有当 `shouldShowSearchBar` 为 `YES` 时才有用 + * + * 默认为 `nil` + * + * @see QMUITableViewDelegate + */ +@property(nonatomic, strong, readonly, nullable) QMUISearchController *searchController; + +/** + * 获取当前的 searchBar,注意只有当 `shouldShowSearchBar` 为 `YES` 时才有用 + * + * 默认为 `nil` + * + * @see QMUITableViewDelegate + */ +@property(nonatomic, strong, readonly, nullable) UISearchBar *searchBar; + +/** + * 是否应该在显示空界面时自动隐藏搜索框 + * + * 默认为 `NO` + */ +- (BOOL)shouldHideSearchBarWhenEmptyViewShowing; + +/** + * 初始化searchController和searchBar,在initSubViews的时候被自动调用。 + * + * 会询问 `self.shouldShowSearchBar`,若返回 `YES`,则创建 searchBar 并将其以 `tableHeaderView` 的形式呈现在界面里;若返回 `NO`,则将 `tableHeaderView` 置为nil。 + * + * @warning `self.shouldShowSearchBar` 默认为 NO,需要 searchBar 的界面必须手动将其置为 `YES`。 + */ +- (void)initSearchController; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/QMUISearchController.m b/QMUI/QMUIKit/QMUIComponents/QMUISearchController.m new file mode 100644 index 00000000..0a77d8c2 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUISearchController.m @@ -0,0 +1,536 @@ +/** + * 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. + */ + +// +// QMUISearchController.m +// Test +// +// Created by QMUI Team on 16/5/25. +// + +#import "QMUISearchController.h" +#import "QMUICore.h" +#import "QMUISearchBar.h" +#import "QMUICommonTableViewController.h" +#import "QMUIEmptyView.h" +#import "UISearchBar+QMUI.h" +#import "UITableView+QMUI.h" +#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 + +@class QMUISearchResultsTableViewController; + +@protocol QMUISearchResultsTableViewControllerDelegate + +- (void)didLoadTableViewInSearchResultsTableViewController:(QMUISearchResultsTableViewController *)viewController; +@end + +@interface QMUISearchResultsTableViewController : QMUICommonTableViewController + +@property(nonatomic,weak) id delegate; +@end + +@implementation QMUISearchResultsTableViewController + +- (void)initTableView { + [super initTableView]; + + // UISearchController.searchBar 作为 UITableView.tableHeaderView 时,进入搜索状态,搜索结果列表顶部有一大片空白 + // 不要让系统自适应了,否则在搜索结果(navigationBar 隐藏)push 进入下一级界面(navigationBar 显示)过程中系统自动调整的 contentInset 会跳来跳去 + // https://github.com/Tencent/QMUI_iOS/issues/1473 + self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + + self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; + if ([self.delegate respondsToSelector:@selector(didLoadTableViewInSearchResultsTableViewController:)]) { + [self.delegate didLoadTableViewInSearchResultsTableViewController:self]; + } +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + if ([self.delegate isKindOfClass:QMUISearchController.class]) { + QMUISearchController *searchController = (QMUISearchController *)self.delegate; + if (searchController.emptyViewShowing) { + [searchController layoutEmptyView]; + } + } +} + +@end + +@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 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]; + + _searchController = [[UISearchController alloc] initWithSearchResultsController:resultsViewController]; + self.searchController.obscuresBackgroundDuringPresentation = YES;// iOS 15 开始该默认值为 NO 了,为了保持与旧版本一致的表现,这里改默认值 + self.searchController.searchResultsUpdater = self; + self.searchController.delegate = self; + _searchBar = self.searchController.searchBar; + if (CGRectIsEmpty(self.searchBar.frame)) { + // iOS8 下 searchBar.frame 默认是 CGRectZero,不 sizeToFit 就看不到了 + [self.searchBar sizeToFit]; + } + [self.searchBar qmui_styledAsQMUISearchBar]; + + self.hidesNavigationBarDuringPresentation = YES; + } + 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 + [self.searchController loadViewIfNeeded]; +} + +- (void)setSearchResultsDelegate:(id)searchResultsDelegate { + _searchResultsDelegate = searchResultsDelegate; + self.tableView.dataSource = _searchResultsDelegate; + self.tableView.delegate = _searchResultsDelegate; +} + +- (void)setDimmingColor:(UIColor *)dimmingColor { + _dimmingColor = dimmingColor; + self.searchController.qmui_dimmingColor = dimmingColor; +} + +- (BOOL)isActive { + return self.searchController.active; +} + +- (void)setActive:(BOOL)active { + [self setActive:active animated:NO]; +} + +- (void)setActive:(BOOL)active animated:(BOOL)animated { + 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 { + 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 *)launchView { + _launchView = launchView; + self.searchController.qmui_launchView = launchView; +} + +- (BOOL)hidesNavigationBarDuringPresentation { + return self.searchController.hidesNavigationBarDuringPresentation; +} + +- (void)setHidesNavigationBarDuringPresentation:(BOOL)hidesNavigationBarDuringPresentation { + 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 { + // 搜索框文字为空时,界面会显示遮罩,此时不需要显示emptyView了 + // 为什么加这个是因为当搜索框被点击时(进入搜索状态)会触发searchController:updateResultsForSearchString:,里面如果直接根据搜索结果为空来showEmptyView的话,就会导致在遮罩层上有emptyView出现,要么在那边showEmptyView之前判断一下searchBar.text.length,要么在showEmptyView里判断,为了方便,这里选择后者。 + if (self.searchBar.text.length <= 0) { + return; + } + + [super showEmptyView]; + + // 格式化样式,以适应当前项目的需求 + self.emptyView.backgroundColor = TableViewBackgroundColor ?: UIColorWhite; + if ([self.searchResultsDelegate respondsToSelector:@selector(searchController:willShowEmptyView:)]) { + [self.searchResultsDelegate searchController:self willShowEmptyView:self.emptyView]; + } + + if (self.searchController) { + UIView *superview = self.searchController.searchResultsController.view; + [superview addSubview:self.emptyView]; + } else { + QMUIAssert(NO, NSStringFromClass(self.class), @"QMUISearchController 无法为 emptyView 找到合适的 superview"); + } + + [self layoutEmptyView]; +} + +#pragma mark - + +- (void)didLoadTableViewInSearchResultsTableViewController:(QMUISearchResultsTableViewController *)viewController { + if ([self.searchResultsDelegate respondsToSelector:@selector(searchController:didLoadSearchResultsTableView:)]) { + [self.searchResultsDelegate searchController:self didLoadSearchResultsTableView:viewController.tableView]; + } +} + +#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]; + } +} + +#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]; + } +} + +- (void)didPresentSearchController:(UISearchController *)searchController { + if ([self.searchResultsDelegate respondsToSelector:@selector(didPresentSearchController:)]) { + [self.searchResultsDelegate didPresentSearchController:self]; + } +} + +- (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 { + // 退出搜索必定先隐藏emptyView + [self hideEmptyView]; + + if ([self.searchResultsDelegate respondsToSelector:@selector(didDismissSearchController:)]) { + [self.searchResultsDelegate didDismissSearchController:self]; + } + + if (self.supportsSwipeToDismissSearch && !self.dismissBySwipe) { + [self cleanSnapshotObjects]; + } +} + +@end + +EndIgnoreDeprecatedWarning + +@implementation QMUICommonTableViewController (Search) + +QMUISynthesizeIdStrongProperty(searchController, setSearchController) +QMUISynthesizeIdStrongProperty(searchBar, setSearchBar) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + ExtendImplementationOfVoidMethodWithoutArguments([QMUICommonTableViewController class], @selector(initSubviews), ^(QMUICommonTableViewController *selfObject) { + [selfObject initSearchController]; + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([QMUICommonTableViewController class], @selector(viewWillAppear:), BOOL, ^(QMUICommonTableViewController *selfObject, BOOL firstArgv) { + if (!selfObject.searchController.tableView.allowsMultipleSelection) { + [selfObject.searchController.tableView qmui_clearsSelection]; + } + }); + + ExtendImplementationOfVoidMethodWithoutArguments([QMUICommonTableViewController class], @selector(showEmptyView), ^(QMUICommonTableViewController *selfObject) { + if ([selfObject shouldHideSearchBarWhenEmptyViewShowing] && selfObject.tableView.tableHeaderView == selfObject.searchBar) { + selfObject.tableView.tableHeaderView = nil; + } + }); + + ExtendImplementationOfVoidMethodWithoutArguments([QMUICommonTableViewController class], @selector(hideEmptyView), ^(QMUICommonTableViewController *selfObject) { + if (selfObject.shouldShowSearchBar && [selfObject shouldHideSearchBarWhenEmptyViewShowing] && selfObject.tableView.tableHeaderView == nil) { + [selfObject initSearchController]; + // 隐藏 emptyView 后重新设置 tableHeaderView,会导致原先 shouldHideTableHeaderViewInitial 隐藏头部的操作被重置,所以下面的 force 参数要传 YES + // https://github.com/Tencent/QMUI_iOS/issues/128 + selfObject.tableView.tableHeaderView = selfObject.searchBar; + [selfObject hideTableHeaderViewInitialIfCanWithAnimated:NO force:YES]; + } + }); + }); +} + +static char kAssociatedObjectKey_shouldShowSearchBar; +- (void)setShouldShowSearchBar:(BOOL)shouldShowSearchBar { + BOOL isValueChanged = self.shouldShowSearchBar != shouldShowSearchBar; + if (!isValueChanged) { + return; + } + + objc_setAssociatedObject(self, &kAssociatedObjectKey_shouldShowSearchBar, @(shouldShowSearchBar), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + if (shouldShowSearchBar) { + [self initSearchController]; + } else { + if (self.searchBar) { + if (self.tableView.tableHeaderView == self.searchBar) { + self.tableView.tableHeaderView = nil; + } + [self.searchBar removeFromSuperview]; + self.searchBar = nil; + } + if (self.searchController) { + self.searchController.searchResultsDelegate = nil; + self.searchController = nil; + } + } +} + +- (BOOL)shouldShowSearchBar { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_shouldShowSearchBar)) boolValue]; +} + +- (void)initSearchController { + if ([self isViewLoaded] && self.shouldShowSearchBar && !self.searchController) { + 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 + self.tableView.tableHeaderView = self.searchController.searchBar; + self.searchBar = self.searchController.searchBar; + } +} + +- (BOOL)shouldHideSearchBarWhenEmptyViewShowing { + return NO; +} + +#pragma mark - + +- (void)searchController:(QMUISearchController *)searchController updateResultsForSearchString:(NSString *)searchString { + +} + +@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/UIKitExtensions/QMUISegmentedControl.h b/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.h similarity index 76% rename from QMUI/QMUIKit/UIKitExtensions/QMUISegmentedControl.h rename to QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.h index 8700292e..20c389d2 100644 --- a/QMUI/QMUIKit/UIKitExtensions/QMUISegmentedControl.h +++ b/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.h @@ -1,9 +1,16 @@ +/** + * 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. + */ + // // QMUISegmentedControl.h // qmui // -// Created by ZhoonChen on 14/11/3. -// Copyright (c) 2014年 QMUI Team. All rights reserved. +// Created by QMUI Team on 14/11/3. // #import diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUISegmentedControl.m b/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.m similarity index 87% rename from QMUI/QMUIKit/UIKitExtensions/QMUISegmentedControl.m rename to QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.m index 19e0e32f..2381a4d6 100644 --- a/QMUI/QMUIKit/UIKitExtensions/QMUISegmentedControl.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUISegmentedControl.m @@ -1,9 +1,16 @@ +/** + * 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. + */ + // // QMUISegmentedControl.m // qmui // -// Created by ZhoonChen on 14/11/3. -// Copyright (c) 2014年 QMUI Team. All rights reserved. +// Created by QMUI Team on 14/11/3. // #import "QMUISegmentedControl.h" 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/QMUITableView.h b/QMUI/QMUIKit/QMUIComponents/QMUITableView.h new file mode 100644 index 00000000..4cfc9383 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableView.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. + */ + +// +// QMUITableView.h +// qmui +// +// Created by QMUI Team on 14-7-2. +// + +#import +#import "QMUITableViewProtocols.h" + +@interface QMUITableView : UITableView + +@property(nonatomic, weak) id delegate; +@property(nonatomic, weak) id dataSource; + +@end + diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITableView.m b/QMUI/QMUIKit/QMUIComponents/QMUITableView.m new file mode 100644 index 00000000..f65a5c16 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableView.m @@ -0,0 +1,72 @@ +/** + * 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. + */ + +// +// QMUITableView.m +// qmui +// +// Created by QMUI Team on 14-7-2. +// + +#import "QMUITableView.h" +#import "UITableView+QMUI.h" +#import "UIView+QMUI.h" + +@implementation QMUITableView + +@dynamic delegate; +@dynamic dataSource; + +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style { + if (self = [super initWithFrame:frame style:style]) { + [self didInitialize]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self didInitialize]; + } + return self; +} + +- (void)didInitialize { + [self qmui_styledAsQMUITableView]; +} + +- (void)dealloc { + self.dataSource = nil; + self.delegate = nil; +} + +// 保证一直存在tableFooterView,以去掉列表内容不满一屏时尾部的空白分割线 +- (void)setTableFooterView:(UIView *)tableFooterView { + if (!tableFooterView) { + tableFooterView = [[UIView alloc] init]; + } + [super setTableFooterView:tableFooterView]; +} + +- (BOOL)touchesShouldCancelInContentView:(UIView *)view { + if ([self.delegate respondsToSelector:@selector(tableView:touchesShouldCancelInContentView:)]) { + return [self.delegate tableView:self touchesShouldCancelInContentView:view]; + } + // 默认情况下只有当view是非UIControl的时候才会返回yes,这里统一对UIButton也返回yes + // 原因是UITableView上面把事件延迟去掉了,但是这样如果拖动的时候手指是在UIControl上面的话,就拖动不了了 + if ([view isKindOfClass:[UIControl class]]) { + if ([view isKindOfClass:[UIButton class]]) { + return YES; + } else { + return NO; + } + } + return YES; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.h b/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.h new file mode 100644 index 00000000..c5582d56 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.h @@ -0,0 +1,97 @@ +/** + * 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. + */ + +// +// QMUITableViewCell.h +// qmui +// +// Created by QMUI Team on 14-7-7. +// + +#import +#import "UITableView+QMUI.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QMUITableViewCell : UITableViewCell + +@property(nonatomic, assign, readonly) UITableViewCellStyle style; + +/** + * 调整 imageView 的位置偏移,常用于调整 imageView 和 textLabel 之间的间距,默认为 UIEdgeInsetsZero。 + * @warning 目前只对 UITableViewCellStyleDefault 和 UITableViewCellStyleSubtitle 类型的 cell 开放 + */ +@property(nonatomic, assign) UIEdgeInsets imageEdgeInsets; + +/** + * 调整 textLabel 的位置偏移,默认为 UIEdgeInsetsZero。 + * @warning 目前只对 UITableViewCellStyleDefault 和 UITableViewCellStyleSubtitle 类型的 cell 开放 + */ +@property(nonatomic, assign) UIEdgeInsets textLabelEdgeInsets; + +/// 调整 detailTextLabel 的位置偏移,默认为 UIEdgeInsetsZero。 +@property(nonatomic, assign) UIEdgeInsets detailTextLabelEdgeInsets; + +/** + 调整右边 accessoryView 的布局偏移,默认为 UIEdgeInsetsZero。 + @warning 对系统原生的 view 不生效(例如向右箭头、“i”详情按钮等),如果通过配置表设置了 TableViewCellDisclosureIndicatorImage,由于该配置本质上是使用了自定义的 accessoryView 来实现,所以这个属性对其生效。 + */ +@property(nonatomic, assign) UIEdgeInsets accessoryEdgeInsets; + +/** + 调整右边 accessoryView 的点击响应区域,可用负值扩大点击范围,默认为(-12, -12, -12, -12)。 + @warning 对系统原生的 view 不生效(例如向右箭头、“i”详情按钮等),如果通过配置表设置了 TableViewCellDetailButtonImage,由于该配置本质上是使用了自定义的 accessoryView 来实现,所以这个属性对其生效。 + */ +@property(nonatomic, assign) UIEdgeInsets accessoryHitTestEdgeInsets; + +/// 设置当前 cell 是否可用,setter 方法里面会修改当前的 subviews 样式,以展示出禁用的样式,具体样式请查看源码。 +@property(nonatomic, assign, getter = isEnabled) BOOL enabled; + +/// 保存对 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; + +/** + * 首选初始化方法 + * + * @param tableView cell所在的tableView + * @param style tableView的style + * @param reuseIdentifier tableView的reuseIdentifier + * + * @return 一个QMUITableViewCell实例 + */ +- (nullable instancetype)initForTableView:(nullable UITableView *)tableView withStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier; + +/// 同上 +- (nullable instancetype)initForTableView:(nullable UITableView *)tableView withReuseIdentifier:(NSString *)reuseIdentifier; + +@end + + +@interface QMUITableViewCell (QMUISubclassingHooks) + +/** + * 初始化时调用的方法,会在两个 NS_DESIGNATED_INITIALIZER 方法中被调用,所以子类如果需要同时支持两个 NS_DESIGNATED_INITIALIZER 方法,则建议把初始化时要做的事情放到这个方法里。否则仅需重写要支持的那个 NS_DESIGNATED_INITIALIZER 方法即可。 + */ +- (void)didInitializeWithStyle:(UITableViewCellStyle)style NS_REQUIRES_SUPER; + +/// 用于继承的接口,设置一些cell相关的UI,需要自 cellForRowAtIndexPath 里面调用。默认实现是设置当前cell在哪个position。 +- (void)updateCellAppearanceWithIndexPath:(nullable NSIndexPath *)indexPath NS_REQUIRES_SUPER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUITableViewCell.m b/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.m similarity index 77% rename from QMUI/QMUIKit/UIKitExtensions/QMUITableViewCell.m rename to QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.m index 5d816eb7..ff908098 100644 --- a/QMUI/QMUIKit/UIKitExtensions/QMUITableViewCell.m +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableViewCell.m @@ -1,18 +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. + */ + // // QMUITableViewCell.m // qmui // -// Created by QQMail on 14-7-7. -// Copyright (c) 2014年 QMUI Team. All rights reserved. +// Created by QMUI Team on 14-7-7. // #import "QMUITableViewCell.h" #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; @@ -24,14 +33,18 @@ @implementation QMUITableViewCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { - [self didInitializedWithStyle: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; } @@ -42,55 +55,12 @@ - (instancetype)initForTableView:(UITableView *)tableView withReuseIdentifier:(N - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { - [self didInitializedWithStyle:UITableViewCellStyleDefault]; + [self didInitializeWithStyle:UITableViewCellStyleDefault]; } return self; } -- (void)didInitializedWithStyle:(UITableViewCellStyle)style { - _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 *selectedBackgroundColor = TableViewCellSelectedBackgroundColor; - if (selectedBackgroundColor) { - UIView *selectedBackgroundView = [[UIView alloc] init]; - selectedBackgroundView.backgroundColor = selectedBackgroundColor; - self.selectedBackgroundView = selectedBackgroundView; - } - - // 因为在hitTest里扩大了accessoryView的响应范围,因此提高了系统一个与此相关的bug的出现几率,所以又在scrollView.delegate里做一些补丁性质的东西来修复 - if ([self.subviews.firstObject isKindOfClass:[UIScrollView class]]) { - UIScrollView *scrollView = (UIScrollView *)[self.subviews objectAtIndex:0]; - scrollView.delegate = 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]; @@ -124,30 +94,27 @@ - (void)layoutSubviews { imageViewFrame.origin.y += self.imageEdgeInsets.top - self.imageEdgeInsets.bottom; textLabelFrame.origin.x += self.imageEdgeInsets.left; - textLabelFrame.size.width = fminf(CGRectGetWidth(textLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(textLabelFrame)); + textLabelFrame.size.width = fmin(CGRectGetWidth(textLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(textLabelFrame)); if (shouldChangeDetailTextLabelFrame) { detailTextLabelFrame.origin.x += self.imageEdgeInsets.left; - detailTextLabelFrame.size.width = fminf(CGRectGetWidth(detailTextLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(detailTextLabelFrame)); + detailTextLabelFrame.size.width = fmin(CGRectGetWidth(detailTextLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(detailTextLabelFrame)); } } 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 = fminf(CGRectGetWidth(textLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(textLabelFrame)); + textLabelFrame.size.width = fmin(CGRectGetWidth(textLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(textLabelFrame)); } if (hasCustomDetailLabelEdgeInsets) { detailTextLabelFrame.origin.x += self.detailTextLabelEdgeInsets.left - self.detailTextLabelEdgeInsets.right; detailTextLabelFrame.origin.y += self.detailTextLabelEdgeInsets.top - self.detailTextLabelEdgeInsets.bottom; - detailTextLabelFrame.size.width = fminf(CGRectGetWidth(detailTextLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(detailTextLabelFrame)); + detailTextLabelFrame.size.width = fmin(CGRectGetWidth(detailTextLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(detailTextLabelFrame)); } self.imageView.frame = imageViewFrame; self.textLabel.frame = textLabelFrame; self.detailTextLabel.frame = detailTextLabelFrame; - - // `layoutSubviews`这里不可以拿textLabel的minX来设置separatorInset,如果要设置只能写死一个值 - // 否则会导致textLabel的minX逐渐叠加从而使textLabel被移出屏幕外 } // 由于调整 accessoryEdgeInsets 可能会影响 contentView 的宽度,所以几个 subviews 的布局也要保护一下 @@ -161,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; } @@ -203,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 = @"更多信息"; } } @@ -254,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]; @@ -266,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; @@ -331,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]]; } } @@ -340,11 +306,31 @@ - (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); + + // 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 new file mode 100644 index 00000000..5dde954a --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.h @@ -0,0 +1,53 @@ +/** + * 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. + */ + +// +// QMUITableViewHeaderFooterView.h +// QMUIKit +// +// Created by QMUI Team on 2017/12/7. +// + +#import + +typedef NS_ENUM(NSUInteger, QMUITableViewHeaderFooterViewType) { + QMUITableViewHeaderFooterViewTypeUnknow, + QMUITableViewHeaderFooterViewTypeHeader, + QMUITableViewHeaderFooterViewTypeFooter +}; + +/** + * 适用于 UITableView 的 sectionHeaderFooterView,提供的特性包括: + * 1. 支持单个 UILabel,该 label 支持多行文字。 + * 2. 支持右边添加一个 accessoryView(注意,设置 accessoryView 之前请先保证自身大小正确)。 + * 3. 支持调整 headerFooterView 的 padding。 + * 4. 支持应用配置表的样式。 + * + * 使用方式: + * 基本与系统的 UITableViewHeaderFooterView 使用方式一致,额外需要做的事情有: + * 1. 如果要支持高度自动根据内容变化,则按系统的 self-sizing 方式,用 UITableViewAutomaticDimension 指定。或者重写 tableView:heightForHeaderInSection:、tableView:heightForFooterInSection:,在里面调用 headerFooterView 的 sizeThatFits:。 + * 2. 如果要应用配置表样式,则设置 parentTableView 和 type 这两个属性即可。特别的,QMUICommonTableViewController 里默认已经处理好 parentTableView 和 type,子类无需操作。 + */ +@interface QMUITableViewHeaderFooterView : UITableViewHeaderFooterView + +@property(nonatomic, weak) UITableView *parentTableView; +@property(nonatomic, assign) QMUITableViewHeaderFooterViewType type; + +@property(nonatomic, strong, readonly) UILabel *titleLabel; +@property(nonatomic, strong) __kindof UIView *accessoryView; + +@property(nonatomic, assign) UIEdgeInsets contentEdgeInsets; +@property(nonatomic, assign) UIEdgeInsets accessoryViewMargins; +@end + +@interface QMUITableViewHeaderFooterView (UISubclassingHooks) + +/// 子类重写,用于修改样式,会在 parentTableView、type 属性发生变化的时候被调用 +- (void)updateAppearance; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.m b/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.m new file mode 100644 index 00000000..d0661f25 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.m @@ -0,0 +1,143 @@ +/** + * 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. + */ + +// +// QMUITableViewHeaderFooterView.m +// QMUIKit +// +// Created by QMUI Team on 2017/12/7. +// + +#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]) { + [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 (!QMUICMIActivated || (!self.parentTableView && !self.qmui_tableView) || self.type == QMUITableViewHeaderFooterViewTypeUnknow) return; + + UITableViewStyle style = (self.parentTableView ?: self.qmui_tableView).style; + + if (self.type == QMUITableViewHeaderFooterViewTypeHeader) { + 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 = 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); + } +} + +- (void)layoutSubviews { + [super layoutSubviews]; + if (self.accessoryView) { + [self.accessoryView sizeToFit]; + self.accessoryView.qmui_right = self.contentView.qmui_width - self.contentEdgeInsets.right - self.accessoryViewMargins.right; + 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.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; + 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 { + CGSize resultSize = size; + + CGSize accessoryViewSize = self.accessoryView ? self.accessoryView.frame.size : CGSizeZero; + if (self.accessoryView) { + accessoryViewSize.width = accessoryViewSize.width + UIEdgeInsetsGetHorizontalValue(self.accessoryViewMargins); + accessoryViewSize.height = accessoryViewSize.height + UIEdgeInsetsGetVerticalValue(self.accessoryViewMargins); + } + + CGFloat titleLabelWidth = size.width - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) - accessoryViewSize.width; + CGSize titleLabelSize = [self.titleLabel sizeThatFits:CGSizeMake(titleLabelWidth, CGFLOAT_MAX)]; + + resultSize.height = fmax(titleLabelSize.height, accessoryViewSize.height) + UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets); + return resultSize; +} + +#pragma mark - getter / setter + +- (void)setAccessoryView:(UIView *)accessoryView { + if (_accessoryView && _accessoryView != accessoryView) { + [_accessoryView removeFromSuperview]; + } + _accessoryView = accessoryView; + self.isAccessibilityElement = NO; + self.titleLabel.accessibilityTraits |= UIAccessibilityTraitHeader; + [self.contentView addSubview:accessoryView]; +} + +- (void)setParentTableView:(UITableView *)parentTableView { + _parentTableView = parentTableView; + [self updateAppearance]; +} + +- (void)setType:(QMUITableViewHeaderFooterViewType)type { + _type = type; + [self updateAppearance]; +} + +- (void)setContentEdgeInsets:(UIEdgeInsets)contentEdgeInsets { + _contentEdgeInsets = contentEdgeInsets; + [self setNeedsLayout]; +} + +- (void)setAccessoryViewMargins:(UIEdgeInsets)accessoryViewMargins { + _accessoryViewMargins = accessoryViewMargins; + [self setNeedsLayout]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITableViewProtocols.h b/QMUI/QMUIKit/QMUIComponents/QMUITableViewProtocols.h new file mode 100644 index 00000000..b56fb2f7 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITableViewProtocols.h @@ -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. + */ + +// +// QMUITableViewProtocols.h +// qmui +// +// Created by QMUI Team on 2016/12/9. +// + +#import + +@class QMUITableView; + +@protocol QMUICellHeightCache_UITableViewDataSource + +@optional +/// 搭配 QMUICellHeightCache 使用,对于 UITableView 而言如果要用 QMUICellHeightCache 那套高度计算方式,则必须实现这个方法 +- (nullable __kindof UITableViewCell *)qmui_tableView:(nullable UITableView *)tableView cellWithIdentifier:(nonnull NSString *)identifier; + +@end + +@protocol QMUICellHeightKeyCache_UITableViewDelegate + +@optional + +- (nonnull id)qmui_tableView:(nonnull UITableView *)tableView cacheKeyForRowAtIndexPath:(nonnull NSIndexPath *)indexPath; +@end + +@protocol QMUITableViewDelegate + +@optional + +/** + * 自定义要在- (BOOL)touchesShouldCancelInContentView:(UIView *)view内的逻辑
+ * 若delegate不实现这个方法,则默认对所有UIControl返回NO(UIButton除外,它会返回YES),非UIControl返回YES。 + */ +- (BOOL)tableView:(nonnull QMUITableView *)tableView touchesShouldCancelInContentView:(nonnull UIView *)view; + +@end + + +@protocol QMUITableViewDataSource + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITestView.h b/QMUI/QMUIKit/QMUIComponents/QMUITestView.h new file mode 100644 index 00000000..d584b718 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITestView.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. + */ + +// +// QMUITestView.h +// qmui +// +// Created by QMUI Team on 16/1/28. +// + +#import + +@interface QMUITestView : UIView + +@end + + +@interface QMUITestWindow : UIWindow + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITestView.m b/QMUI/QMUIKit/QMUIComponents/QMUITestView.m new file mode 100644 index 00000000..393d281a --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITestView.m @@ -0,0 +1,155 @@ +/** + * 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. + */ + +// +// QMUITestView.m +// qmui +// +// Created by QMUI Team on 16/1/28. +// + +#import "QMUITestView.h" +#import "QMUILog.h" + +@implementation QMUITestView + +- (instancetype)init { + if (self = [super 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]) { + + } + return self; +} + +- (void)dealloc { + QMUILog(NSStringFromClass(self.class), @"%@, dealloc", self); +} + +- (void)setFrame:(CGRect)frame { + CGRect oldFrame = self.frame; + BOOL isFrameChanged = CGRectEqualToRect(oldFrame, frame); + if (!isFrameChanged) { + QMUILog(NSStringFromClass(self.class), @"frame 发生变化, 旧的是 %@, 新的是 %@", NSStringFromCGRect(oldFrame), NSStringFromCGRect(frame)); + } + [super setFrame:frame]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + QMUILog(NSStringFromClass(self.class), @"frame = %@", NSStringFromCGRect(self.frame)); +} + +- (void)willMoveToSuperview:(UIView *)newSuperview { + [super willMoveToSuperview:newSuperview]; + QMUILog(NSStringFromClass(self.class), @"superview is %@, newSuperview is %@, window is %@", self.superview, newSuperview, self.window); +} + +- (void)didMoveToSuperview { + [super didMoveToSuperview]; + QMUILog(NSStringFromClass(self.class), @"superview is %@, window is %@", self.superview, self.window); +} + +- (void)willMoveToWindow:(UIWindow *)newWindow { + [super willMoveToWindow:newWindow]; + QMUILog(NSStringFromClass(self.class), @"self.window is %@, newWindow is %@", self.window, newWindow); +} + +- (void)didMoveToWindow { + [super didMoveToWindow]; + QMUILog(NSStringFromClass(self.class), @"self.window is %@", self.window); +} + +- (void)addSubview:(UIView *)view { + [super addSubview:view]; + 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), @"hidden is %@", @(hidden)); +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + UIView *view = [super hitTest:point withEvent:event]; + return view; +} + +@end + +@implementation QMUITestWindow + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + + } + return self; +} + +- (void)dealloc { + QMUILog(NSStringFromClass(self.class), @"dealloc, %@", self); +} + +- (void)setRootViewController:(UIViewController *)rootViewController { + [super setRootViewController:rootViewController]; +} + +- (void)makeKeyAndVisible { + [super makeKeyAndVisible]; +} + +- (void)makeKeyWindow { + [super makeKeyWindow]; +} + +- (void)setHidden:(BOOL)hidden { + [super setHidden:hidden]; +} + +- (void)addSubview:(UIView *)view { + [super addSubview:view]; + QMUILog(NSStringFromClass(self.class), @"QMUITestWindow, subviews = %@, view = %@", self.subviews, view); +} + +- (void)setFrame:(CGRect)frame { + CGRect oldFrame = self.frame; + BOOL isFrameChanged = CGRectEqualToRect(oldFrame, frame); + if (isFrameChanged) { + QMUILog(NSStringFromClass(self.class), @"QMUITestWindow, frame发生变化, old is %@, new is %@", NSStringFromCGRect(oldFrame), NSStringFromCGRect(frame)); + } + [super setFrame:frame]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + QMUILog(NSStringFromClass(self.class), @"QMUITestWindow, layoutSubviews"); +} + +- (void)setAlpha:(CGFloat)alpha { + [super setAlpha:alpha]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITextField.h b/QMUI/QMUIKit/QMUIComponents/QMUITextField.h new file mode 100644 index 00000000..5b71e029 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITextField.h @@ -0,0 +1,104 @@ +/** + * 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. + */ + +// +// QMUITextField.h +// qmui +// +// Created by QMUI Team on 16-11-03 +// + +#import + +@class QMUITextField; + +@protocol QMUITextFieldDelegate + +@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 这两个参数的值也会比较特殊,具体请看参数讲解。 + * + * @param textField 触发的 textField + * @param range 要变化的文字的位置,如果在 UIControlEventEditingChanged 里,这里的 range 也即文字变化后的 range,所以可能比最大长度要大。 + * @param replacementString 要变化的文字,如果在 UIControlEventEditingChanged 里,这里永远传入 nil。 + */ +- (void)textField:(QMUITextField *)textField didPreventTextChangeInRange:(NSRange)range replacementString:(NSString *)replacementString; + +@end + +/** + * 支持的特性包括: + * + * 1. 自定义 placeholderColor。 + * 2. 自定义 UITextField 的文字 padding。 + * 3. 支持限制输入的文字的长度。 + * 4. 修复 iOS 10 之后 UITextField 输入中文超过文本框宽度后再删除,文字往下掉的 bug。 + */ +@interface QMUITextField : UITextField + +@property(nonatomic, weak) id delegate; + +/** + * 修改 placeholder 的颜色,默认是 UIColorPlaceholder。 + */ +@property(nonatomic, strong) IBInspectable UIColor *placeholderColor; + +/** + * 文字在输入框内的 padding。如果出现 clearButton,则 textInsets.right 会控制 clearButton 的右边距 + * + * 默认为 TextFieldTextInsets + */ +@property(nonatomic, assign) UIEdgeInsets textInsets; + +/** + clearButton 在默认位置上的偏移 + */ +@property(nonatomic, assign) UIOffset clearButtonPositionAdjustment UI_APPEARANCE_SELECTOR; + +/** + * 当通过 `setText:`、`setAttributedText:`等方式修改文字时,是否应该自动触发 UIControlEventEditingChanged 事件及 UITextFieldTextDidChangeNotification 通知。 + * + * 默认为YES(注意系统的 UITextField 对这种行为默认是 NO) + */ +@property(nonatomic, assign) IBInspectable BOOL shouldResponseToProgrammaticallyTextChanges; + +/** + * 显示允许输入的最大文字长度,默认为 NSUIntegerMax,也即不限制长度。 + */ +@property(nonatomic, assign) IBInspectable NSUInteger maximumTextLength; + +/** + * 在使用 maximumTextLength 功能的时候,是否应该把文字长度按照 [NSString (QMUI) qmui_lengthWhenCountingNonASCIICharacterAsTwo] 的方法来计算。 + * 默认为 NO。 + */ +@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 new file mode 100644 index 00000000..44ce23f9 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITextField.m @@ -0,0 +1,309 @@ +/** + * 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. + */ + +// +// QMUITextField.m +// qmui +// +// Created by QMUI Team on 16-11-03 +// + +#import "QMUITextField.h" +#import "QMUICore.h" +#import "NSString+QMUI.h" +#import "UITextField+QMUI.h" +#import "QMUIMultipleDelegates.h" + +// 私有的类,专用于实现 QMUITextFieldDelegate,避免 self.delegate = self 的写法(以前是 QMUITextField 自己实现了 delegate) +@interface _QMUITextFieldDelegator : NSObject + +@property(nonatomic, weak) QMUITextField *textField; +- (void)handleTextChangeEvent:(QMUITextField *)textField; +@end + +@interface QMUITextField () + +@property(nonatomic, strong) _QMUITextFieldDelegator *delegator; +@end + +@implementation QMUITextField + +@dynamic delegate; + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self didInitialize]; + if (QMUICMIActivated) { + UIColor *textColor = TextFieldTextColor; + if (textColor) { + self.textColor = textColor; + } + + self.tintColor = TextFieldTintColor; + } + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self didInitialize]; + } + return self; +} + +- (void)didInitialize { + self.qmui_multipleDelegatesEnabled = YES; + self.delegator = [[_QMUITextFieldDelegator alloc] init]; + self.delegator.textField = self; + self.delegate = self.delegator; + [self addTarget:self.delegator action:@selector(handleTextChangeEvent:) forControlEvents:UIControlEventEditingChanged]; + + self.shouldResponseToProgrammaticallyTextChanges = YES; + self.maximumTextLength = NSUIntegerMax; + + if (QMUICMIActivated) { + self.placeholderColor = UIColorPlaceholder; + self.textInsets = TextFieldTextInsets; + } +} + +- (void)dealloc { + self.delegate = nil; +} + +#pragma mark - Placeholder + +- (void)setPlaceholderColor:(UIColor *)placeholderColor { + _placeholderColor = placeholderColor; + if (self.placeholder) { + [self updateAttributedPlaceholderIfNeeded]; + } +} + +- (void)setPlaceholder:(NSString *)placeholder { + [super setPlaceholder:placeholder]; + if (self.placeholderColor) { + [self updateAttributedPlaceholderIfNeeded]; + } +} + +- (void)updateAttributedPlaceholderIfNeeded { + self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder attributes:@{NSForegroundColorAttributeName: self.placeholderColor}]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + // 以下代码修复系统的 UITextField 在 iOS 10 下的 bug:https://github.com/Tencent/QMUI_iOS/issues/64 + 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 { + NSString *textBeforeChange = self.text; + [super setText:text]; + + if (self.shouldResponseToProgrammaticallyTextChanges && ![textBeforeChange isEqualToString:text]) { + [self fireTextDidChangeEventForTextField:self]; + } +} + +- (void)setAttributedText:(NSAttributedString *)attributedText { + NSAttributedString *textBeforeChange = self.attributedText; + [super setAttributedText:attributedText]; + if (self.shouldResponseToProgrammaticallyTextChanges && ![textBeforeChange isEqualToAttributedString:attributedText]) { + [self fireTextDidChangeEventForTextField:self]; + } +} + +- (void)fireTextDidChangeEventForTextField:(QMUITextField *)textField { + [textField sendActionsForControlEvents:UIControlEventEditingChanged]; + [[NSNotificationCenter defaultCenter] postNotificationName:UITextFieldTextDidChangeNotification object:textField]; +} + +- (NSUInteger)lengthWithString:(NSString *)string { + return self.shouldCountingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length; +} + +#pragma mark - Positioning Overrides + +// 这样写已经可以让 sizeThatFits 时高度加上 textInsets 的值了 +- (CGRect)textRectForBounds:(CGRect)bounds { + bounds = CGRectInsetEdges(bounds, self.textInsets); + CGRect resultRect = [super textRectForBounds:bounds]; + return resultRect; +} + +- (CGRect)editingRectForBounds:(CGRect)bounds { + bounds = CGRectInsetEdges(bounds, self.textInsets); + return [super editingRectForBounds:bounds]; +} + +- (CGRect)clearButtonRectForBounds:(CGRect)bounds { + CGRect result = [super clearButtonRectForBounds:bounds]; + result = CGRectOffset(result, self.clearButtonPositionAdjustment.horizontal, self.clearButtonPositionAdjustment.vertical); + 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 + +#pragma mark - + +- (BOOL)textField:(QMUITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { + if (textField.maximumTextLength < NSUIntegerMax) { + + // 如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符),所以在 shouldChange 这里不会限制,而是放在 didChange 那里限制。 + if (textField.markedTextRange) { + 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; + if ([textField lengthWithString:textField.text] - rangeLength + [textField lengthWithString:string] > textField.maximumTextLength) { + // 将要插入的文字裁剪成这么长,就可以让它插入了 + NSInteger substringLength = textField.maximumTextLength - [textField lengthWithString:textField.text] + rangeLength; + 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]; + } + } + } + + if ([textField.delegate respondsToSelector:@selector(textField:didPreventTextChangeInRange:replacementString:)]) { + [textField.delegate textField:textField didPreventTextChangeInRange:range replacementString:string]; + } + return NO; + } + } + + if ([textField.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:originalValue:)]) { + return [textField.delegate textField:textField shouldChangeCharactersInRange:range replacementString:string originalValue:YES]; + } + + return YES; +} + +- (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) { + 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]; + } + } + } +} + +#pragma mark - + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + + // 以下代码修复系统的 UITextField 在 iOS 10 下的 bug:https://github.com/Tencent/QMUI_iOS/issues/64 + + if (scrollView != self.textField.subviews.firstObject) { + return; + } + + CGFloat lineHeight = ((NSParagraphStyle *)self.textField.defaultTextAttributes[NSParagraphStyleAttributeName]).minimumLineHeight; + lineHeight = lineHeight ?: ((UIFont *)self.textField.defaultTextAttributes[NSFontAttributeName]).lineHeight; + if (scrollView.contentSize.height > ceil(lineHeight) && scrollView.contentOffset.y < 0) { + scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, 0); + } +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITextView.h b/QMUI/QMUIKit/QMUIComponents/QMUITextView.h new file mode 100644 index 00000000..f6c59915 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITextView.h @@ -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. + */ + +// +// QMUITextView.h +// qmui +// +// Created by QMUI Team on 14-8-5. +// + +#import + +@class QMUITextView; + +@protocol QMUITextViewDelegate + +@optional +/** + * 输入框高度发生变化时的回调,当实现了这个方法后,文字输入过程中就会不断去计算输入框新内容的高度,并通过这个方法通知到 delegate + * @note 只有当内容高度与当前输入框的高度不一致时才会调用到这里,所以无需在内部做高度是否变化的判断。 + */ +- (void)textView:(QMUITextView *)textView newHeightAfterTextChanged:(CGFloat)height; + +/** + * 用户点击键盘的 return 按钮时的回调(return 按钮本质上是输入换行符“\n”) + * @return 返回 YES 表示程序认为当前的点击是为了进行类似“发送”之类的操作,所以最终“\n”并不会被输入到文本框里。返回 NO 表示程序认为当前的点击只是普通的输入,所以会继续询问 textView:shouldChangeTextInRange:replacementText: 方法,根据该方法的返回结果来决定是否要输入这个“\n”。 + * @see maximumTextLength + */ +- (BOOL)textViewShouldReturn:(QMUITextView *)textView; + +/** + 由于 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 要变化的文字的位置,length > 0 表示文字被自动裁剪前,输入框已有一段文字被选中。 + * @param replacementText 要变化的文字 + */ +- (void)textView:(QMUITextView *)textView didPreventTextChangeInRange:(NSRange)range replacementText:(NSString *)replacementText; + +@end + +/** + * 自定义 UITextView,提供的特性如下: + * + * 1. 支持 placeholder 并支持更改 placeholderColor;若使用了富文本文字,则 placeholder 的样式也会跟随文字的样式(除了 placeholder 颜色) + * 2. 支持在文字发生变化时计算内容高度并通知 delegate。 + * 3. 支持限制输入框最大高度,一般配合第 2 点使用。 + * 4. 支持限制输入的文本的最大长度,默认不限制。 + * 5. 修正系统 UITextView 在输入时自然换行的时候,contentOffset 的滚动位置没有考虑 textContainerInset.bottom + */ +@interface QMUITextView : UITextView + +@property(nonatomic, weak) id delegate; + +/** + * 当通过 `setText:`、`setAttributedText:`等方式修改文字时,是否应该自动触发 `UITextViewDelegate` 里的 `textView:shouldChangeTextInRange:replacementText:`、 `textViewDidChange:` 方法 + * + * 默认为YES(注意系统的 UITextView 对这种行为默认是 NO) + */ +@property(nonatomic, assign) IBInspectable BOOL shouldResponseToProgrammaticallyTextChanges; + +/** + * 显示允许输入的最大文字长度,默认为 NSUIntegerMax,也即不限制长度。 + */ +@property(nonatomic, assign) IBInspectable NSUInteger maximumTextLength; + +/** + * 在使用 maximumTextLength 功能的时候,是否应该把文字长度按照 [NSString (QMUI) qmui_lengthWhenCountingNonASCIICharacterAsTwo] 的方法来计算。 + * 默认为 NO。 + */ +@property(nonatomic, assign) IBInspectable BOOL shouldCountingNonASCIICharacterAsTwo; + +/** + * placeholder 的文字 + */ +@property(nonatomic, copy) IBInspectable NSString *placeholder; + +/** + * placeholder 文字的颜色 + */ +@property(nonatomic, strong) IBInspectable UIColor *placeholderColor; + +/** + * placeholder 在默认位置上的偏移(默认位置会自动根据 textContainerInset、contentInset 来调整) + */ +@property(nonatomic, assign) UIEdgeInsets placeholderMargins; + +/** + * 最大高度,当设置了这个属性后,超过这个高度值的 frame 是不生效的。默认为 CGFLOAT_MAX,也即无限制。 + */ +@property(nonatomic, assign) CGFloat maximumHeight; + +/** + 在 textView:shouldChangeTextInRange:replacementText: 里可用这个属性判断当前是否点击了删除。特别注意,当输入框为空时继续点删除也会触发,且这种情况只能通过这个属性区分,无法用别的判断方式。 + */ +@property(nonatomic, assign) BOOL isDeletingDuringTextChange; + +/** + * 控制输入框是否要出现“粘贴”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/QMUITextView.m b/QMUI/QMUIKit/QMUIComponents/QMUITextView.m new file mode 100644 index 00000000..0f319661 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITextView.m @@ -0,0 +1,510 @@ +/** + * 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. + */ + +// +// QMUITextView.m +// qmui +// +// Created by QMUI Team on 14-8-5. +// +#import "QMUITextView.h" +#import "QMUICore.h" +#import "QMUILabel.h" +#import "NSObject+QMUI.h" +#import "NSString+QMUI.h" +#import "UITextView+QMUI.h" +#import "UIScrollView+QMUI.h" +#import "QMUILog.h" +#import "QMUIMultipleDelegates.h" + +/// 系统 textView 默认的字号大小,用于 placeholder 默认的文字大小。实测得到,请勿修改。 +const CGFloat kSystemTextViewDefaultFontPointSize = 12.0f; + +/// 当系统的 textView.textContainerInset 为 UIEdgeInsetsZero 时,文字与 textView 边缘的间距。实测得到,请勿修改(在输入框font大于13时准确,小于等于12时,y有-1px的偏差)。 +const UIEdgeInsets kSystemTextViewFixTextInsets = {0, 5, 0, 5}; + +// 私有的类,专用于实现 QMUITextViewDelegate,避免 self.delegate = self 的写法(以前是 QMUITextView 自己实现了 delegate) +@interface _QMUITextViewDelegator : NSObject + +@property(nonatomic, weak) QMUITextView *textView; +@end + +@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; + +@end + +@implementation QMUITextView + +@dynamic delegate; + +- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer { + if (self = [super initWithFrame:frame textContainer:textContainer]) { + [self didInitialize]; + if (QMUICMIActivated) { + UIColor *textColor = TextFieldTextColor; + if (textColor) { + self.textColor = textColor; + } + self.tintColor = TextFieldTintColor; + } + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self didInitialize]; + } + return self; +} + +- (void)didInitialize { + self.debug = NO; + + self.qmui_multipleDelegatesEnabled = YES; + self.delegator = [[_QMUITextViewDelegator alloc] init]; + self.delegator.textView = self; + self.delegate = self.delegator; + + self.scrollsToTop = NO; + if (QMUICMIActivated) self.placeholderColor = UIColorPlaceholder; + self.placeholderMargins = UIEdgeInsetsZero; + self.maximumHeight = CGFLOAT_MAX; + self.maximumTextLength = NSUIntegerMax; + self.shouldResponseToProgrammaticallyTextChanges = YES; + self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + + self.placeholderLabel = [[UILabel alloc] init]; + self.placeholderLabel.font = UIFontMake(kSystemTextViewDefaultFontPointSize); + self.placeholderLabel.textColor = self.placeholderColor; + self.placeholderLabel.numberOfLines = 0; + 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 { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + self.delegate = nil; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@; text.length: %@ | %@; markedTextRange: %@", [super description], @(self.text.length), @([self lengthWithString:self.text]), self.markedTextRange]; +} + +- (BOOL)isCurrentTextDifferentOfText:(NSString *)text { + NSString *textBeforeChange = self.text;// UITextView 如果文字为空,self.text 永远返回 @"" 而不是 nil(即便你设置为 nil 后立即 get 出来也是) + if ([textBeforeChange isEqualToString:text] || (textBeforeChange.length == 0 && !text)) { + return NO; + } + return YES; +} + +- (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 { + BOOL textDifferent = [self isCurrentTextDifferentOfText:attributedText.string]; + + if (!textDifferent) { + [super setAttributedText:attributedText]; + return; + } + + if (self.shouldResponseToProgrammaticallyTextChanges) { + 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 setAttributedText:attributedText]; + if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) { + [self.delegate textViewDidChange:self]; + } + } else { + [super setAttributedText:attributedText]; + } + + [self handleTextChanged:self]; +} + +- (void)setTypingAttributes:(NSDictionary *)typingAttributes { + [super setTypingAttributes:typingAttributes]; + [self updatePlaceholderStyle]; +} + +- (void)setFont:(UIFont *)font { + [super setFont:font]; + [self updatePlaceholderStyle]; +} + +- (void)setTextColor:(UIColor *)textColor { + [super setTextColor:textColor]; + [self updatePlaceholderStyle]; +} + +- (void)setTextAlignment:(NSTextAlignment)textAlignment { + [super setTextAlignment:textAlignment]; + [self updatePlaceholderStyle]; +} + +- (void)setPlaceholder:(NSString *)placeholder { + _placeholder = placeholder; + 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 { + _placeholderColor = placeholderColor; + self.placeholderLabel.textColor = _placeholderColor; +} + +- (void)updatePlaceholderStyle { + self.placeholder = self.placeholder;// 触发文字样式的更新 +} + +- (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) { + textView = (QMUITextView *)object; + } + } else if ([sender isKindOfClass:[QMUITextView class]]) { + textView = (QMUITextView *)sender; + } + + 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:)]) { + + 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]; + } + } + + // 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 { + 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 + frame = CGRectSetHeight(frame, MIN(CGRectGetHeight(frame), self.maximumHeight)); + } + + // 重写了 UITextView 的 drawRect: 后,对于带小数点的 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 { + // 重写了 UITextView 的 drawRect: 后,对于带小数点的 frame 会导致文本框右边多出一条黑线,原因未明,暂时这样处理 + // https://github.com/Tencent/QMUI_iOS/issues/557 + bounds = CGRectFlatted(bounds); + [super setBounds:bounds]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + if (self.placeholder.length > 0) { + CGRect frame = [self preferredPlaceholderFrameWithSize:self.bounds.size]; + self.placeholderLabel.frame = frame; + } +} + +- (void)drawRect:(CGRect)rect { + [super drawRect:rect]; + [self updatePlaceholderLabelHidden]; +} + +- (NSUInteger)lengthWithString:(NSString *)string { + return self.shouldCountingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length; +} + +- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated { + if (!self.shouldRejectSystemScroll) { + [super setContentOffset:contentOffset animated:animated]; + if (self.debug) QMUILog(NSStringFromClass(self.class), @"%@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y); + } else { + if (self.debug) QMUILog(NSStringFromClass(self.class), @"被屏蔽的 %@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y); + } +} + +- (void)setContentOffset:(CGPoint)contentOffset { + if (!self.shouldRejectSystemScroll) { + [super setContentOffset:contentOffset]; + if (self.debug) QMUILog(NSStringFromClass(self.class), @"%@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y); + } else { + if (self.debug) QMUILog(NSStringFromClass(self.class), @"被屏蔽的 %@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y); + } +} + +- (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 { + 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 _QMUITextViewDelegator + +#pragma mark - + +- (BOOL)textView:(QMUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { + if (self.textView.debug) QMUILog(NSStringFromClass(self.class), @"textView.text(%@ | %@) = %@\nmarkedTextRange = %@\nrange = %@\ntext = %@", @(textView.text.length), @(textView.text.qmui_lengthWhenCountingNonASCIICharacterAsTwo), textView.text, textView.markedTextRange, NSStringFromRange(range), text); + + if ([text isEqualToString:@"\n"]) { + if ([textView.delegate respondsToSelector:@selector(textViewShouldReturn:)]) { + BOOL shouldReturn = [textView.delegate textViewShouldReturn:textView]; + if (shouldReturn) { + return NO; + } + } + } + + if (textView.maximumTextLength < NSUIntegerMax) { + + // 如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符) + // 注意当点击了候选词后触发的那一次 textView:shouldChangeTextInRange:replacementText:,此时的 marktedTextRange 依然存在,尚未被清除,所以这种情况下的字符长度限制逻辑会交给 handleTextChanged: 那边处理。 + if (textView.markedTextRange) { + 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; + BOOL textWillOutofMaximumTextLength = [textView lengthWithString:textView.text] - rangeLength + [textView lengthWithString:text] > textView.maximumTextLength; + if (textWillOutofMaximumTextLength) { + // 当输入的文本达到最大长度限制后,此时继续点击 return 按钮(相当于尝试插入“\n”),就会认为总文字长度已经超过最大长度限制,所以此次 return 按钮的点击被拦截,外界无法感知到有这个 return 事件发生,所以这里为这种情况做了特殊保护 + if ([textView lengthWithString:textView.text] - rangeLength == textView.maximumTextLength && [text isEqualToString:@"\n"]) { + return NO; + } + // 将要插入的文字裁剪成多长,就可以让它插入了 + NSInteger substringLength = textView.maximumTextLength - [textView lengthWithString:textView.text] + rangeLength; + + 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) { + 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; + }); + } + } + + if ([textView.delegate respondsToSelector:@selector(textView:didPreventTextChangeInRange:replacementText:)]) { + [textView.delegate textView:textView didPreventTextChangeInRange:range replacementText:text]; + } + return NO; + } + } + + 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 new file mode 100644 index 00000000..3aeb0b4e --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITips.h @@ -0,0 +1,112 @@ +/** + * 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. + */ + +// +// QMUITips.h +// qmui +// +// Created by QMUI Team on 15/12/25. +// + +#import +#import "QMUIToastView.h" + +// 自动计算秒数的标志符,在 delay 里面赋值 QMUITipsAutomaticallyHideToastSeconds 即可通过自动计算 tips 消失的秒数 +extern const NSInteger QMUITipsAutomaticallyHideToastSeconds; + +/// 默认的 parentView +#define DefaultTipsParentView (UIApplication.sharedApplication.delegate.window) + +/** + * 简单封装了 QMUIToastView,支持弹出纯文本、loading、succeed、error、info 等五种 tips。如果这些接口还满足不了业务的需求,可以通过 QMUITips 的分类自行添加接口。 + * 注意用类方法显示 tips 的话,会导致父类的 willShowBlock 无法正常工作,具体请查看 willShowBlock 的注释。 + * @warning 使用类方法,除了 showLoading 系列方法不会自动隐藏外,其他方法如果没有 delay 参数,则会自动隐藏 + * @see [QMUIToastView willShowBlock] + */ +@interface QMUITips : QMUIToastView + +NS_ASSUME_NONNULL_BEGIN + +/// 实例方法:需要自己addSubview,hide之后不会自动removeFromSuperView + +- (void)showLoading; +- (void)showLoading:(nullable NSString *)text; +- (void)showLoadingHideAfterDelay:(NSTimeInterval)delay; +- (void)showLoading:(nullable NSString *)text hideAfterDelay:(NSTimeInterval)delay; +- (void)showLoading:(nullable NSString *)text detailText:(nullable NSString *)detailText; +- (void)showLoading:(nullable NSString *)text detailText:(nullable NSString *)detailText hideAfterDelay:(NSTimeInterval)delay; + +- (void)showWithText:(nullable NSString *)text; +- (void)showWithText:(nullable NSString *)text hideAfterDelay:(NSTimeInterval)delay; +- (void)showWithText:(nullable NSString *)text detailText:(nullable NSString *)detailText; +- (void)showWithText:(nullable NSString *)text detailText:(nullable NSString *)detailText hideAfterDelay:(NSTimeInterval)delay; + +- (void)showSucceed:(nullable NSString *)text; +- (void)showSucceed:(nullable NSString *)text hideAfterDelay:(NSTimeInterval)delay; +- (void)showSucceed:(nullable NSString *)text detailText:(nullable NSString *)detailText; +- (void)showSucceed:(nullable NSString *)text detailText:(nullable NSString *)detailText hideAfterDelay:(NSTimeInterval)delay; + +- (void)showError:(nullable NSString *)text; +- (void)showError:(nullable NSString *)text hideAfterDelay:(NSTimeInterval)delay; +- (void)showError:(nullable NSString *)text detailText:(nullable NSString *)detailText; +- (void)showError:(nullable NSString *)text detailText:(nullable NSString *)detailText hideAfterDelay:(NSTimeInterval)delay; + +- (void)showInfo:(nullable NSString *)text; +- (void)showInfo:(nullable NSString *)text hideAfterDelay:(NSTimeInterval)delay; +- (void)showInfo:(nullable NSString *)text detailText:(nullable NSString *)detailText; +- (void)showInfo:(nullable NSString *)text detailText:(nullable NSString *)detailText hideAfterDelay:(NSTimeInterval)delay; + +/// 类方法:主要用在局部一次性使用的场景,hide之后会自动removeFromSuperView + ++ (QMUITips *)createTipsToView:(UIView *)view; + ++ (QMUITips *)showLoadingInView:(UIView *)view; ++ (QMUITips *)showLoading:(nullable NSString *)text inView:(UIView *)view; ++ (QMUITips *)showLoadingInView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; ++ (QMUITips *)showLoading:(nullable NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; ++ (QMUITips *)showLoading:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view; ++ (QMUITips *)showLoading:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; + ++ (QMUITips *)showWithText:(nullable NSString *)text; ++ (QMUITips *)showWithText:(nullable NSString *)text detailText:(nullable NSString *)detailText; ++ (QMUITips *)showWithText:(nullable NSString *)text inView:(UIView *)view; ++ (QMUITips *)showWithText:(nullable NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; ++ (QMUITips *)showWithText:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view; ++ (QMUITips *)showWithText:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; + ++ (QMUITips *)showSucceed:(nullable NSString *)text; ++ (QMUITips *)showSucceed:(nullable NSString *)text detailText:(nullable NSString *)detailText; ++ (QMUITips *)showSucceed:(nullable NSString *)text inView:(UIView *)view; ++ (QMUITips *)showSucceed:(nullable NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; ++ (QMUITips *)showSucceed:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view; ++ (QMUITips *)showSucceed:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; + ++ (QMUITips *)showError:(nullable NSString *)text; ++ (QMUITips *)showError:(nullable NSString *)text detailText:(nullable NSString *)detailText; ++ (QMUITips *)showError:(nullable NSString *)text inView:(UIView *)view; ++ (QMUITips *)showError:(nullable NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; ++ (QMUITips *)showError:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view; ++ (QMUITips *)showError:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; + ++ (QMUITips *)showInfo:(nullable NSString *)text; ++ (QMUITips *)showInfo:(nullable NSString *)text detailText:(nullable NSString *)detailText; ++ (QMUITips *)showInfo:(nullable NSString *)text inView:(UIView *)view; ++ (QMUITips *)showInfo:(nullable NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; ++ (QMUITips *)showInfo:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view; ++ (QMUITips *)showInfo:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; + +/// 隐藏 tips ++ (void)hideAllTipsInView:(UIView *)view; ++ (void)hideAllTips; + +/// 自动隐藏 toast 可以使用这个方法自动计算秒数 ++ (NSTimeInterval)smartDelaySecondsForTipsText:(NSString *)text; + +NS_ASSUME_NONNULL_END + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUITips.m b/QMUI/QMUIKit/QMUIComponents/QMUITips.m new file mode 100644 index 00000000..45aa0099 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUITips.m @@ -0,0 +1,320 @@ +/** + * 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. + */ + +// +// QMUITips.m +// qmui +// +// Created by QMUI Team on 15/12/25. +// + +#import "QMUITips.h" +#import "QMUICore.h" +#import "QMUIToastContentView.h" +#import "QMUIToastBackgroundView.h" +#import "NSString+QMUI.h" + +const NSInteger QMUITipsAutomaticallyHideToastSeconds = -1; + +@interface QMUITips () + +@property(nonatomic, strong) UIView *contentCustomView; + +@end + +@implementation QMUITips + +- (void)showLoading { + [self showLoading:nil hideAfterDelay:0]; +} + +- (void)showLoadingHideAfterDelay:(NSTimeInterval)delay { + [self showLoading:nil hideAfterDelay:delay]; +} + +- (void)showLoading:(NSString *)text { + [self showLoading:text hideAfterDelay:0]; +} + +- (void)showLoading:(NSString *)text hideAfterDelay:(NSTimeInterval)delay { + [self showLoading:text detailText:nil hideAfterDelay:delay]; +} + +- (void)showLoading:(NSString *)text detailText:(NSString *)detailText { + [self showLoading:text detailText:detailText hideAfterDelay:0]; +} + +- (void)showLoading:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { + UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + [indicator startAnimating]; + self.contentCustomView = indicator; + [self showTipWithText:text detailText:detailText hideAfterDelay:delay]; +} + +- (void)showWithText:(NSString *)text { + [self showWithText:text detailText:nil hideAfterDelay:0]; +} + +- (void)showWithText:(NSString *)text hideAfterDelay:(NSTimeInterval)delay { + [self showWithText:text detailText:nil hideAfterDelay:delay]; +} + +- (void)showWithText:(NSString *)text detailText:(NSString *)detailText { + [self showWithText:text detailText:detailText hideAfterDelay:0]; +} + +- (void)showWithText:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { + self.contentCustomView = nil; + [self showTipWithText:text detailText:detailText hideAfterDelay:delay]; +} + +- (void)showSucceed:(NSString *)text { + [self showSucceed:text detailText:nil hideAfterDelay:0]; +} + +- (void)showSucceed:(NSString *)text hideAfterDelay:(NSTimeInterval)delay { + [self showSucceed:text detailText:nil hideAfterDelay:delay]; +} + +- (void)showSucceed:(NSString *)text detailText:(NSString *)detailText { + [self showSucceed:text detailText:detailText hideAfterDelay:0]; +} + +- (void)showSucceed:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { + self.contentCustomView = [[UIImageView alloc] initWithImage:[[QMUIHelper imageWithName:@"QMUI_tips_done"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]]; + [self showTipWithText:text detailText:detailText hideAfterDelay:delay]; +} + +- (void)showError:(NSString *)text { + [self showError:text detailText:nil hideAfterDelay:0]; +} + +- (void)showError:(NSString *)text hideAfterDelay:(NSTimeInterval)delay { + [self showError:text detailText:nil hideAfterDelay:delay]; +} + +- (void)showError:(NSString *)text detailText:(NSString *)detailText { + [self showError:text detailText:detailText hideAfterDelay:0]; +} + +- (void)showError:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { + self.contentCustomView = [[UIImageView alloc] initWithImage:[[QMUIHelper imageWithName:@"QMUI_tips_error"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]]; + [self showTipWithText:text detailText:detailText hideAfterDelay:delay]; +} + +- (void)showInfo:(NSString *)text { + [self showInfo:text detailText:nil hideAfterDelay:0]; +} + +- (void)showInfo:(NSString *)text hideAfterDelay:(NSTimeInterval)delay { + [self showInfo:text detailText:nil hideAfterDelay:delay]; +} + +- (void)showInfo:(NSString *)text detailText:(NSString *)detailText { + [self showInfo:text detailText:detailText hideAfterDelay:0]; +} + +- (void)showInfo:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { + self.contentCustomView = [[UIImageView alloc] initWithImage:[[QMUIHelper imageWithName:@"QMUI_tips_info"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]]; + [self showTipWithText:text detailText:detailText hideAfterDelay:delay]; +} + +- (void)showTipWithText:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { + + QMUIToastContentView *contentView = (QMUIToastContentView *)self.contentView; + contentView.customView = self.contentCustomView; + + contentView.textLabelText = text ?: @""; + contentView.detailTextLabelText = detailText ?: @""; + + [self showAnimated:YES]; + + if (delay == QMUITipsAutomaticallyHideToastSeconds) { + [self hideAnimated:YES afterDelay:[QMUITips smartDelaySecondsForTipsText:text]]; + } 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 { + NSUInteger length = text.qmui_lengthWhenCountingNonASCIICharacterAsTwo; + if (length <= 20) { + return 1.5; + } else if (length <= 40) { + return 2.0; + } else if (length <= 50) { + return 2.5; + } else { + return 3.0; + } +} + ++ (QMUITips *)showLoadingInView:(UIView *)view { + return [self showLoading:nil detailText:nil inView:view hideAfterDelay:0]; +} + ++ (QMUITips *)showLoading:(NSString *)text inView:(UIView *)view { + return [self showLoading:text detailText:nil inView:view hideAfterDelay:0]; +} + ++ (QMUITips *)showLoadingInView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { + return [self showLoading:nil detailText:nil inView:view hideAfterDelay:delay]; +} + ++ (QMUITips *)showLoading:(NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { + return [self showLoading:text detailText:nil inView:view hideAfterDelay:delay]; +} + ++ (QMUITips *)showLoading:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view { + return [self showLoading:text detailText:detailText inView:view hideAfterDelay:0]; +} + ++ (QMUITips *)showLoading:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { + QMUITips *tips = [self createTipsToView:view]; + [tips showLoading:text detailText:detailText hideAfterDelay:delay]; + return tips; +} + ++ (QMUITips *)showWithText:(nullable NSString *)text { + return [self showWithText:text detailText:nil inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showWithText:(nullable NSString *)text detailText:(nullable NSString *)detailText { + return [self showWithText:text detailText:detailText inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showWithText:(NSString *)text inView:(UIView *)view { + return [self showWithText:text detailText:nil inView:view hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showWithText:(NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { + return [self showWithText:text detailText:nil inView:view hideAfterDelay:delay]; +} + ++ (QMUITips *)showWithText:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view { + return [self showWithText:text detailText:detailText inView:view hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showWithText:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { + QMUITips *tips = [self createTipsToView:view]; + [tips showWithText:text detailText:detailText hideAfterDelay:delay]; + return tips; +} + ++ (QMUITips *)showSucceed:(nullable NSString *)text { + return [self showSucceed:text detailText:nil inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showSucceed:(nullable NSString *)text detailText:(nullable NSString *)detailText { + return [self showSucceed:text detailText:detailText inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showSucceed:(NSString *)text inView:(UIView *)view { + return [self showSucceed:text detailText:nil inView:view hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showSucceed:(NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { + return [self showSucceed:text detailText:nil inView:view hideAfterDelay:delay]; +} + ++ (QMUITips *)showSucceed:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view { + return [self showSucceed:text detailText:detailText inView:view hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showSucceed:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { + QMUITips *tips = [self createTipsToView:view]; + [tips showSucceed:text detailText:detailText hideAfterDelay:delay]; + return tips; +} + ++ (QMUITips *)showError:(nullable NSString *)text { + return [self showError:text detailText:nil inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showError:(nullable NSString *)text detailText:(nullable NSString *)detailText { + return [self showError:text detailText:detailText inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showError:(NSString *)text inView:(UIView *)view { + return [self showError:text detailText:nil inView:view hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showError:(NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { + return [self showError:text detailText:nil inView:view hideAfterDelay:delay]; +} + ++ (QMUITips *)showError:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view { + return [self showError:text detailText:detailText inView:view hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showError:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { + QMUITips *tips = [self createTipsToView:view]; + [tips showError:text detailText:detailText hideAfterDelay:delay]; + return tips; +} + ++ (QMUITips *)showInfo:(nullable NSString *)text { + return [self showInfo:text detailText:nil inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showInfo:(nullable NSString *)text detailText:(nullable NSString *)detailText { + return [self showInfo:text detailText:detailText inView:DefaultTipsParentView hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showInfo:(NSString *)text inView:(UIView *)view { + return [self showInfo:text detailText:nil inView:view hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showInfo:(NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { + return [self showInfo:text detailText:nil inView:view hideAfterDelay:delay]; +} + ++ (QMUITips *)showInfo:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view { + return [self showInfo:text detailText:detailText inView:view hideAfterDelay:QMUITipsAutomaticallyHideToastSeconds]; +} + ++ (QMUITips *)showInfo:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { + QMUITips *tips = [self createTipsToView:view]; + [tips showInfo:text detailText:detailText hideAfterDelay:delay]; + return tips; +} + ++ (QMUITips *)createTipsToView:(UIView *)view { + QMUITips *tips = [[QMUITips alloc] initWithView:view]; + [view addSubview:tips]; + tips.removeFromSuperViewWhenHide = YES; + return tips; +} + ++ (void)hideAllTipsInView:(UIView *)view { + [self hideAllToastInView:view animated:NO]; +} + ++ (void)hideAllTips { + [self hideAllToastInView:nil animated:NO]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.h b/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.h new file mode 100644 index 00000000..c478400c --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.h @@ -0,0 +1,37 @@ +/** + * 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. + */ + +// +// QMUIWeakObjectContainer.h +// QMUIKit +// +// Created by QMUI Team on 2018/7/24. +// + +#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 : NSProxy + +/// 将一个 object 包装到一个 QMUIWeakObjectContainer 里 +- (instancetype)initWithObject:(id)object; +- (instancetype)init; ++ (instancetype)containerWithObject:(id)object; + +/// 获取原始对象 object,如果 object 已被释放则该属性返回 nil +@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 new file mode 100644 index 00000000..001a313e --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIWeakObjectContainer.m @@ -0,0 +1,99 @@ +/** + * 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. + */ + +// +// QMUIWeakObjectContainer.m +// QMUIKit +// +// Created by QMUI Team on 2018/7/24. +// + +#import "QMUIWeakObjectContainer.h" + +// from https://github.com/ibireme/YYKit/blob/master/YYKit/Utility/YYWeakProxy.m + +@implementation QMUIWeakObjectContainer + +- (instancetype)initWithObject:(id)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 new file mode 100644 index 00000000..4265a1d0 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.h @@ -0,0 +1,176 @@ +/** + * 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. + */ + +// +// QMUIZoomImageView.h +// qmui +// +// Created by QMUI Team on 14-9-14. +// + +#import +#import +#import "QMUIAsset.h" + +@class QMUIZoomImageView; +@class QMUIEmptyView; +@class QMUIButton; +@class QMUIZoomImageViewVideoToolbar; +@class QMUIPieProgressView; + +@protocol QMUIZoomImageViewDelegate +@optional +- (void)singleTouchInZoomingImageView:(QMUIZoomImageView *)zoomImageView location:(CGPoint)location; +- (void)doubleTouchInZoomingImageView:(QMUIZoomImageView *)zoomImageView location:(CGPoint)location; +- (void)longPressInZoomingImageView:(QMUIZoomImageView *)zoomImageView; + +/** + * 告知 delegate 用户点击了 iCloud 图片的重试按钮 + */ +- (void)didTouchICloudRetryButtonInZoomImageView:(QMUIZoomImageView *)imageView; + +/** + * 告知 delegate 在视频预览界面里,由于用户点击了空白区域或播放视频等导致了底部的视频工具栏被显示或隐藏 + * @param didHide 如果为 YES 则表示工具栏被隐藏,NO 表示工具栏被显示了出来 + */ +- (void)zoomImageView:(QMUIZoomImageView *)imageView didHideVideoToolbar:(BOOL)didHide; + +/// 是否支持缩放,默认为 YES +- (BOOL)enabledZoomViewInZoomImageView:(QMUIZoomImageView *)zoomImageView; + +@end + +/** + * 支持缩放查看静态图片、live photo、视频的控件 + * 默认显示完整图片或视频,可双击查看原始大小,再次双击查看放大后的大小,第三次双击恢复到初始大小。 + * + * 支持通过修改 contentMode 来控制静态图片和 live photo 默认的显示模式,目前仅支持 UIViewContentModeCenter、UIViewContentModeScaleAspectFill、UIViewContentModeScaleAspectFit,默认为 UIViewContentModeCenter。注意这里的显示模式是基于 viewportRect 而言的而非整个 zoomImageView + * @see viewportRect + * + * QMUIZoomImageView 提供最基础的图片预览和缩放功能以及 loading、错误等状态的展示支持,其他功能请通过继承来实现。 + */ +@interface QMUIZoomImageView : UIView + +@property(nonatomic, weak) id delegate; + +@property(nonatomic, strong, readonly) UIScrollView *scrollView; + +/** + * 比如常见的上传头像预览界面中间有一个用于裁剪的方框,则 viewportRect 必须被设置为这个方框在 zoomImageView 坐标系内的 frame,否则拖拽图片或视频时无法正确限制它们的显示范围 + * @note 图片或视频的初始位置会位于 viewportRect 正中间 + * @note 如果想要图片覆盖整个 viewportRect,将 contentMode 设置为 UIViewContentModeScaleAspectFill 即可 + * 如果设置为 CGRectZero 则表示使用默认值,默认值为和整个 zoomImageView 一样大 + */ +@property(nonatomic, assign) CGRect viewportRect; + +@property(nonatomic, assign) CGFloat maximumZoomScale; + +@property(nonatomic, copy) NSObject *reusedIdentifier; + +/// 设置当前要显示的图片,会把 livePhoto/video 相关内容清空,因此注意不要直接通过 imageView.image 来设置图片。 +@property(nonatomic, weak) UIImage *image; + +/// 用于显示图片的 UIImageView,注意不要通过 imageView.image 来设置图片,请使用 image 属性。 +@property(nonatomic, strong, readonly) UIImageView *imageView; + +/// 设置当前要显示的 Live Photo,会把 image/video 相关内容清空,因此注意不要直接通过 livePhotoView.livePhoto 来设置 +@property(nonatomic, weak) PHLivePhoto *livePhoto; + +/// 用于显示 Live Photo 的 view,仅在 iOS 9.1 及以后才有效 +@property(nonatomic, strong, readonly) PHLivePhotoView *livePhotoView; + +/// 设置当前要显示的 video ,会把 image/livePhoto 相关内容清空,因此注意不要直接通过 videoPlayerLayer 来设置 +@property(nonatomic, weak) AVPlayerItem *videoPlayerItem; + +/// 用于显示 video 的 layer +@property(nonatomic, weak, readonly) AVPlayerLayer *videoPlayerLayer; + +// 播放 video 时底部的工具栏,你可通过此属性来拿到并修改上面的播放/暂停按钮、进度条、Label 等的样式 +// @see QMUIZoomImageViewVideoToolbar +@property(nonatomic, strong, readonly) QMUIZoomImageViewVideoToolbar *videoToolbar; + +// 视频底部控制条的 margins,会在此基础上自动叠加 QMUIZoomImageView.safeAreaInsets,因此无需考虑在 iPhone X 下的兼容 +// 默认值为 {0, 25, 25, 18} +@property(nonatomic, assign) UIEdgeInsets videoToolbarMargins UI_APPEARANCE_SELECTOR; + +// 播放 video 时屏幕中央的播放按钮 +@property(nonatomic, strong, readonly) QMUIButton *videoCenteredPlayButton; + +// 可通过此属性修改 video 播放时屏幕中央的播放按钮图片 +@property(nonatomic, strong) UIImage *videoCenteredPlayButtonImage UI_APPEARANCE_SELECTOR; + +// 从 iCloud 加载资源的进度展示 +@property(nonatomic, strong) QMUIPieProgressView *cloudProgressView; + +// 从 iCloud 加载资源失败的重试按钮 +@property(nonatomic, strong) QMUIButton *cloudDownloadRetryButton; + +// 当前展示的资源的下载状态 +@property(nonatomic, assign) QMUIAssetDownloadStatus cloudDownloadStatus; + +/// 暂停视频播放 +- (void)pauseVideo; +/// 停止视频播放,将播放状态重置到初始状态 +- (void)endPlayingVideo; + +/// 获取当前正在显示的图片/视频的容器 +@property(nonatomic, weak, readonly) __kindof UIView *contentView; + +/** + * 获取当前正在显示的图片/视频在整个 QMUIZoomImageView 坐标系里的 rect(会按照当前的缩放状态来计算) + */ +- (CGRect)contentViewRectInZoomImageView; + +/** + * 重置图片或视频的大小,使用的场景例如:相册控件里放大当前图片、划到下一张、再回来,当前的图片或视频应该恢复到原来大小。 + * 注意子类重写需要调一下super。 + */ +- (void)revertZooming; + +@property(nonatomic, strong, readonly) QMUIEmptyView *emptyView; + +/** + * 显示一个 loading + * @info 注意 cell 复用可能导致当前页面显示一张错误的旧图片/视频,所以一般情况下需要视情况同时将 image/livePhoto/videoPlayerItem 等属性置为 nil 以清除图片/视频的显示 + */ +- (void)showLoading; + +/** + * 显示一句提示语 + * @info 注意 cell 复用可能导致当前页面显示一张错误的旧图片/视频,所以一般情况下需要视情况同时将 image/livePhoto/videoPlayerItem 等属性置为 nil 以清除图片/视频的显示 + */ +- (void)showEmptyViewWithText:(NSString *)text; +- (void)showEmptyViewWithText:(NSString *)text + detailText:(NSString *)detailText + buttonTitle:(NSString *)buttonTitle + buttonTarget:(id)buttonTarget + buttonAction:(SEL)action; + +/** + * 将 emptyView 隐藏 + */ +- (void)hideEmptyView; + +@end + +@interface QMUIZoomImageViewVideoToolbar : UIView + +@property(nonatomic, strong, readonly) QMUIButton *playButton; +@property(nonatomic, strong, readonly) QMUIButton *pauseButton; +@property(nonatomic, strong, readonly) UISlider *slider; +@property(nonatomic, strong, readonly) UILabel *sliderLeftLabel; +@property(nonatomic, strong, readonly) UILabel *sliderRightLabel; + +// 可通过调整此属性来调整 toolbar 内部的间距,默认为 {0, 0, 0, 0} +@property(nonatomic, assign) UIEdgeInsets paddings UI_APPEARANCE_SELECTOR; + +// 可通过这些属性修改 video 播放时屏幕底部工具栏的播放/暂停图标 +@property(nonatomic, strong) UIImage *playButtonImage UI_APPEARANCE_SELECTOR; +@property(nonatomic, strong) UIImage *pauseButtonImage UI_APPEARANCE_SELECTOR; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.m b/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.m new file mode 100644 index 00000000..9059d605 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/QMUIZoomImageView.m @@ -0,0 +1,1138 @@ +/** + * 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. + */ + +// +// QMUIZoomImageView.m +// qmui +// +// Created by QMUI Team on 14-9-14. +// + +#import "QMUIZoomImageView.h" +#import "QMUICore.h" +#import "QMUIEmptyView.h" +#import "UIView+QMUI.h" +#import "UIImage+QMUI.h" +#import "UIColor+QMUI.h" +#import "UIScrollView+QMUI.h" +#import "QMUIButton.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) + +// generate icon images needed by QMUIZoomImageView +// 用于生成 QMUIZoomImageView 所需的一些简单的图标图片 +@interface QMUIZoomImageViewImageGenerator : NSObject + ++ (UIImage *)largePlayImage; ++ (UIImage *)smallPlayImage; ++ (UIImage *)pauseImage; + +@end + +@interface QMUIZoomImageVideoPlayerView : UIView + +@end + +static NSUInteger const kTagForCenteredPlayButton = 1; + +@interface QMUIZoomImageView () + +// video play +@property(nonatomic, strong) QMUIZoomImageVideoPlayerView *videoPlayerView; +@property(nonatomic, strong) AVPlayer *videoPlayer; +@property(nonatomic, strong) id videoTimeObserver; +@property(nonatomic, assign) BOOL isSeekingVideo; +@property(nonatomic, assign) CGSize videoSize; + +@end + +@implementation QMUIZoomImageView + +@synthesize imageView = _imageView; +@synthesize livePhotoView = _livePhotoView; +@synthesize videoPlayerLayer = _videoPlayerLayer; +@synthesize videoToolbar = _videoToolbar; +@synthesize videoCenteredPlayButton = _videoCenteredPlayButton; +@synthesize cloudProgressView = _cloudProgressView; +@synthesize cloudDownloadRetryButton = _cloudDownloadRetryButton; + +- (void)didMoveToWindow { + [super didMoveToWindow]; + // 当 self.window 为 nil 时说明此 view 被移出了可视区域(比如所在的 controller 被 pop 了),此时应该停止视频播放 + if (!self.window) { + [self endPlayingVideo]; + } +} + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + + self.maximumZoomScale = 2.0; + + _scrollView = [[UIScrollView alloc] qmui_initWithSize:frame.size]; + self.scrollView.showsHorizontalScrollIndicator = NO; + self.scrollView.showsVerticalScrollIndicator = NO; + self.scrollView.minimumZoomScale = 0; + self.scrollView.maximumZoomScale = self.maximumZoomScale; + self.scrollView.delegate = self; + self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + [self addSubview:self.scrollView]; + + _emptyView = [[QMUIEmptyView alloc] init]; + ((UIActivityIndicatorView *)self.emptyView.loadingView).color = UIColorWhite; + self.emptyView.hidden = YES; + [self addSubview:self.emptyView]; + + UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTapGestureWithPoint:)]; + singleTapGesture.delegate = self; + singleTapGesture.numberOfTapsRequired = 1; + singleTapGesture.numberOfTouchesRequired = 1; + [self addGestureRecognizer:singleTapGesture]; + + UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTapGestureWithPoint:)]; + doubleTapGesture.numberOfTapsRequired = 2; + doubleTapGesture.numberOfTouchesRequired = 1; + [self addGestureRecognizer:doubleTapGesture]; + + UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; + [self addGestureRecognizer:longPressGesture]; + + // 双击失败后才出发单击 + [singleTapGesture requireGestureRecognizerToFail:doubleTapGesture]; + + self.contentMode = UIViewContentModeCenter; + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + if (CGRectIsEmpty(self.bounds)) { + return; + } + + self.scrollView.frame = self.bounds; + self.emptyView.frame = self.bounds; + + CGRect viewportRect = [self finalViewportRect]; + + if (_videoCenteredPlayButton) { + [_videoCenteredPlayButton sizeToFit]; + _videoCenteredPlayButton.center = CGPointGetCenterWithRect(viewportRect); + } + + if (_videoToolbar) { + _videoToolbar.frame = ({ + 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); + }); + } + + if (_cloudProgressView && _cloudDownloadRetryButton) { + CGPoint origin = CGPointMake(12, 12); + _cloudDownloadRetryButton.frame = CGRectSetXY(_cloudDownloadRetryButton.frame, origin.x, NavigationContentTopConstant + origin.y); + _cloudProgressView.frame = CGRectSetSize(_cloudProgressView.frame, _cloudDownloadRetryButton.currentImage.size); + _cloudProgressView.center = _cloudDownloadRetryButton.center; + } +} + +- (void)setFrame:(CGRect)frame { + BOOL isBoundsChanged = !CGSizeEqualToSize(frame.size, self.frame.size); + [super setFrame:frame]; + if (isBoundsChanged) { + [self revertZooming]; + } +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Normal Image + +- (UIImageView *)imageView { + [self initImageViewIfNeeded]; + return _imageView; +} + +- (void)initImageViewIfNeeded { + if (_imageView) { + return; + } + _imageView = [[UIImageView alloc] init]; + [self.scrollView addSubview:_imageView]; +} + +- (void)setImage:(UIImage *)image { + _image = image; + + if (image) { + self.livePhoto = nil; + self.videoPlayerItem = nil; + } + + if (!image) { + _imageView.image = nil; + [_imageView removeFromSuperview]; + _imageView = nil; + return; + } + self.imageView.image = image; + + // 更新 imageView 的大小时,imageView 可能已经被缩放过,所以要应用当前的缩放 + self.imageView.qmui_frameApplyTransform = CGRectMakeWithSize(image.size); + + [self hideViews]; + self.imageView.hidden = NO; + + [self revertZooming]; +} + +#pragma mark - Live Photo + +- (PHLivePhotoView *)livePhotoView { + [self initLivePhotoViewIfNeeded]; + return _livePhotoView; +} + +- (void)setLivePhoto:(PHLivePhoto *)livePhoto { + _livePhoto = livePhoto; + + if (livePhoto) { + self.image = nil; + self.videoPlayerItem = nil; + } + + if (!livePhoto) { + _livePhotoView.livePhoto = nil; + [_livePhotoView removeFromSuperview]; + _livePhotoView = nil; + return; + } + + [self initLivePhotoViewIfNeeded]; + _livePhotoView.livePhoto = livePhoto; + _livePhotoView.hidden = NO; + + // 更新 livePhotoView 的大小时,livePhotoView 可能已经被缩放过,所以要应用当前的缩放 + _livePhotoView.qmui_frameApplyTransform = CGRectMakeWithSize(livePhoto.size); + + [self revertZooming]; +} + +- (void)initLivePhotoViewIfNeeded { + if (_livePhotoView) { + return; + } + _livePhotoView = [[PHLivePhotoView alloc] init]; + [self.scrollView addSubview:_livePhotoView]; +} + +#pragma mark - Image Scale + +- (void)setContentMode:(UIViewContentMode)contentMode { + BOOL isContentModeChanged = self.contentMode != contentMode; + [super setContentMode:contentMode]; + if (isContentModeChanged) { + [self revertZooming]; + } +} + +- (void)setMaximumZoomScale:(CGFloat)maximumZoomScale { + _maximumZoomScale = maximumZoomScale; + self.scrollView.maximumZoomScale = maximumZoomScale; +} + +- (CGFloat)minimumZoomScale { + BOOL isLivePhoto = !!self.livePhoto; + + if (!self.image && !isLivePhoto && !self.videoPlayerItem) { + return 1; + } + + CGRect viewport = [self finalViewportRect]; + CGSize mediaSize = CGSizeZero; + if (self.image) { + mediaSize = self.image.size; + } else if (isLivePhoto) { + mediaSize = self.livePhoto.size; + } else if (self.videoPlayerItem) { + mediaSize = self.videoSize; + } + + CGFloat minScale = 1; + CGFloat scaleX = CGRectGetWidth(viewport) / mediaSize.width; + CGFloat scaleY = CGRectGetHeight(viewport) / mediaSize.height; + if (self.contentMode == UIViewContentModeScaleAspectFit) { + minScale = MIN(scaleX, scaleY); + } else if (self.contentMode == UIViewContentModeScaleAspectFill) { + minScale = MAX(scaleX, scaleY); + } else if (self.contentMode == UIViewContentModeCenter) { + if (scaleX >= 1 && scaleY >= 1) { + minScale = 1; + } else { + minScale = MIN(scaleX, scaleY); + } + } + return minScale; +} + +- (void)revertZooming { + if (CGRectIsEmpty(self.bounds)) { + return; + } + + BOOL enabledZoomImageView = [self enabledZoomImageView]; + CGFloat minimumZoomScale = [self minimumZoomScale]; + CGFloat maximumZoomScale = enabledZoomImageView ? self.maximumZoomScale : minimumZoomScale; + maximumZoomScale = MAX(minimumZoomScale, maximumZoomScale);// 可能外部通过 contentMode = UIViewContentModeScaleAspectFit 的方式来让小图片撑满当前的 zoomImageView,所以算出来 minimumZoomScale 会很大(至少比 maximumZoomScale 大),所以这里要做一个保护 + CGFloat zoomScale = minimumZoomScale; + BOOL shouldFireDidZoomingManual = zoomScale == self.scrollView.zoomScale; + self.scrollView.panGestureRecognizer.enabled = enabledZoomImageView; + self.scrollView.pinchGestureRecognizer.enabled = enabledZoomImageView; + self.scrollView.minimumZoomScale = minimumZoomScale; + self.scrollView.maximumZoomScale = maximumZoomScale; + self.contentView.frame = CGRectSetXY(self.contentView.frame, 0, 0);// 重置 origin 的目的是:https://github.com/Tencent/QMUI_iOS/issues/400 + [self setZoomScale:zoomScale animated:NO]; + + // 只有前后的 zoomScale 不相等,才会触发 UIScrollViewDelegate scrollViewDidZoom:,因此对于相等的情况要自己手动触发 + if (shouldFireDidZoomingManual) { + [self handleDidEndZooming]; + } + + // 当内容比 viewport 的区域更大时,要把内容放在 viewport 正中间 + self.scrollView.contentOffset = ({ + CGFloat x = self.scrollView.contentOffset.x; + CGFloat y = self.scrollView.contentOffset.y; + CGRect viewport = [self finalViewportRect]; + if (!CGRectIsEmpty(viewport)) { + UIView *contentView = [self contentView]; + if (CGRectGetWidth(viewport) < CGRectGetWidth(contentView.frame)) { + x = (CGRectGetWidth(contentView.frame) / 2 - CGRectGetWidth(viewport) / 2) - CGRectGetMinX(viewport); + } + if (CGRectGetHeight(viewport) < CGRectGetHeight(contentView.frame)) { + y = (CGRectGetHeight(contentView.frame) / 2 - CGRectGetHeight(viewport) / 2) - CGRectGetMinY(viewport); + } + } + CGPointMake(x, y); + }); +} + +- (void)setZoomScale:(CGFloat)zoomScale animated:(BOOL)animated { + if (animated) { + [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + self.scrollView.zoomScale = zoomScale; + } completion:nil]; + } else { + self.scrollView.zoomScale = zoomScale; + } +} + +- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated { + if (animated) { + [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + [self.scrollView zoomToRect:rect animated:NO]; + } completion:nil]; + } else { + [self.scrollView zoomToRect:rect animated:NO]; + } +} + +- (CGRect)contentViewRectInZoomImageView { + UIView *contentView = [self contentView]; + if (!contentView) { + return CGRectZero; + } + return [self convertRect:contentView.frame fromView:contentView.superview]; +} + +- (void)handleDidEndZooming { + CGRect viewport = [self finalViewportRect]; + + UIView *contentView = [self contentView]; + // 强制 layout 以确保下面的一堆计算依赖的都是最新的 frame 的值 + [self layoutIfNeeded]; + CGRect contentViewFrame = contentView ? [self convertRect:contentView.frame fromView:contentView.superview] : CGRectZero; + UIEdgeInsets contentInset = UIEdgeInsetsZero; + + contentInset.top = CGRectGetMinY(viewport); + contentInset.left = CGRectGetMinX(viewport); + contentInset.right = CGRectGetWidth(self.bounds) - CGRectGetMaxX(viewport); + contentInset.bottom = CGRectGetHeight(self.bounds) - CGRectGetMaxY(viewport); + + // 图片 height 比选图框(viewport)的 height 小,这时应该把图片纵向摆放在选图框中间,且不允许上下移动 + if (CGRectGetHeight(viewport) > CGRectGetHeight(contentViewFrame)) { + // 用 floor 而不是 flat,是因为 flat 本质上是向上取整,会导致 top + bottom 比实际的大,然后 scrollView 就认为可滚动了 + contentInset.top = floor(CGRectGetMidY(viewport) - CGRectGetHeight(contentViewFrame) / 2.0); + contentInset.bottom = floor(CGRectGetHeight(self.bounds) - CGRectGetMidY(viewport) - CGRectGetHeight(contentViewFrame) / 2.0); + } + + // 图片 width 比选图框的 width 小,这时应该把图片横向摆放在选图框中间,且不允许左右移动 + if (CGRectGetWidth(viewport) > CGRectGetWidth(contentViewFrame)) { + contentInset.left = floor(CGRectGetMidX(viewport) - CGRectGetWidth(contentViewFrame) / 2.0); + contentInset.right = floor(CGRectGetWidth(self.bounds) - CGRectGetMidX(viewport) - CGRectGetWidth(contentViewFrame) / 2.0); + } + + self.scrollView.contentInset = contentInset; + self.scrollView.contentSize = contentView.frame.size; +} + +- (BOOL)enabledZoomImageView { + BOOL enabledZoom = YES; + BOOL isLivePhoto = !!self.livePhoto; + if ([self.delegate respondsToSelector:@selector(enabledZoomViewInZoomImageView:)]) { + enabledZoom = [self.delegate enabledZoomViewInZoomImageView:self]; + } else if (!self.image && !isLivePhoto && !self.videoPlayerItem) { + enabledZoom = NO; + } + return enabledZoom; +} + +#pragma mark - Video + +- (void)setVideoPlayerItem:(AVPlayerItem *)videoPlayerItem { + _videoPlayerItem = videoPlayerItem; + + if (videoPlayerItem) { + self.livePhoto = nil; + self.image = nil; + [self hideViews]; + } + + // 移除旧的 videoPlayer 时,同时移除相应的 timeObserver + if (self.videoPlayer) { + [self removePlayerTimeObserver]; + } + + if (!videoPlayerItem) { + [self destroyVideoRelatedObjectsIfNeeded]; + return; + } + + // 获取视频尺寸 + NSArray *tracksArray = videoPlayerItem.asset.tracks; + self.videoSize = CGSizeZero; + for (AVAssetTrack *track in tracksArray) { + if ([track.mediaType isEqualToString:AVMediaTypeVideo]) { + CGSize size = CGSizeApplyAffineTransform(track.naturalSize, track.preferredTransform); + self.videoSize = CGSizeMake(fabs(size.width), fabs(size.height)); + break; + } + } + + self.videoPlayer = [AVPlayer playerWithPlayerItem:videoPlayerItem]; + [self initVideoRelatedViewsIfNeeded]; + _videoPlayerLayer.player = self.videoPlayer; + // 更新 videoPlayerView 的大小时,videoView 可能已经被缩放过,所以要应用当前的缩放 + self.videoPlayerView.qmui_frameApplyTransform = CGRectMakeWithSize(self.videoSize); + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleVideoPlayToEndEvent) name:AVPlayerItemDidPlayToEndTimeNotification object:videoPlayerItem]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; + + [self configVideoProgressSlider]; + + self.videoPlayerLayer.hidden = NO; + self.videoCenteredPlayButton.hidden = NO; + self.videoToolbar.playButton.hidden = NO; + + [self revertZooming]; +} + +- (void)handlePlayButton:(UIButton *)button { + [self addPlayerTimeObserver]; + [self.videoPlayer play]; + self.videoCenteredPlayButton.hidden = YES; + self.videoToolbar.playButton.hidden = YES; + self.videoToolbar.pauseButton.hidden = NO; + if (button.tag == kTagForCenteredPlayButton) { + self.videoToolbar.hidden = YES; + if ([self.delegate respondsToSelector:@selector(zoomImageView:didHideVideoToolbar:)]) { + [self.delegate zoomImageView:self didHideVideoToolbar:YES]; + } + } +} +- (void)handlePauseButton { + [self.videoPlayer pause]; + self.videoToolbar.playButton.hidden = NO; + self.videoToolbar.pauseButton.hidden = YES; +} + +- (void)handleVideoPlayToEndEvent { + [self.videoPlayer seekToTime:CMTimeMake(0, 1)]; + self.videoCenteredPlayButton.hidden = NO; + self.videoToolbar.playButton.hidden = NO; + self.videoToolbar.pauseButton.hidden = YES; +} + +- (void)handleStartDragVideoSlider:(UISlider *)slider { + [self.videoPlayer pause]; + [self removePlayerTimeObserver]; +} + +- (void)handleDraggingVideoSlider:(UISlider *)slider { + if (!self.isSeekingVideo) { + self.isSeekingVideo = YES; + [self updateVideoSliderLeftLabel]; + + CGFloat currentValue = slider.value; + [self.videoPlayer seekToTime:CMTimeMakeWithSeconds(currentValue, NSEC_PER_SEC) completionHandler:^(BOOL finished) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.isSeekingVideo = NO; + }); + }]; + } +} + +- (void)handleFinishDragVideoSlider:(UISlider *)slider { + [self.videoPlayer play]; + self.videoCenteredPlayButton.hidden = YES; + self.videoToolbar.playButton.hidden = YES; + self.videoToolbar.pauseButton.hidden = NO; + + [self addPlayerTimeObserver]; +} + +- (void)syncVideoProgressSlider { + double currentSeconds = CMTimeGetSeconds(self.videoPlayer.currentTime); + [self.videoToolbar.slider setValue:currentSeconds]; + [self updateVideoSliderLeftLabel]; +} + +- (void)configVideoProgressSlider { + self.videoToolbar.sliderLeftLabel.text = [self timeStringFromSeconds:0]; + double duration = CMTimeGetSeconds(self.videoPlayerItem.asset.duration); + self.videoToolbar.sliderRightLabel.text = [self timeStringFromSeconds:duration]; + + self.videoToolbar.slider.minimumValue = 0.0; + self.videoToolbar.slider.maximumValue = duration; + self.videoToolbar.slider.value = 0; + [self.videoToolbar.slider addTarget:self action:@selector(handleStartDragVideoSlider:) forControlEvents:UIControlEventTouchDown]; + [self.videoToolbar.slider addTarget:self action:@selector(handleDraggingVideoSlider:) forControlEvents:UIControlEventValueChanged]; + [self.videoToolbar.slider addTarget:self action:@selector(handleFinishDragVideoSlider:) forControlEvents:UIControlEventTouchUpInside]; + + [self addPlayerTimeObserver]; +} + +- (void)addPlayerTimeObserver { + if (self.videoTimeObserver) { + return; + } + double interval = .1f; + __weak QMUIZoomImageView *weakSelf = self; + self.videoTimeObserver = [self.videoPlayer addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(interval, NSEC_PER_SEC) queue:NULL usingBlock:^(CMTime time) { + [weakSelf syncVideoProgressSlider]; + }]; +} + +- (void)removePlayerTimeObserver { + if (!self.videoTimeObserver) { + return; + } + [self.videoPlayer removeTimeObserver:self.videoTimeObserver]; + self.videoTimeObserver = nil; +} + +- (void)updateVideoSliderLeftLabel { + double currentSeconds = CMTimeGetSeconds(self.videoPlayer.currentTime); + self.videoToolbar.sliderLeftLabel.text = [self timeStringFromSeconds:currentSeconds]; +} + +// convert "100" to "01:40" +- (NSString *)timeStringFromSeconds:(NSUInteger)seconds { + NSUInteger min = floor(seconds / 60); + NSUInteger sec = floor(seconds - min * 60); + return [NSString stringWithFormat:@"%02ld:%02ld", (long)min, (long)sec]; +} + +- (void)pauseVideo { + if (!self.videoPlayer) { + return; + } + [self handlePauseButton]; + [self removePlayerTimeObserver]; +} + +- (void)endPlayingVideo { + if (!self.videoPlayer) { + return; + } + [self.videoPlayer seekToTime:CMTimeMake(0, 1)]; + [self pauseVideo]; + [self syncVideoProgressSlider]; + self.videoToolbar.hidden = YES; + self.videoCenteredPlayButton.hidden = NO; + +} + +- (AVPlayerLayer *)videoPlayerLayer { + [self initVideoPlayerLayerIfNeeded]; + return _videoPlayerLayer; +} + +- (QMUIZoomImageViewVideoToolbar *)videoToolbar { + [self initVideoToolbarIfNeeded]; + return _videoToolbar; +} + +- (QMUIButton *)videoCenteredPlayButton { + [self initVideoCenteredPlayButtonIfNeeded]; + return _videoCenteredPlayButton; +} + +- (void)initVideoPlayerLayerIfNeeded { + if (self.videoPlayerView) { + return; + } + self.videoPlayerView = [[QMUIZoomImageVideoPlayerView alloc] init]; + _videoPlayerLayer = (AVPlayerLayer *)self.videoPlayerView.layer; + self.videoPlayerView.hidden = YES; + [self.scrollView addSubview:self.videoPlayerView]; +} + +- (void)initVideoToolbarIfNeeded { + if (_videoToolbar) { + return; + } + _videoToolbar = ({ + QMUIZoomImageViewVideoToolbar * b = [[QMUIZoomImageViewVideoToolbar alloc] init]; + [b.playButton addTarget:self action:@selector(handlePlayButton:) forControlEvents:UIControlEventTouchUpInside]; + [b.pauseButton addTarget:self action:@selector(handlePauseButton) forControlEvents:UIControlEventTouchUpInside]; + [self insertSubview:b belowSubview:self.emptyView]; + b.hidden = YES; + b; + }); +} + +- (void)initVideoCenteredPlayButtonIfNeeded { + if (_videoCenteredPlayButton) { + return; + } + + _videoCenteredPlayButton = ({ + QMUIButton *b = [[QMUIButton alloc] init]; + b.qmui_outsideEdge = UIEdgeInsetsMake(-60, -60, -60, -60); + b.tag = kTagForCenteredPlayButton; + [b setImage:self.videoCenteredPlayButtonImage forState:UIControlStateNormal]; + [b addTarget:self action:@selector(handlePlayButton:) forControlEvents:UIControlEventTouchUpInside]; + b.hidden = YES; + [self insertSubview:b belowSubview:self.emptyView]; + b; + }); +} + +- (void)initVideoRelatedViewsIfNeeded { + [self initVideoPlayerLayerIfNeeded]; + [self initVideoToolbarIfNeeded]; + [self initVideoCenteredPlayButtonIfNeeded]; + [self setNeedsLayout]; +} + +- (void)destroyVideoRelatedObjectsIfNeeded { + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; + [self removePlayerTimeObserver]; + + [self.videoPlayerView removeFromSuperview]; + self.videoPlayerView = nil; + + [self.videoToolbar removeFromSuperview]; + _videoToolbar = nil; + + [self.videoCenteredPlayButton removeFromSuperview]; + _videoCenteredPlayButton = nil; + + self.videoPlayer = nil; + _videoPlayerLayer.player = nil; +} + +- (void)setVideoToolbarMargins:(UIEdgeInsets)videoToolbarMargins { + _videoToolbarMargins = videoToolbarMargins; + [self setNeedsLayout]; +} + +- (void)setVideoCenteredPlayButtonImage:(UIImage *)videoCenteredPlayButtonImage { + _videoCenteredPlayButtonImage = videoCenteredPlayButtonImage; + if (!self.videoCenteredPlayButton) { + return; + } + [self.videoCenteredPlayButton setImage:videoCenteredPlayButtonImage forState:UIControlStateNormal]; + [self setNeedsLayout]; +} + +- (void)applicationDidEnterBackground { + [self pauseVideo]; +} + +#pragma mark - iCloud + +- (QMUIPieProgressView *)cloudProgressView { + [self initCloudRelatedViewsIfNeeded]; + return _cloudProgressView; +} + +- (UIButton *)cloudDownloadRetryButton { + [self initCloudRelatedViewsIfNeeded]; + return _cloudDownloadRetryButton; +} + +- (void)initCloudRelatedViewsIfNeeded { + [self initCloudProgressViewIfNeeded]; + [self initCloudDownloadRetryButtonIfNeeded]; +} + +- (void)initCloudProgressViewIfNeeded { + if (_cloudProgressView) { + return; + } + _cloudProgressView = [[QMUIPieProgressView alloc] init]; + _cloudProgressView.tintColor = ((UIActivityIndicatorView *)self.emptyView.loadingView).color; + _cloudProgressView.hidden = YES; + [self addSubview:_cloudProgressView]; +} + +- (void)initCloudDownloadRetryButtonIfNeeded { + if (_cloudDownloadRetryButton) { + return; + } + + _cloudDownloadRetryButton = [[QMUIButton alloc] init]; + [_cloudDownloadRetryButton setImage:[QMUIHelper imageWithName:@"QMUI_icloud_download_fault"] forState:UIControlStateNormal]; + _cloudDownloadRetryButton.adjustsImageTintColorAutomatically = YES; + _cloudDownloadRetryButton.tintColor = ((UIActivityIndicatorView *)self.emptyView.loadingView).color; + [_cloudDownloadRetryButton sizeToFit]; + _cloudDownloadRetryButton.qmui_outsideEdge = UIEdgeInsetsMake(-6, -6, -6, -6); + _cloudDownloadRetryButton.hidden = YES; + [_cloudDownloadRetryButton addTarget:self action:@selector(handleICloudDownloadRetryEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:_cloudDownloadRetryButton]; +} + +- (void)setCloudDownloadStatus:(QMUIAssetDownloadStatus)cloudDownloadStatus { + BOOL statusChanged = _cloudDownloadStatus != cloudDownloadStatus; + _cloudDownloadStatus = cloudDownloadStatus; + switch (cloudDownloadStatus) { + case QMUIAssetDownloadStatusSucceed: + self.cloudProgressView.hidden = YES; + self.cloudDownloadRetryButton.hidden = YES; + break; + + case QMUIAssetDownloadStatusDownloading: + self.cloudProgressView.hidden = NO; + [self.cloudProgressView.superview bringSubviewToFront:self.cloudProgressView]; + self.cloudDownloadRetryButton.hidden = YES; + break; + + case QMUIAssetDownloadStatusCanceled: + self.cloudProgressView.hidden = YES; + self.cloudDownloadRetryButton.hidden = YES; + break; + + case QMUIAssetDownloadStatusFailed: + self.cloudProgressView.hidden = YES; + self.cloudDownloadRetryButton.hidden = NO; + [self.cloudDownloadRetryButton.superview bringSubviewToFront:self.cloudDownloadRetryButton]; + break; + + default: + break; + } + if (statusChanged) { + [self setNeedsLayout]; + } +} + +- (void)handleICloudDownloadRetryEvent:(UIView *)sender { + if ([self.delegate respondsToSelector:@selector(didTouchICloudRetryButtonInZoomImageView:)]) { + [self.delegate didTouchICloudRetryButtonInZoomImageView:self]; + } +} + +#pragma mark - GestureRecognizers + +- (void)handleSingleTapGestureWithPoint:(UITapGestureRecognizer *)gestureRecognizer { + CGPoint gesturePoint = [gestureRecognizer locationInView:gestureRecognizer.view]; + if ([self.delegate respondsToSelector:@selector(singleTouchInZoomingImageView:location:)]) { + [self.delegate singleTouchInZoomingImageView:self location:gesturePoint]; + } + if (self.videoPlayerItem) { + self.videoToolbar.hidden = !self.videoToolbar.hidden; + if ([self.delegate respondsToSelector:@selector(zoomImageView:didHideVideoToolbar:)]) { + [self.delegate zoomImageView:self didHideVideoToolbar:self.videoToolbar.hidden]; + } + } +} + +- (void)handleDoubleTapGestureWithPoint:(UITapGestureRecognizer *)gestureRecognizer { + CGPoint gesturePoint = [gestureRecognizer locationInView:gestureRecognizer.view]; + if ([self.delegate respondsToSelector:@selector(doubleTouchInZoomingImageView:location:)]) { + [self.delegate doubleTouchInZoomingImageView:self location:gesturePoint]; + } + + if ([self enabledZoomImageView]) { + // 如果图片被压缩了,则第一次放大到原图大小,第二次放大到最大倍数 + if (self.scrollView.zoomScale >= self.scrollView.maximumZoomScale) { + [self setZoomScale:self.scrollView.minimumZoomScale animated:YES]; + } else { + CGFloat newZoomScale = 0; + if (self.scrollView.zoomScale < 1) { + // 如果目前显示的大小比原图小,则放大到原图 + newZoomScale = 1; + } else { + // 如果当前显示原图,则放大到最大的大小 + newZoomScale = self.scrollView.maximumZoomScale; + } + + CGRect zoomRect = CGRectZero; + CGPoint tapPoint = [[self contentView] convertPoint:gesturePoint fromView:gestureRecognizer.view]; + zoomRect.size.width = CGRectGetWidth(self.bounds) / newZoomScale; + zoomRect.size.height = CGRectGetHeight(self.bounds) / newZoomScale; + zoomRect.origin.x = tapPoint.x - CGRectGetWidth(zoomRect) / 2; + zoomRect.origin.y = tapPoint.y - CGRectGetHeight(zoomRect) / 2; + [self zoomToRect:zoomRect animated:YES]; + } + } +} + +- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)longPressGestureRecognizer { + if ([self enabledZoomImageView] && longPressGestureRecognizer.state == UIGestureRecognizerStateBegan) { + if ([self.delegate respondsToSelector:@selector(longPressInZoomingImageView:)]) { + [self.delegate longPressInZoomingImageView:self]; + } + } +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { + if ([touch.view isKindOfClass:[UISlider class]]) { + return NO; + } + return YES; +} + +#pragma mark - EmptyView + +- (void)showLoading { + // 挪到最前面 + [self insertSubview:self.emptyView atIndex:(self.subviews.count - 1)]; + [self.emptyView setLoadingViewHidden:NO]; + [self.emptyView setTextLabelText:nil]; + [self.emptyView setDetailTextLabelText:nil]; + [self.emptyView setActionButtonTitle:nil]; + self.emptyView.hidden = NO; + [self setNeedsLayout]; +} + +- (void)showEmptyViewWithText:(NSString *)text { + [self insertSubview:self.emptyView atIndex:(self.subviews.count - 1)]; + [self.emptyView setLoadingViewHidden:YES]; + [self.emptyView setTextLabelText:text]; + [self.emptyView setDetailTextLabelText:nil]; + [self.emptyView setActionButtonTitle:nil]; + self.emptyView.hidden = NO; + [self setNeedsLayout]; +} + +- (void)showEmptyViewWithText:(NSString *)text + detailText:(NSString *)detailText + buttonTitle:(NSString *)buttonTitle + buttonTarget:(id)buttonTarget + buttonAction:(SEL)action { + [self insertSubview:self.emptyView atIndex:(self.subviews.count - 1)]; + [self.emptyView setLoadingViewHidden:YES]; + [self.emptyView setImage:nil]; + [self.emptyView setTextLabelText:text]; + [self.emptyView setDetailTextLabelText:detailText]; + [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 { + self.emptyView.hidden = YES; + [self setNeedsLayout]; +} + +#pragma mark - + +- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { + return [self contentView]; +} + +- (void)scrollViewDidZoom:(UIScrollView *)scrollView { + [self handleDidEndZooming]; +} + +#pragma mark - 工具方法 + +- (CGRect)finalViewportRect { + CGRect rect = self.viewportRect; + if (CGRectIsEmpty(rect) && !CGRectIsEmpty(self.bounds)) { + // 有可能此时还没有走到过 layoutSubviews 因此拿不到正确的 scrollView 的 size,因此这里要强制 layout 一下 + if (!CGSizeEqualToSize(self.scrollView.bounds.size, self.bounds.size)) { + [self setNeedsLayout]; + [self layoutIfNeeded]; + } + rect = CGRectMakeWithSize(self.scrollView.bounds.size); + } + return rect; +} + +- (void)hideViews { + _livePhotoView.hidden = YES; + _imageView.hidden = YES; + _videoCenteredPlayButton.hidden = YES; + _videoPlayerLayer.hidden = YES; + _videoToolbar.hidden = YES; + _videoToolbar.pauseButton.hidden = YES; + _videoToolbar.playButton.hidden = YES; + _videoCenteredPlayButton.hidden = YES; +} + + +- (UIView *)contentView { + if (_imageView) { + return _imageView; + } + if (_livePhotoView) { + return _livePhotoView; + } + if (self.videoPlayerView) { + return self.videoPlayerView; + } + return nil; +} + +@end + +@interface QMUIZoomImageView (UIAppearance) + +@end + +@implementation QMUIZoomImageView (UIAppearance) + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self setDefaultAppearance]; + }); +} + ++ (void)setDefaultAppearance { + QMUIZoomImageView *appearance = [QMUIZoomImageView appearance]; + appearance.videoToolbarMargins = UIEdgeInsetsMake(0, 25, 25, 18); + appearance.videoCenteredPlayButtonImage = [QMUIZoomImageViewImageGenerator largePlayImage]; +} + +@end + +@implementation QMUIZoomImageVideoPlayerView + ++ (Class)layerClass { + return [AVPlayerLayer class]; +} + +@end + +@implementation QMUIZoomImageViewImageGenerator + ++ (UIImage *)largePlayImage { + CGFloat width = 60; + return [UIImage qmui_imageWithSize:CGSizeMake(width, width) opaque:NO scale:0 actions:^(CGContextRef contextRef) { + UIColor *color = kIconsColor; + CGContextSetStrokeColorWithColor(contextRef, color.CGColor); + + // circle outside + CGContextSetFillColorWithColor(contextRef, UIColorMakeWithRGBA(0, 0, 0, .25).CGColor); + CGFloat circleLineWidth = 1; + // consider line width to avoid edge clip + UIBezierPath *circle = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(circleLineWidth / 2, circleLineWidth / 2, width - circleLineWidth, width - circleLineWidth)]; + [circle setLineWidth:circleLineWidth]; + [circle stroke]; + [circle fill]; + + // triangle inside + CGContextSetFillColorWithColor(contextRef, color.CGColor); + CGFloat triangleLength = width / 2.5; + UIBezierPath *triangle = [self trianglePathWithLength:triangleLength]; + UIOffset offset = UIOffsetMake(width / 2 - triangleLength * tan(M_PI / 6) / 2, width / 2 - triangleLength / 2); + [triangle applyTransform:CGAffineTransformMakeTranslation(offset.horizontal, offset.vertical)]; + [triangle fill]; + }]; +} + ++ (UIImage *)smallPlayImage { + // width and height are equal + CGFloat width = 17; + return [UIImage qmui_imageWithSize:CGSizeMake(width, width) opaque:NO scale:0 actions:^(CGContextRef contextRef) { + UIColor *color = kIconsColor; + CGContextSetFillColorWithColor(contextRef, color.CGColor); + UIBezierPath *path = [self trianglePathWithLength:width]; + [path fill]; + }]; +} + ++ (UIImage *)pauseImage { + CGSize size = CGSizeMake(12, 18); + return [UIImage qmui_imageWithSize:size opaque:NO scale:0 actions:^(CGContextRef contextRef) { + UIColor *color = kIconsColor; + CGContextSetStrokeColorWithColor(contextRef, color.CGColor); + CGFloat lineWidth = 2; + UIBezierPath *path = [UIBezierPath bezierPath]; + [path moveToPoint:CGPointMake(lineWidth / 2, 0)]; + [path addLineToPoint:CGPointMake(lineWidth / 2, size.height)]; + [path moveToPoint:CGPointMake(size.width - lineWidth / 2, 0)]; + [path addLineToPoint:CGPointMake(size.width - lineWidth / 2, size.height)]; + [path setLineWidth:lineWidth]; + [path stroke]; + }]; +} + +// @param length of the triangle side ++ (UIBezierPath *)trianglePathWithLength:(CGFloat)length { + UIBezierPath *path = [UIBezierPath bezierPath]; + [path moveToPoint:CGPointZero]; + [path addLineToPoint:CGPointMake(length * cos(M_PI / 6), length / 2)]; + [path addLineToPoint:CGPointMake(0, length)]; + [path closePath]; + return path; +} + +@end + +@implementation QMUIZoomImageViewVideoToolbar + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + + _playButton = [[QMUIButton alloc] init]; + self.playButton.qmui_outsideEdge = UIEdgeInsetsMake(-10, -10, -10, -10); + [self.playButton setImage:self.playButtonImage forState:UIControlStateNormal]; + [self addSubview:self.playButton]; + + _pauseButton = [[QMUIButton alloc] init]; + self.pauseButton.qmui_outsideEdge = UIEdgeInsetsMake(-10, -10, -10, -10); + [self.pauseButton setImage:self.pauseButtonImage forState:UIControlStateNormal]; + [self addSubview:self.pauseButton]; + + _slider = [[UISlider alloc] init]; + self.slider.minimumTrackTintColor = UIColorMake(195, 195, 195); + self.slider.maximumTrackTintColor = UIColorMake(95, 95, 95); + 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]; + self.sliderLeftLabel.textAlignment = NSTextAlignmentCenter; + [self addSubview:self.sliderLeftLabel]; + + _sliderRightLabel = [[UILabel alloc] init]; + [self.sliderRightLabel qmui_setTheSameAppearanceAsLabel:self.sliderLeftLabel]; + [self addSubview:self.sliderRightLabel]; + + self.layer.qmui_shadow = [NSShadow qmui_shadowWithColor:[UIColorBlack colorWithAlphaComponent:.5] shadowOffset:CGSizeZero shadowRadius:10]; + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + CGFloat contentHeight = CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.paddings); + + self.playButton.frame = ({ + CGSize size = [self.playButton sizeThatFits:CGSizeMax]; + CGRectFlatMake(self.paddings.left, CGFloatGetCenter(contentHeight, size.height) + self.paddings.top, size.width, size.height); + }); + + self.pauseButton.frame = ({ + CGSize size = [self.pauseButton sizeThatFits:CGSizeMax]; + CGRectFlatMake(CGRectGetMidX(self.playButton.frame) - size.width / 2, CGRectGetMidY(self.playButton.frame) - size.height / 2, size.width, size.height); + }); + + CGFloat timeLabelWidth = 55; + self.sliderLeftLabel.frame = ({ + CGFloat marginLeft = 19; + CGRectFlatMake(CGRectGetMaxX(self.playButton.frame) + marginLeft, self.paddings.top, timeLabelWidth, contentHeight); + }); + self.sliderRightLabel.frame = ({ + CGRectFlatMake(CGRectGetWidth(self.bounds) - self.paddings.right - timeLabelWidth, self.paddings.top, timeLabelWidth, contentHeight); + }); + self.slider.frame = ({ + CGFloat marginToLabel = 4; + CGFloat x = CGRectGetMaxX(self.sliderLeftLabel.frame) + marginToLabel; + CGRectFlatMake(x, self.paddings.top, CGRectGetMinX(self.sliderRightLabel.frame) - marginToLabel - x, contentHeight); + }); +} + +- (CGSize)sizeThatFits:(CGSize)size { + CGFloat contentHeight = [self maxHeightAmongViews:@[self.playButton, self.pauseButton, self.sliderLeftLabel, self.sliderRightLabel, self.slider]]; + size.height = contentHeight + UIEdgeInsetsGetVerticalValue(self.paddings); + return size; +} + +- (void)setPaddings:(UIEdgeInsets)paddings { + _paddings = paddings; + [self setNeedsLayout]; +} + +- (void)setPlayButtonImage:(UIImage *)playButtonImage { + _playButtonImage = playButtonImage; + [self.playButton setImage:playButtonImage forState:UIControlStateNormal]; + [self setNeedsLayout]; +} + +- (void)setPauseButtonImage:(UIImage *)pauseButtonImage { + _pauseButtonImage = pauseButtonImage; + [self.pauseButton setImage:pauseButtonImage forState:UIControlStateNormal]; + [self setNeedsLayout]; +} + +// 返回一堆 view 中高度最大的那个的高度 +- (CGFloat)maxHeightAmongViews:(NSArray *)views { + __block CGFloat maxValue = 0; + [views enumerateObjectsUsingBlock:^(UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + CGFloat height = [obj sizeThatFits:CGSizeMax].height; + maxValue = MAX(height, maxValue); + }]; + return maxValue; +} + +@end + +@interface QMUIZoomImageViewVideoToolbar (UIAppearance) + +@end + +@implementation QMUIZoomImageViewVideoToolbar (UIAppearance) + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self setDefaultAppearance]; + }); +} + ++ (void)setDefaultAppearance { + QMUIZoomImageViewVideoToolbar *appearance = [QMUIZoomImageViewVideoToolbar appearance]; + appearance.playButtonImage = [QMUIZoomImageViewImageGenerator smallPlayImage]; + appearance.pauseButtonImage = [QMUIZoomImageViewImageGenerator pauseImage]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.h b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.h new file mode 100644 index 00000000..a5d8c834 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.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. + */ + +// +// QMUIStaticTableViewCellData.h +// qmui +// +// Created by QMUI Team on 15/5/3. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUITableViewCell; + +typedef NS_ENUM(NSInteger, QMUIStaticTableViewCellAccessoryType) { + QMUIStaticTableViewCellAccessoryTypeNone, + QMUIStaticTableViewCellAccessoryTypeDisclosureIndicator, + QMUIStaticTableViewCellAccessoryTypeDetailDisclosureButton, + QMUIStaticTableViewCellAccessoryTypeCheckmark, + QMUIStaticTableViewCellAccessoryTypeDetailButton, + QMUIStaticTableViewCellAccessoryTypeSwitch, +}; + +/** + * 一个 cellData 对象用于存储 static tableView(例如设置界面那种列表) 列表里的一行 cell 的基本信息,包括这个 cell 的 class、text、detailText、accessoryView 等。 + * @see QMUIStaticTableViewCellDataSource + */ +@interface QMUIStaticTableViewCellData : NSObject + +/// 当前 cellData 的标志,一般同个 tableView 里的每个 cellData 都会拥有不相同的 identifier +@property(nonatomic, assign) NSInteger identifier; + +/// 当前 cellData 所对应的 indexPath +@property(nonatomic, strong, readonly, nullable) NSIndexPath *indexPath; + +/// cell 要使用的 class,默认为 QMUITableViewCell,若要改为自定义 class,必须是 QMUITableViewCell 的子类 +@property(nonatomic, assign) Class cellClass; + +/// init cell 时要使用的 style +@property(nonatomic, assign) UITableViewCellStyle style; + +/// cell 的高度,默认为 TableViewCellNormalHeight +@property(nonatomic, assign) CGFloat height; + +/// cell 左边要显示的图片,将会被设置到 cell.imageView.image +@property(nonatomic, strong, nullable) UIImage *image; + +/// cell 的文字,将会被设置到 cell.textLabel.text +@property(nonatomic, copy, nullable) NSString *text; + +/// cell 的详细文字,将会被设置到 cell.detailTextLabel.text,所以要求 cellData.style 的值必须是带 detailTextLabel 类型的 style +@property(nonatomic, copy, nullable) NSString *detailText; + +/// 会自动在 tableView:cellForRowAtIndexPath: 里调用,这样就不需要实现 cellForRow +@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 的点击事件被触发时,要由哪个对象来接收,当实现了 didSelectBlock 时本属性无效 +@property(nonatomic, assign, nullable) id didSelectTarget; + +/// 当 cell 的点击事件被触发时,要向 didSelectTarget 指针发送什么消息以响应事件,当实现了 didSelectBlock 时本属性无效 +/// @warning 这个 selector 接收一个参数,这个参数也即当前的 QMUIStaticTableViewCellData 对象 +@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, nullable) NSObject *accessoryValueObject; + +/// 当 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, nullable) id accessoryTarget; +@property(nonatomic, assign, nullable) SEL accessoryAction; + + + ++ (instancetype)staticTableViewCellDataWithIdentifier:(NSInteger)identifier + 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:(nullable UIImage *)image + text:(nullable NSString *)text + detailText:(nullable NSString *)detailText + didSelectTarget:(nullable id)didSelectTarget + didSelectAction:(nullable SEL)didSelectAction + accessoryType:(QMUIStaticTableViewCellAccessoryType)accessoryType + 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 new file mode 100644 index 00000000..341f5432 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.m @@ -0,0 +1,107 @@ +/** + * 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. + */ + +// +// QMUIStaticTableViewCellData.m +// qmui +// +// Created by QMUI Team on 15/5/3. +// + +#import "QMUIStaticTableViewCellData.h" +#import "QMUICore.h" +#import "QMUITableViewCell.h" + +@implementation QMUIStaticTableViewCellData + +- (void)setIndexPath:(NSIndexPath *)indexPath { + _indexPath = indexPath; +} + ++ (instancetype)staticTableViewCellDataWithIdentifier:(NSInteger)identifier + image:(UIImage *)image + text:(NSString *)text + detailText:(NSString *)detailText + didSelectTarget:(id)didSelectTarget + didSelectAction:(SEL)didSelectAction + accessoryType:(QMUIStaticTableViewCellAccessoryType)accessoryType { + 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 + cellClass:(Class)cellClass + style:(UITableViewCellStyle)style + height:(CGFloat)height + image:(UIImage *)image + text:(NSString *)text + detailText:(NSString *)detailText + didSelectTarget:(id)didSelectTarget + didSelectAction:(SEL)didSelectAction + accessoryType:(QMUIStaticTableViewCellAccessoryType)accessoryType + accessoryValueObject:(NSObject *)accessoryValueObject + accessoryTarget:(id)accessoryTarget + accessoryAction:(SEL)accessoryAction { + QMUIStaticTableViewCellData *data = [[self alloc] init]; + data.identifier = identifier; + data.cellClass = cellClass; + data.style = style; + data.height = height; + data.image = image; + data.text = text; + data.detailText = detailText; + data.didSelectTarget = didSelectTarget; + data.didSelectAction = didSelectAction; + data.accessoryType = accessoryType; + data.accessoryValueObject = accessoryValueObject; + data.accessoryTarget = accessoryTarget; + data.accessoryAction = accessoryAction; + return data; +} + +- (instancetype)init { + if (self = [super init]) { + self.cellClass = [QMUITableViewCell class]; + self.height = TableViewCellNormalHeight; + } + return self; +} + +- (void)setCellClass:(Class)cellClass { + QMUIAssert([cellClass isSubclassOfClass:[QMUITableViewCell class]], NSStringFromClass(self.class), @"%@.cellClass 必须为 QMUITableViewCell 的子类", NSStringFromClass(self.class)); + _cellClass = cellClass; +} + ++ (UITableViewCellAccessoryType)tableViewCellAccessoryTypeWithStaticAccessoryType:(QMUIStaticTableViewCellAccessoryType)type { + switch (type) { + case QMUIStaticTableViewCellAccessoryTypeDisclosureIndicator: + return UITableViewCellAccessoryDisclosureIndicator; + case QMUIStaticTableViewCellAccessoryTypeDetailDisclosureButton: + return UITableViewCellAccessoryDetailDisclosureButton; + case QMUIStaticTableViewCellAccessoryTypeCheckmark: + return UITableViewCellAccessoryCheckmark; + case QMUIStaticTableViewCellAccessoryTypeDetailButton: + return UITableViewCellAccessoryDetailButton; + case QMUIStaticTableViewCellAccessoryTypeSwitch: + default: + return UITableViewCellAccessoryNone; + } +} + +@end diff --git a/QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h similarity index 83% rename from QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h rename to QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h index be2d8538..3f225cac 100644 --- a/QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h @@ -1,9 +1,16 @@ +/** + * 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. + */ + // // QMUIStaticTableViewCellDataSource.h // qmui // -// Created by MoLice on 2017/6/20. -// Copyright © 2017年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2017/6/20. // #import diff --git a/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m new file mode 100644 index 00000000..ccb14970 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m @@ -0,0 +1,199 @@ +/** + * 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. + */ + +// +// QMUIStaticTableViewCellDataSource.m +// qmui +// +// Created by QMUI Team on 2017/6/20. +// + +#import "QMUIStaticTableViewCellDataSource.h" +#import "QMUICore.h" +#import "QMUIStaticTableViewCellData.h" +#import "QMUITableViewCell.h" +#import "UITableView+QMUIStaticCell.h" +#import "QMUILog.h" +#import "QMUIMultipleDelegates.h" +#import "NSArray+QMUI.h" + +@interface QMUIStaticTableViewCellDataSource () +@end + +@implementation QMUIStaticTableViewCellDataSource + +- (instancetype)init { + if (self = [super init]) { + } + return self; +} + +- (instancetype)initWithCellDataSections:(NSArray *> *)cellDataSections { + if (self = [super init]) { + self.cellDataSections = cellDataSections; + } + return self; +} + +- (void)setCellDataSections:(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]; +} + +// 在 UITableView (QMUI_StaticCell) 那边会把 tableView 的 property 改为 readwrite,所以这里补上 setter +- (void)setTableView:(UITableView *)tableView { + _tableView = tableView; + // 触发 UITableView (QMUI_StaticCell) 里重写的 setter 里的逻辑 + tableView.dataSource = tableView.dataSource; + tableView.delegate = tableView.delegate; +} + +@end + +@interface QMUIStaticTableViewCellData (Manual) + +@property(nonatomic, strong, readwrite) NSIndexPath *indexPath; +@end + +@implementation QMUIStaticTableViewCellDataSource (Manual) + +- (QMUIStaticTableViewCellData *)cellDataAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section >= self.cellDataSections.count) { + QMUILog(NSStringFromClass(self.class), @"cellDataWithIndexPath:%@, data not exist in section!", indexPath); + return nil; + } + + NSArray *rowDatas = [self.cellDataSections objectAtIndex:indexPath.section]; + if (indexPath.row >= rowDatas.count) { + QMUILog(NSStringFromClass(self.class), @"cellDataWithIndexPath:%@, data not exist in row!", indexPath); + return nil; + } + + QMUIStaticTableViewCellData *cellData = [rowDatas objectAtIndex:indexPath.row]; + [cellData setIndexPath:indexPath];// 在这里才为 cellData.indexPath 赋值 + return cellData; +} + +- (NSString *)reuseIdentifierForCellAtIndexPath:(NSIndexPath *)indexPath { + QMUIStaticTableViewCellData *data = [self cellDataAtIndexPath:indexPath]; + return [NSString stringWithFormat:@"cell_%@", @(data.identifier)]; +} + +- (QMUITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath { + + QMUIStaticTableViewCellData *data = [self cellDataAtIndexPath:indexPath]; + if (!data) { + return nil; + } + + NSString *identifier = [self reuseIdentifierForCellAtIndexPath:indexPath]; + + QMUITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[data.cellClass alloc] initForTableView:self.tableView withStyle:data.style reuseIdentifier:identifier]; + } + cell.imageView.image = data.image; + cell.textLabel.text = data.text; + cell.detailTextLabel.text = data.detailText; + cell.accessoryType = [QMUIStaticTableViewCellData tableViewCellAccessoryTypeWithStaticAccessoryType:data.accessoryType]; + + // 为某些控件类型的accessory添加控件及相应的事件绑定 + if (data.accessoryType == QMUIStaticTableViewCellAccessoryTypeSwitch) { + UISwitch *switcher; + BOOL switcherOn = NO; + if ([cell.accessoryView isKindOfClass:[UISwitch class]]) { + switcher = (UISwitch *)cell.accessoryView; + } else { + switcher = [[UISwitch alloc] init]; + } + if ([data.accessoryValueObject isKindOfClass:[NSNumber class]]) { + switcherOn = [((NSNumber *)data.accessoryValueObject) boolValue]; + } + switcher.on = switcherOn; + [switcher removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents]; + 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.didSelectBlock && (!data.didSelectTarget || !data.didSelectAction))) { + cell.selectionStyle = UITableViewCellSelectionStyleNone; + } else { + cell.selectionStyle = UITableViewCellSelectionStyleBlue; + } + + [cell updateCellAppearanceWithIndexPath:indexPath]; + + if (data.cellForRowBlock) { + data.cellForRowBlock(self.tableView, cell, data); + } + + return cell; +} + +- (CGFloat)heightForRowAtIndexPath:(NSIndexPath *)indexPath { + QMUIStaticTableViewCellData *cellData = [self cellDataAtIndexPath:indexPath]; + return cellData.height; +} + +- (void)didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + QMUIStaticTableViewCellData *cellData = [self cellDataAtIndexPath:indexPath]; + if (!cellData || (!cellData.didSelectBlock && (!cellData.didSelectTarget || !cellData.didSelectAction))) { + QMUITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; + if (cell.selectionStyle != UITableViewCellSelectionStyleNone) { + [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; + } + return; + } + + // 1、分发选中事件(UISwitch 类型不支持 didSelect) + 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,选中后自动反选) + if (cellData.accessoryType == QMUIStaticTableViewCellAccessoryTypeCheckmark) { + [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; + } +} + +- (void)accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath { + QMUIStaticTableViewCellData *cellData = [self cellDataAtIndexPath:indexPath]; + 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 new file mode 100644 index 00000000..643b37b5 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.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. + */ + +// +// UITableView+QMUIStaticCell.h +// qmui +// +// Created by QMUI Team on 2017/6/20. +// + +#import +#import + +@class QMUIStaticTableViewCellDataSource; + +/** + * 配合 QMUIStaticTableViewCellDataSource 使用,主要负责: + * 1. 提供 property 去绑定一个 static dataSource + * 2. 重写 setDataSource:、setDelegate: 方法,自动实现 UITableViewDataSource、UITableViewDelegate 里一些必要的方法 + * + * 使用方式:初始化一个 QMUIStaticTableViewCellDataSource 并将其赋值给 qmui_staticCellDataSource 属性即可。 + * + * @warning 当要动态更新 dataSource 时,可直接修改 self.qmui_staticCellDataSource.cellDataSections 数组,或者创建一个新的 QMUIStaticTableViewCellDataSource。不管用哪种方法,都不需要手动调用 reloadData,tableView 会自动刷新的。 + */ +@interface UITableView (QMUI_StaticCell) + +@property(nonatomic, strong) QMUIStaticTableViewCellDataSource *qmui_staticCellDataSource; +@end diff --git a/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.m b/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.m new file mode 100644 index 00000000..a63841be --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.m @@ -0,0 +1,143 @@ +/** + * 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. + */ + +// +// UITableView+QMUIStaticCell.m +// qmui +// +// Created by QMUI Team on 2017/6/20. +// + +#import "UITableView+QMUIStaticCell.h" +#import "QMUICore.h" +#import "QMUIStaticTableViewCellDataSource.h" +#import "QMUILog.h" +#import "QMUIMultipleDelegates.h" + +@interface QMUIStaticTableViewCellDataSource () + +@property(nonatomic, weak, readwrite) UITableView *tableView; +@end + +@implementation UITableView (QMUI_StaticCell) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + OverrideImplementation([UITableView class], @selector(setDataSource:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableView *selfObject, id dataSource) { + 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); + } + } + + // call super + void (*originSelectorIMP)(id, SEL, id); + originSelectorIMP = (void (*)(id, SEL, id))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, dataSource); + }; + }); + + OverrideImplementation([UITableView class], @selector(setDelegate:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITableView *selfObject, id delegate) { + + 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); + } + } + + // call super + void (*originSelectorIMP)(id, SEL, id); + originSelectorIMP = (void (*)(id, SEL, id))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, delegate); + }; + }); + }); +} + +static char kAssociatedObjectKey_staticCellDataSource; +- (void)setQmui_staticCellDataSource:(QMUIStaticTableViewCellDataSource *)qmui_staticCellDataSource { + objc_setAssociatedObject(self, &kAssociatedObjectKey_staticCellDataSource, qmui_staticCellDataSource, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + qmui_staticCellDataSource.tableView = self; + [self reloadData]; +} + +- (QMUIStaticTableViewCellDataSource *)qmui_staticCellDataSource { + return (QMUIStaticTableViewCellDataSource *)objc_getAssociatedObject(self, &kAssociatedObjectKey_staticCellDataSource); +} + +- (void)addSelector:(SEL)selector withImplementation:(IMP)implementation types:(const char *)types forObject:(NSObject *)object { + if (!class_addMethod(object.class, selector, implementation, types)) { + // 把那些已经手动 addMethod 过的 class 存起来,避免每次都触发 log,打了一堆重复的信息 + [QMUIHelper executeBlock:^{ + QMUILog(NSStringFromClass(self.class), @"尝试为 %@ 添加方法 %@ 失败,可能该类里已经实现了这个方法", NSStringFromClass(object.class), NSStringFromSelector(selector)); + } oncePerIdentifier:[NSString stringWithFormat:@"addedlog %@-%@", NSStringFromClass(object.class), NSStringFromSelector(selector)]]; + } +} + +#pragma mark - DataSource + +NSInteger staticCell_numberOfSections (id current_self, SEL current_cmd, UITableView *tableView) { + return tableView.qmui_staticCellDataSource.cellDataSections.count; +} + +NSInteger staticCell_numberOfRows (id current_self, SEL current_cmd, UITableView *tableView, NSInteger section) { + return tableView.qmui_staticCellDataSource.cellDataSections[section].count; +} + +id staticCell_cellForRow (id current_self, SEL current_cmd, UITableView *tableView, NSIndexPath *indexPath) { + QMUITableViewCell *cell = [tableView.qmui_staticCellDataSource cellForRowAtIndexPath:indexPath]; + return cell; +} + +#pragma mark - Delegate + +CGFloat staticCell_heightForRow (id current_self, SEL current_cmd, UITableView *tableView, NSIndexPath *indexPath) { + return [tableView.qmui_staticCellDataSource heightForRowAtIndexPath:indexPath]; +} + +void staticCell_didSelectRow (id current_self, SEL current_cmd, UITableView *tableView, NSIndexPath *indexPath) { + [tableView.qmui_staticCellDataSource didSelectRowAtIndexPath:indexPath]; +} + +void staticCell_accessoryButtonTapped (id current_self, SEL current_cmd, UITableView *tableView, NSIndexPath *indexPath) { + [tableView.qmui_staticCellDataSource accessoryButtonTappedForRowWithIndexPath:indexPath]; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.h b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.h new file mode 100644 index 00000000..d8be3f04 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.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. + */ + +// +// QMUIToastAnimator.h +// qmui +// +// Created by QMUI Team on 2016/12/12. +// + +#import + +@class QMUIToastView; + +/** + * `QMUIToastAnimatorDelegate`是所有`QMUIToastAnimator`或者其子类必须遵循的协议,是整个动画过程实现的地方。 + */ +@protocol QMUIToastAnimatorDelegate + +@required + +- (void)showWithCompletion:(void (^)(BOOL finished))completion; +- (void)hideWithCompletion:(void (^)(BOOL finished))completion; +- (BOOL)isShowing; +- (BOOL)isAnimating; +@end + +typedef NS_ENUM(NSInteger, QMUIToastAnimationType) { + QMUIToastAnimationTypeFade = 0, + QMUIToastAnimationTypeZoom, + QMUIToastAnimationTypeSlide +}; + +/** + * `QMUIToastAnimator`可以让你通过实现一些协议来自定义ToastView显示和隐藏的动画。你可以继承`QMUIToastAnimator`,然后实现`QMUIToastAnimatorDelegate`中的方法,即可实现自定义的动画。QMUIToastAnimator默认也提供了几种type的动画:1、QMUIToastAnimationTypeFade;2、QMUIToastAnimationTypeZoom;3、QMUIToastAnimationTypeSlide; + */ +@interface QMUIToastAnimator : NSObject + +/** + * 初始化方法,请务必使用这个方法来初始化。 + * + * @param toastView 要使用这个animator的QMUIToastView实例。 + */ +- (instancetype)initWithToastView:(QMUIToastView *)toastView NS_DESIGNATED_INITIALIZER; + +/** + * 获取初始化传进来的QMUIToastView。 + */ +@property(nonatomic, weak, readonly) QMUIToastView *toastView; + +/** + * 指定QMUIToastAnimator做动画的类型type。此功能暂时未实现,目前所有动画类型都是QMUIToastAnimationTypeFade。 + */ +@property(nonatomic, assign) QMUIToastAnimationType animationType; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.m b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.m new file mode 100644 index 00000000..e9f41818 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.m @@ -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. + */ + +// +// QMUIToastAnimator.m +// qmui +// +// Created by QMUI Team on 2016/12/12. +// + +#import "QMUIToastAnimator.h" +#import "QMUICore.h" +#import "QMUIToastView.h" + +#define kSlideAnimationKey @"kSlideAnimationKey" + +@interface QMUIToastAnimator () + +@property (nonatomic, assign) BOOL isShowing; +@property (nonatomic, assign) BOOL isAnimating; +@property (nonatomic, copy) void (^basicAnimationCompletion)(BOOL finished); + +@end + +@implementation QMUIToastAnimator + +- (instancetype)init { + NSAssert(NO, @"请使用initWithToastView:初始化"); + return [self initWithToastView:nil]; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + NSAssert(NO, @"请使用initWithToastView:初始化"); + return [self initWithToastView:nil]; +} + +- (instancetype)initWithToastView:(QMUIToastView *)toastView { + NSAssert(toastView, @"toastView不能为空"); + if (self = [super init]) { + _toastView = toastView; + } + return self; +} + +- (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; + 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); + } + }]; +} + +- (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 = alpha; + self.toastView.contentView.alpha = alpha; + } completion:^(BOOL finished) { + self.isAnimating = NO; + if (completion) { + completion(finished); + } + }]; +} + +- (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; +} + +- (BOOL)isAnimating { + return self.isAnimating; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.h b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.h new file mode 100644 index 00000000..e1a8f818 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.h @@ -0,0 +1,37 @@ +/** + * 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. + */ + +// +// QMUIToastBackgroundView.h +// qmui +// +// Created by QMUI Team on 2016/12/11. +// + +#import + +@interface QMUIToastBackgroundView : UIView + +/** + * 是否需要磨砂,默认NO。仅支持iOS8及以上版本。可以通过修改`styleColor`来控制磨砂的效果。 + */ +@property(nonatomic, assign) BOOL shouldBlurBackgroundView; + +@property(nullable, nonatomic, strong, readonly) UIVisualEffectView *effectView; + +/** + * 如果不设置磨砂,则styleColor直接作为`QMUIToastBackgroundView`的backgroundColor;如果需要磨砂,则会新增加一个`UIVisualEffectView`放在`QMUIToastBackgroundView`上面。 + */ +@property(nullable, nonatomic, strong) UIColor *styleColor UI_APPEARANCE_SELECTOR; + +/** + * 设置圆角。 + */ +@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.m b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.m new file mode 100644 index 00000000..4a77c457 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.m @@ -0,0 +1,96 @@ +/** + * 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. + */ + +// +// QMUIToastBackgroundView.m +// qmui +// +// Created by QMUI Team on 2016/12/11. +// + +#import "QMUIToastBackgroundView.h" +#import "QMUICore.h" + +@interface QMUIToastBackgroundView () + +@end + +@implementation QMUIToastBackgroundView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.layer.allowsGroupOpacity = NO; + self.backgroundColor = self.styleColor; + self.layer.cornerRadius = self.cornerRadius; + + } + return self; +} + +- (void)setShouldBlurBackgroundView:(BOOL)shouldBlurBackgroundView { + _shouldBlurBackgroundView = shouldBlurBackgroundView; + if (shouldBlurBackgroundView) { + UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; + _effectView = [[UIVisualEffectView alloc] initWithEffect:effect]; + self.effectView.layer.cornerRadius = self.cornerRadius; + self.effectView.layer.masksToBounds = YES; + [self addSubview:self.effectView]; + } else { + if (self.effectView) { + [self.effectView removeFromSuperview]; + _effectView = nil; + } + } +} + +- (void)layoutSubviews { + [super layoutSubviews]; + if (self.effectView) { + self.effectView.frame = self.bounds; + } +} + +#pragma mark - UIAppearance + +- (void)setStyleColor:(UIColor *)styleColor { + _styleColor = styleColor; + self.backgroundColor = styleColor; +} + +- (void)setCornerRadius:(CGFloat)cornerRadius { + _cornerRadius = cornerRadius; + self.layer.cornerRadius = cornerRadius; + if (self.effectView) { + self.effectView.layer.cornerRadius = cornerRadius; + } +} + +@end + + +@interface QMUIToastBackgroundView (UIAppearance) + +@end + +@implementation QMUIToastBackgroundView (UIAppearance) + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self setDefaultAppearance]; + }); +} + ++ (void)setDefaultAppearance { + QMUIToastBackgroundView *appearance = [QMUIToastBackgroundView appearance]; + appearance.styleColor = UIColorMakeWithRGBA(0, 0, 0, 0.8); + appearance.cornerRadius = 10.0; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.h b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.h new file mode 100644 index 00000000..e57b549b --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.h @@ -0,0 +1,86 @@ +/** + * 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. + */ + +// +// QMUIToastContentView.h +// qmui +// +// Created by QMUI Team on 2016/12/11. +// + +#import + +/** + * `QMUIToastView`默认使用的contentView。其结构是:customView->textLabel->detailTextLabel等三个view依次往下排列。其中customView可以赋值任意的UIView或者自定义的view。 + * 注意,customView 会响应 tintColor 的变化。而 textLabel/detailTextLabel 在没设置颜色到 attributes 里的情况下,也会跟随 tintColor 变化,设置了 attributes 的颜色则优先使用 attributes 里的颜色。 + * + * @TODO: 增加多种类型的progressView的支持。 + */ +@interface QMUIToastContentView : UIView + +/** + * 设置一个UIView,可以是:菊花、图片等等,请自行保证 customView 的 size 被正确设置。 + */ +@property(nonatomic, strong) UIView *customView; + +/** + * 设置第一行大文字label。 + */ +@property(nonatomic, strong, readonly) UILabel *textLabel; + +/** + * 通过textLabelText设置可以应用textLabelAttributes的样式,如果通过textLabel.text设置则可能导致一些样式失效。 + */ +@property(nonatomic, copy) NSString *textLabelText; + +/** + * 设置第二行小文字label。 + */ +@property(nonatomic, strong, readonly) UILabel *detailTextLabel; + +/** + * 通过detailTextLabelText设置可以应用detailTextLabelAttributes的样式,如果通过detailTextLabel.text设置则可能导致一些样式失效。 + */ +@property(nonatomic, copy) NSString *detailTextLabelText; + +/** + * 设置上下左右的padding。 + */ +@property(nonatomic, assign) UIEdgeInsets insets UI_APPEARANCE_SELECTOR; + +/** + * 设置最小size。 + */ +@property(nonatomic, assign) CGSize minimumSize UI_APPEARANCE_SELECTOR; + +/** + * 设置customView的marginBottom。 + */ +@property(nonatomic, assign) CGFloat customViewMarginBottom UI_APPEARANCE_SELECTOR; + +/** + * 设置textLabel的marginBottom。 + */ +@property(nonatomic, assign) CGFloat textLabelMarginBottom UI_APPEARANCE_SELECTOR; + +/** + * 设置detailTextLabel的marginBottom。 + */ +@property(nonatomic, assign) CGFloat detailTextLabelMarginBottom UI_APPEARANCE_SELECTOR; + +/** + * 设置textLabel的attributes,如果包含 NSForegroundColorAttributeName 则 textLabel 不响应 tintColor,如果不包含则 textLabel 会拿 tintColor 当成文字颜色。 + */ +@property(nonatomic, strong) NSDictionary *textLabelAttributes UI_APPEARANCE_SELECTOR; + +/** + * 设置 detailTextLabel 的 attributes,如果包含 NSForegroundColorAttributeName 则 detailTextLabel 不响应 tintColor,如果不包含则 detailTextLabel 会拿 tintColor 当成文字颜色。 + */ +@property(nonatomic, strong) NSDictionary *detailTextLabelAttributes UI_APPEARANCE_SELECTOR; + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.m b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.m new file mode 100644 index 00000000..4e3c5afb --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.m @@ -0,0 +1,249 @@ +/** + * 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. + */ + +// +// QMUIToastContentView.m +// qmui +// +// Created by QMUI Team on 2016/12/11. +// + +#import "QMUIToastContentView.h" +#import "QMUICore.h" +#import "UIView+QMUI.h" +#import "NSParagraphStyle+QMUI.h" + +@implementation QMUIToastContentView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.layer.allowsGroupOpacity = NO; + [self initSubviews]; + } + return self; +} + +- (void)initSubviews { + + _textLabel = [[UILabel alloc] init]; + self.textLabel.numberOfLines = 0; + self.textLabel.opaque = NO; + [self addSubview:self.textLabel]; + + _detailTextLabel = [[UILabel alloc] init]; + self.detailTextLabel.numberOfLines = 0; + self.detailTextLabel.opaque = NO; + [self addSubview:self.detailTextLabel]; +} + +- (void)setCustomView:(UIView *)customView { + if (self.customView) { + [self.customView removeFromSuperview]; + _customView = nil; + } + _customView = customView; + [self addSubview:self.customView]; + [self updateCustomViewTintColor]; + [self.superview setNeedsLayout]; +} + +- (void)setTextLabelText:(NSString *)textLabelText { + _textLabelText = textLabelText; + if (textLabelText) { + self.textLabel.attributedText = [[NSAttributedString alloc] initWithString:textLabelText attributes:self.textLabelAttributes]; + self.textLabel.textAlignment = NSTextAlignmentCenter; + } + [self.superview setNeedsLayout]; +} + +- (void)setDetailTextLabelText:(NSString *)detailTextLabelText { + _detailTextLabelText = detailTextLabelText; + if (detailTextLabelText) { + self.detailTextLabel.attributedText = [[NSAttributedString alloc] initWithString:detailTextLabelText attributes:self.detailTextLabelAttributes]; + self.detailTextLabel.textAlignment = NSTextAlignmentCenter; + } + [self.superview setNeedsLayout]; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return [self sizeThatFits:size shouldConsiderMinimumSize:YES]; +} + +- (CGSize)sizeThatFits:(CGSize)size shouldConsiderMinimumSize:(BOOL)shouldConsiderMinimumSize { + BOOL hasCustomView = !!self.customView; + BOOL hasTextLabel = self.textLabel.text.length > 0; + BOOL hasDetailTextLabel = self.detailTextLabel.text.length > 0; + + CGFloat width = 0; + CGFloat height = 0; + + CGFloat maxContentWidth = size.width - UIEdgeInsetsGetHorizontalValue(self.insets); + CGFloat maxContentHeight = size.height - UIEdgeInsetsGetVerticalValue(self.insets); + + if (hasCustomView) { + width = MIN(maxContentWidth, MAX(width, CGRectGetWidth(self.customView.frame))); + height += (CGRectGetHeight(self.customView.frame) + ((hasTextLabel || hasDetailTextLabel) ? self.customViewMarginBottom : 0)); + } + + if (hasTextLabel) { + CGSize textLabelSize = [self.textLabel sizeThatFits:CGSizeMake(maxContentWidth, maxContentHeight)]; + width = MIN(maxContentWidth, MAX(width, textLabelSize.width)); + height += (textLabelSize.height + (hasDetailTextLabel ? self.textLabelMarginBottom : 0)); + } + + if (hasDetailTextLabel) { + CGSize detailTextLabelSize = [self.detailTextLabel sizeThatFits:CGSizeMake(maxContentWidth, maxContentHeight)]; + width = MIN(maxContentWidth, MAX(width, detailTextLabelSize.width)); + height += (detailTextLabelSize.height + self.detailTextLabelMarginBottom); + } + + width += UIEdgeInsetsGetHorizontalValue(self.insets); + height += UIEdgeInsetsGetVerticalValue(self.insets); + + if (shouldConsiderMinimumSize) { + width = MAX(width, self.minimumSize.width); + height = MAX(height, self.minimumSize.height); + } + + return CGSizeMake(width, height); +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + BOOL hasCustomView = !!self.customView; + BOOL hasTextLabel = self.textLabel.text.length > 0; + BOOL hasDetailTextLabel = self.detailTextLabel.text.length > 0; + + CGFloat contentLimitWidth = self.qmui_width - UIEdgeInsetsGetHorizontalValue(self.insets); + CGSize contentSize = [self sizeThatFits:self.bounds.size shouldConsiderMinimumSize:NO]; + CGFloat minY = self.insets.top + CGFloatGetCenter(self.qmui_height - UIEdgeInsetsGetVerticalValue(self.insets), contentSize.height - UIEdgeInsetsGetVerticalValue(self.insets)); + if (hasCustomView) { + self.customView.qmui_left = self.insets.left + CGFloatGetCenter(contentLimitWidth, self.customView.qmui_width); + self.customView.qmui_top = minY; + minY = self.customView.qmui_bottom + self.customViewMarginBottom; + } + if (hasTextLabel) { + CGSize textLabelSize = [self.textLabel sizeThatFits:CGSizeMake(contentLimitWidth, CGFLOAT_MAX)]; + self.textLabel.qmui_left = self.insets.left; + self.textLabel.qmui_top = minY; + self.textLabel.qmui_width = contentLimitWidth; + self.textLabel.qmui_height = textLabelSize.height; + minY = self.textLabel.qmui_bottom + self.textLabelMarginBottom; + } + if (hasDetailTextLabel) { + CGSize detailTextLabelSize = [self.detailTextLabel sizeThatFits:CGSizeMake(contentLimitWidth, CGFLOAT_MAX)]; + self.detailTextLabel.qmui_left = self.insets.left; + self.detailTextLabel.qmui_top = minY; + self.detailTextLabel.qmui_width = contentLimitWidth; + self.detailTextLabel.qmui_height = detailTextLabelSize.height; + } +} + +- (void)tintColorDidChange { + [super tintColorDidChange]; + + if (self.customView) { + [self updateCustomViewTintColor]; + } + + // 如果通过 attributes 设置了文字颜色,则不再响应 tintColor + if (!self.textLabelAttributes[NSForegroundColorAttributeName]) { + self.textLabel.textColor = self.tintColor; + } + + if (!self.detailTextLabelAttributes[NSForegroundColorAttributeName]) { + self.detailTextLabel.textColor = self.tintColor; + } +} + +- (void)updateCustomViewTintColor { + if (!self.customView) { + return; + } + self.customView.tintColor = self.tintColor; + if ([self.customView isKindOfClass:[UIActivityIndicatorView class]]) { + UIActivityIndicatorView *customView = (UIActivityIndicatorView *)self.customView; + customView.color = self.tintColor; + } +} + +#pragma mark - UIAppearance + +- (void)setInsets:(UIEdgeInsets)insets { + _insets = insets; + [self.superview setNeedsLayout]; +} + +- (void)setMinimumSize:(CGSize)minimumSize { + _minimumSize = minimumSize; + [self.superview setNeedsLayout]; +} + +- (void)setCustomViewMarginBottom:(CGFloat)customViewMarginBottom { + _customViewMarginBottom = customViewMarginBottom; + [self.superview setNeedsLayout]; +} + +- (void)setTextLabelMarginBottom:(CGFloat)textLabelMarginBottom { + _textLabelMarginBottom = textLabelMarginBottom; + [self.superview setNeedsLayout]; +} + +- (void)setDetailTextLabelMarginBottom:(CGFloat)detailTextLabelMarginBottom { + _detailTextLabelMarginBottom = detailTextLabelMarginBottom; + [self.superview setNeedsLayout]; +} + +- (void)setTextLabelAttributes:(NSDictionary *)textLabelAttributes { + _textLabelAttributes = textLabelAttributes; + if (self.textLabelText && self.textLabelText.length > 0) { + // 刷新label的attributes + self.textLabelText = self.textLabelText; + } + [self.superview setNeedsLayout]; +} + +- (void)setDetailTextLabelAttributes:(NSDictionary *)detailTextLabelAttributes { + _detailTextLabelAttributes = detailTextLabelAttributes; + if (self.detailTextLabelText && self.detailTextLabelText.length > 0) { + // 刷新label的attributes + self.detailTextLabelText = self.detailTextLabelText; + } + [self.superview setNeedsLayout]; +} + +@end + + +@interface QMUIToastContentView (UIAppearance) + +@end + +@implementation QMUIToastContentView (UIAppearance) + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self setDefaultAppearance]; + }); +} + ++ (void)setDefaultAppearance { + QMUIToastContentView *appearance = [QMUIToastContentView appearance]; + appearance.insets = UIEdgeInsetsMake(16, 16, 16, 16); + appearance.minimumSize = CGSizeZero; + appearance.customViewMarginBottom = 8; + appearance.textLabelMarginBottom = 4; + appearance.detailTextLabelMarginBottom = 0; + appearance.textLabelAttributes = @{NSFontAttributeName: UIFontBoldMake(16), NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:22 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter]}; + appearance.detailTextLabelAttributes = @{NSFontAttributeName: UIFontBoldMake(12), NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:18 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter]}; +} + +@end diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.h b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.h new file mode 100644 index 00000000..61123762 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.h @@ -0,0 +1,175 @@ +/** + * 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. + */ + +// +// QMUIToastView.h +// qmui +// +// Created by QMUI Team on 2016/12/11. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class QMUIToastAnimator; + +typedef NS_ENUM(NSInteger, QMUIToastViewPosition) { + QMUIToastViewPositionTop, + QMUIToastViewPositionCenter, + QMUIToastViewPositionBottom +}; + +/** + * `QMUIToastView`是一个用来显示toast的控件,其主要结构包括:`backgroundView`、`contentView`,这两个view都是通过外部赋值获取,默认使用`QMUIToastBackgroundView`和`QMUIToastContentView`。 + * + * 拓展性:`QMUIToastBackgroundView`和`QMUIToastContentView`是QMUI提供的默认的view,这两个view都可以通过appearance来修改样式,如果这两个view满足不了需求,那么也可以通过新建自定义的view来代替这两个view。另外,QMUI也提供了默认的toastAnimator来实现ToastView的显示和隐藏动画,如果需要重新定义一套动画,可以继承`QMUIToastAnimator`并且实现`QMUIToastAnimatorDelegate`中的协议就可以自定义自己的一套动画。 + * + * 样式自定义:建议通过 tintColor 统一修改整个 toastView 的内容样式。当然你也可以单独修改 contentView.tintColor。默认情况下 QMUIToastView.tintColor = UIColorWhite。 + * + * 建议使用`QMUIToastView`的时候,再封装一层,具体可以参考`QMUITips`这个类。 + * + * @see QMUIToastBackgroundView + * @see QMUIToastContentView + * @see QMUIToastAnimator + * @see QMUITips + */ +@interface QMUIToastView : UIView + +/** + * 生成一个ToastView的唯一初始化方法,`view`的bound将会作为ToastView默认frame。 + * + * @param view ToastView的superView。 + */ +- (nonnull instancetype)initWithView:(nonnull UIView *)view NS_DESIGNATED_INITIALIZER; + +/** + * parentView是ToastView初始化的时候传进去的那个view。 + */ +@property(nonatomic, weak, readonly) UIView *parentView; + +/** + * 显示ToastView。 + * + * @param animated 是否需要通过动画显示。 + * + * @see toastAnimator + */ +- (void)showAnimated:(BOOL)animated; + +/** + * 隐藏ToastView。 + * + * @param animated 是否需要通过动画隐藏。 + * + * @see toastAnimator + */ +- (void)hideAnimated:(BOOL)animated; + +/** + * 在`delay`时间后隐藏ToastView。 + * + * @param animated 是否需要通过动画隐藏。 + * @param delay 多少秒后隐藏。 + * + * @see toastAnimator + */ +- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay; + +/// @warning 如果使用 [QMUITips showXxx] 系列快捷方法来显示 tips,willShowBlock 将会在 show 之后才被设置,最终并不会被调用。这种场景建议自己在调用 [QMUITips showXxx] 之前执行一段代码,或者不要使用 [QMUITips showXxx] 的方式显示 tips +@property(nullable, nonatomic, copy) void (^willShowBlock)(UIView *showInView, BOOL animated); +@property(nullable, nonatomic, copy) void (^didShowBlock)(UIView *showInView, BOOL animated); +@property(nullable, nonatomic, copy) void (^willHideBlock)(UIView *hideInView, BOOL animated); +@property(nullable, nonatomic, copy) void (^didHideBlock)(UIView *hideInView, BOOL animated); + +/** + * `QMUIToastAnimator`可以让你通过实现一些协议来自定义ToastView显示和隐藏的动画。你可以继承`QMUIToastAnimator`,然后实现`QMUIToastAnimatorDelegate`中的方法,即可实现自定义的动画。如果不赋值,则会使用`QMUIToastAnimator`中的默认动画。 + */ +@property(nullable, nonatomic, strong) QMUIToastAnimator *toastAnimator; + +/** + * 决定QMUIToastView的位置,目前有上中下三个位置,默认值是center。 + + * 如果设置了top或者bottom,那么ToastView的布局规则是:顶部从marginInsets.top开始往下布局(QMUIToastViewPositionTop) 和 底部从marginInsets.bottom开始往上布局(QMUIToastViewPositionBottom)。 + */ +@property(nonatomic, assign) QMUIToastViewPosition toastPosition; + +/** + * 是否在ToastView隐藏的时候顺便把它从superView移除,默认为NO。 + */ +@property(nonatomic, assign) BOOL removeFromSuperViewWhenHide; + + +/////////////////// + + +/** + * 会盖住整个superView,防止手指可以点击到ToastView下面的内容,默认透明。 + */ +@property(nonatomic, strong, readonly) UIView *dimmingView; + +/**s + * 承载Toast内容的UIView,可以自定义并赋值给contentView。如果contentView需要跟随ToastView的tintColor变化而变化,可以重写自定义view的`tintColorDidChange`来实现。默认使用`QMUIToastContentView`实现。 + */ +@property(nonatomic, strong) __kindof UIView *contentView; + +/** + * `contentView`下面的黑色背景UIView,默认使用`QMUIToastBackgroundView`实现,可以通过`QMUIToastBackgroundView`的 cornerRadius 和 styleColor 来修改圆角和背景色。 + */ +@property(nonatomic, strong) __kindof UIView *backgroundView; + + +/////////////////// + + +/** + * 上下左右的偏移值。 + */ +@property(nonatomic, assign) CGPoint offset UI_APPEARANCE_SELECTOR; + +/** + * ToastView 距离 parentView 去除 safeAreaInsets 后的区域的上下左右间距。 + * + * 例如当 marginInsets.top = 0 且 toastPosition 为 QMUIToastViewPositionTop 时,如果 parentView 是 viewController.view,则 tips 顶边缘将会紧贴 navigationBar 的底边缘。而如果 parentView 是 navigationController.view,则 tips 顶边缘将会紧贴 statusBar 的底边缘。 + */ +@property(nonatomic, assign) UIEdgeInsets marginInsets UI_APPEARANCE_SELECTOR; + +@end + + +@interface QMUIToastView (ToastTool) + +/** + * 工具方法。隐藏`view`里面的所有 ToastView + * + * @param view 即将隐藏的 ToastView 的 superView,如果 view = nil 则移除所有内存中的 ToastView + * @param animated 是否需要通过动画隐藏。 + * + * @return 如果成功隐藏一个 ToastView 则返回 YES,失败则 NO + */ ++ (BOOL)hideAllToastInView:(UIView * _Nullable)view animated:(BOOL)animated; + +/** + * 工具方法。返回`view`里面最顶部的 ToastView + * + * @param view ToastView 的 superView + * @return 返回一个 QMUIToastView 的实例 + */ ++ (nullable __kindof UIView *)toastInView:(UIView *)view; + +/** + * 工具方法。返回`view`里面所有的 ToastView + * + * @param view ToastView 的 superView + * @return 包含所有 view 里面的所有 QMUIToastView,如果 view = nil 则返回所有内存中的 ToastView + */ ++ (nullable NSArray *)allToastInView:(UIView *)view; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.m b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.m new file mode 100644 index 00000000..af83e6f2 --- /dev/null +++ b/QMUI/QMUIKit/QMUIComponents/ToastView/QMUIToastView.m @@ -0,0 +1,361 @@ +/** + * 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. + */ + +// +// QMUIToastView.m +// qmui +// +// Created by QMUI Team on 2016/12/11. +// + +#import "QMUIToastView.h" +#import "QMUICore.h" +#import "QMUIToastAnimator.h" +#import "QMUIToastContentView.h" +#import "QMUIToastBackgroundView.h" +#import "QMUIKeyboardManager.h" +#import "UIView+QMUI.h" + +static NSMutableArray *kToastViews = nil; + +@interface QMUIToastView () + +@property(nonatomic, weak) NSTimer *hideDelayTimer; + +@end + +@implementation QMUIToastView + +#pragma mark - 初始化 + +- (instancetype)initWithFrame:(CGRect)frame { + NSAssert(NO, @"请使用initWithView:初始化"); + return [self initWithView:[[UIView alloc] init]]; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + NSAssert(NO, @"请使用initWithView:初始化"); + return [self initWithView:[[UIView alloc] init]]; +} + +- (nonnull instancetype)initWithView:(nonnull UIView *)view { + NSAssert(view, @"view不能为空"); + if (self = [super initWithFrame:view.bounds]) { + _parentView = view; + [self didInitialize]; + } + return self; +} + +- (void)dealloc { + [self removeNotifications]; + if ([kToastViews containsObject:self]) { + [kToastViews removeObject:self]; + } +} + +- (void)didInitialize { + + self.tintColor = UIColorWhite; + + self.toastPosition = QMUIToastViewPositionCenter; + + // 顺序不能乱,先添加backgroundView再添加contentView + self.backgroundView = [self defaultBackgrondView]; + self.contentView = [self defaultContentView]; + + self.opaque = NO; + self.alpha = 0.0; + self.backgroundColor = UIColorClear; + self.layer.allowsGroupOpacity = NO; + + _dimmingView = [[UIView alloc] init]; + self.dimmingView.backgroundColor = UIColorClear; + [self addSubview:self.dimmingView]; + + [self registerNotifications]; +} + +- (void)didMoveToSuperview { + if (!kToastViews) { + kToastViews = [[NSMutableArray alloc] init]; + } + if (self.superview) { + // show + if (![kToastViews containsObject:self]) { + [kToastViews addObject:self]; + } + } else { + // hide + if ([kToastViews containsObject:self]) { + [kToastViews removeObject:self]; + } + } +} + +- (void)removeFromSuperview { + [super removeFromSuperview]; + _parentView = nil; +} + +- (QMUIToastAnimator *)defaultAnimator { + QMUIToastAnimator *toastAnimator = [[QMUIToastAnimator alloc] initWithToastView:self]; + return toastAnimator; +} + +- (UIView *)defaultBackgrondView { + QMUIToastBackgroundView *backgroundView = [[QMUIToastBackgroundView alloc] init]; + return backgroundView; +} + +- (UIView *)defaultContentView { + QMUIToastContentView *contentView = [[QMUIToastContentView alloc] init]; + return contentView; +} + +- (void)setBackgroundView:(UIView *)backgroundView { + if (self.backgroundView) { + [self.backgroundView removeFromSuperview]; + _backgroundView = nil; + } + _backgroundView = backgroundView; + self.backgroundView.alpha = 0.0; + [self addSubview:self.backgroundView]; + [self setNeedsLayout]; +} + +- (void)setContentView:(UIView *)contentView { + if (self.contentView) { + [self.contentView removeFromSuperview]; + _contentView = nil; + } + _contentView = contentView; + self.contentView.alpha = 0.0; + [self addSubview:self.contentView]; + [self setNeedsLayout]; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + self.frame = self.parentView.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.safeAreaInsets); + + CGFloat limitWidth = contentWidth - UIEdgeInsetsGetHorizontalValue(marginInsets); + CGFloat limitHeight = contentHeight - UIEdgeInsetsGetVerticalValue(marginInsets); + + if ([QMUIKeyboardManager isKeyboardVisible]) { + // 处理键盘相关逻辑,当键盘在显示的时候,内容高度会减去键盘的高度以使 Toast 居中 + CGRect keyboardFrame = [QMUIKeyboardManager currentKeyboardFrame]; + CGRect parentViewRect = [[QMUIKeyboardManager keyboardWindow] convertRect:self.parentView.frame fromView:self.parentView.superview]; + CGRect intersectionRect = CGRectIntersection(keyboardFrame, parentViewRect); + CGRect overlapRect = CGRectIsValidated(intersectionRect) ? CGRectFlatted(intersectionRect) : CGRectZero; + contentHeight -= CGRectGetHeight(overlapRect); + } + + if (self.contentView) { + + CGSize contentViewSize = [self.contentView sizeThatFits:CGSizeMake(limitWidth, limitHeight)]; + contentViewSize.width = MIN(contentViewSize.width, limitWidth); + contentViewSize.height = MIN(contentViewSize.height, limitHeight); + CGFloat contentViewX = MAX(marginInsets.left, (contentWidth - contentViewSize.width) / 2) + self.offset.x; + CGFloat contentViewY = MAX(marginInsets.top, (contentHeight - contentViewSize.height) / 2) + self.offset.y; + + if (self.toastPosition == QMUIToastViewPositionTop) { + contentViewY = marginInsets.top + self.offset.y; + } else if (self.toastPosition == QMUIToastViewPositionBottom) { + contentViewY = contentHeight - contentViewSize.height - marginInsets.bottom + self.offset.y; + } + + 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里面做。 + self.backgroundView.frame = self.contentView.frame; + } +} + +#pragma mark - 横竖屏 + +- (void)registerNotifications { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(statusBarOrientationDidChange:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; +} + +- (void)removeNotifications { + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; +} + +- (void)statusBarOrientationDidChange:(NSNotification *)notification { + if (!self.parentView) { + return; + } + [self setNeedsLayout]; + [self layoutIfNeeded]; +} + +#pragma mark - Show and Hide + +- (void)showAnimated:(BOOL)animated { + + // show之前需要layout以下,防止同一个tip切换不同的状态导致layout没更新 + [self setNeedsLayout]; + + [self.hideDelayTimer invalidate]; + self.alpha = 1.0; + + if (self.willShowBlock) { + self.willShowBlock(self.parentView, animated); + } + + if (animated) { + if (!self.toastAnimator) { + self.toastAnimator = [self defaultAnimator]; + } + if (self.toastAnimator) { + __weak __typeof(self)weakSelf = self; + [self.toastAnimator showWithCompletion:^(BOOL finished) { + if (weakSelf.didShowBlock) { + weakSelf.didShowBlock(weakSelf.parentView, animated); + } + }]; + } + } else { + self.backgroundView.alpha = 1.0; + self.contentView.alpha = 1.0; + if (self.didShowBlock) { + self.didShowBlock(self.parentView, animated); + } + } +} + +- (void)hideAnimated:(BOOL)animated { + if (self.willHideBlock) { + self.willHideBlock(self.parentView, animated); + } + if (animated) { + if (!self.toastAnimator) { + self.toastAnimator = [self defaultAnimator]; + } + if (self.toastAnimator) { + __weak __typeof(self)weakSelf = self; + [self.toastAnimator hideWithCompletion:^(BOOL finished) { + [weakSelf didHideWithAnimated:animated]; + }]; + } + } else { + self.backgroundView.alpha = 0.0; + self.contentView.alpha = 0.0; + [self didHideWithAnimated:animated]; + } +} + +- (void)didHideWithAnimated:(BOOL)animated { + + if (self.didHideBlock) { + self.didHideBlock(self.parentView, animated); + } + + [self.hideDelayTimer invalidate]; + + self.alpha = 0.0; + if (self.removeFromSuperViewWhenHide) { + [self removeFromSuperview]; + } +} + +- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay { + NSTimer *timer = [NSTimer timerWithTimeInterval:delay target:self selector:@selector(handleHideTimer:) userInfo:@(animated) repeats:NO]; + [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; + self.hideDelayTimer = timer; +} + +- (void)handleHideTimer:(NSTimer *)timer { + [self hideAnimated:[timer.userInfo boolValue]]; +} + +#pragma mark - UIAppearance + +- (void)setOffset:(CGPoint)offset { + _offset = offset; + [self setNeedsLayout]; +} + +- (void)setMarginInsets:(UIEdgeInsets)marginInsets { + _marginInsets = marginInsets; + [self setNeedsLayout]; +} + +@end + + +@interface QMUIToastView (UIAppearance) + +@end + +@implementation QMUIToastView (UIAppearance) + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self setDefaultAppearance]; + }); +} + ++ (void)setDefaultAppearance { + QMUIToastView *appearance = [QMUIToastView appearance]; + appearance.offset = CGPointZero; + appearance.marginInsets = UIEdgeInsetsMake(20, 20, 20, 20); +} + +@end + +@implementation QMUIToastView (ToastTool) + ++ (BOOL)hideAllToastInView:(UIView *)view animated:(BOOL)animated { + NSArray *toastViews = [self allToastInView:view]; + BOOL result = NO; + for (QMUIToastView *toastView in toastViews) { + result = YES; + toastView.removeFromSuperViewWhenHide = YES; + [toastView hideAnimated:animated]; + } + return result; +} + ++ (nullable __kindof UIView *)toastInView:(UIView *)view { + if (kToastViews.count <= 0) { + return nil; + } + UIView *toastView = kToastViews.lastObject; + if ([toastView isKindOfClass:self]) { + return toastView; + } + return nil; +} + ++ (nullable NSArray *)allToastInView:(UIView *)view { + if (!view) { + return kToastViews.count > 0 ? [kToastViews mutableCopy] : nil; + } + NSMutableArray *toastViews = [[NSMutableArray alloc] init]; + for (UIView *toastView in kToastViews) { + if (toastView.superview == view && [toastView isKindOfClass:self]) { + [toastViews addObject:toastView]; + } + } + return toastViews.count > 0 ? [toastViews mutableCopy] : nil; +} + +@end diff --git a/QMUI/QMUIKit/QMUICore/QMUICommonDefines.h b/QMUI/QMUIKit/QMUICore/QMUICommonDefines.h new file mode 100644 index 00000000..8f77b6d4 --- /dev/null +++ b/QMUI/QMUIKit/QMUICore/QMUICommonDefines.h @@ -0,0 +1,866 @@ +/** + * 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. + */ + +// +// QMUICommonDefines.h +// qmui +// +// Created by QMUI Team on 14-6-23. +// + +#ifndef QMUICommonDefines_h +#define QMUICommonDefines_h + +#import +#import "QMUIHelper.h" +#import "NSString+QMUI.h" + +#pragma mark - 变量-编译相关 + +/// 判断当前是否debug编译模式 +#ifdef DEBUG +#define IS_DEBUG YES +#else +#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 +#endif + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 +/// 当前编译使用的 Base SDK 版本为 iOS 10.0 及以上 +#define IOS10_SDK_ALLOWED YES +#endif + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 +/// 当前编译使用的 Base SDK 版本为 iOS 11.0 及以上 +#define IOS11_SDK_ALLOWED YES +#endif + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 120000 +/// 当前编译使用的 Base SDK 版本为 iOS 12.0 及以上 +#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 +#define ClangWarningConcat(warning_name) ArgumentToString(clang diagnostic ignored warning_name) + +/// 参数可直接传入 clang 的 warning 名,warning 列表参考:https://clang.llvm.org/docs/DiagnosticsReference.html +#define BeginIgnoreClangWarning(warningName) _Pragma("clang diagnostic push") _Pragma(ClangWarningConcat(#warningName)) +#define EndIgnoreClangWarning _Pragma("clang diagnostic pop") + +#define BeginIgnorePerformSelectorLeaksWarning BeginIgnoreClangWarning(-Warc-performSelector-leaks) +#define EndIgnorePerformSelectorLeaksWarning EndIgnoreClangWarning + +#define BeginIgnoreAvailabilityWarning BeginIgnoreClangWarning(-Wpartial-availability) +#define EndIgnoreAvailabilityWarning EndIgnoreClangWarning + +#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 - 变量-设备相关 + +/// 设备类型 +#define IS_IPAD [QMUIHelper isIPad] +#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]) + +/// 数字形式的操作系统版本号,可直接用于大小比较;如 110205 代表 11.2.5 版本;根据 iOS 规范,版本号最多可能有3位 +#define IOS_VERSION_NUMBER [QMUIHelper numbericOSVersion] + +/// 是否横竖屏 +/// 用户界面横屏了才会返回YES +#define IS_LANDSCAPE UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication.statusBarOrientation) +/// 无论支不支持横屏,只要设备横屏了,就会返回YES +#define IS_DEVICE_LANDSCAPE UIDeviceOrientationIsLandscape([[UIDevice currentDevice] orientation]) + +/// 屏幕宽度,会根据横竖屏的变化而变化 +#define SCREEN_WIDTH ([[UIScreen mainScreen] bounds].size.width) + +/// 屏幕高度,会根据横竖屏的变化而变化 +#define SCREEN_HEIGHT ([[UIScreen mainScreen] bounds].size.height) + +/// 设备宽度,跟横竖屏无关 +#define DEVICE_WIDTH MIN([[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 +#define IS_40INCH_SCREEN [QMUIHelper is40InchScreen] +/// iPhone 4/4S +#define IS_35INCH_SCREEN [QMUIHelper is35InchScreen] +/// iPhone 4/4S/5/5S/SE +#define IS_320WIDTH_SCREEN (IS_35INCH_SCREEN || IS_40INCH_SCREEN) + +/// 是否Retina +#define IS_RETINASCREEN ([[UIScreen mainScreen] scale] >= 2.0) + +/// 是否放大模式(iPhone 6及以上的设备支持放大模式,iPhone X 除外) +#define IS_ZOOMEDMODE [QMUIHelper isZoomedMode] + +/// 当前设备是否拥有灵动岛 +#define IS_DYNAMICISLAND_DEVICE [QMUIHelper isDynamicIslandDevice] + +#pragma mark - 变量-布局相关 + +/// 获取一个像素 +#define PixelOne [QMUIHelper pixelOne] + +/// bounds && nativeBounds / scale && nativeScale +#define ScreenBoundsSize ([[UIScreen mainScreen] bounds].size) +#define ScreenNativeBoundsSize ([[UIScreen mainScreen] nativeBounds].size) +#define ScreenScale ([[UIScreen mainScreen] scale]) +#define ScreenNativeScale ([[UIScreen mainScreen] nativeScale]) + +/// toolBar相关frame +#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) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(49, 32) : 49) + SafeAreaInsetsConstantForDeviceWithNotch.bottom) + +/// 状态栏高度(来电等情况下,状态栏高度会发生变化,所以应该实时计算,iOS 13 起,来电等情况下状态栏高度不会改变) +#define StatusBarHeight (UIApplication.sharedApplication.statusBarHidden ? 0 : UIApplication.sharedApplication.statusBarFrame.size.height) + +/// 状态栏高度(如果状态栏不可见,也会返回一个普通状态下可见的高度) +#define StatusBarHeightConstant [QMUIHelper statusBarHeightConstant] + +/// navigationBar 的静态高度 +#define NavigationBarHeight (IS_IPAD ? 50 : (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44)) + +/// 代表(导航栏+状态栏),这里用于获取其高度 +/// @warn 如果是用于 viewController,请使用 UIViewController(QMUI) qmui_navigationBarMaxYInViewCoordinator 代替 +#define NavigationContentTop (StatusBarHeight + 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) + +/// 同上,单独将 iPad 区分对待 +#define PreferredValueForDeviceIncludingiPad(_iPad, _65or61or55inch, _47or58inch, _40inch, _35inch) PreferredValueForAll(_iPad, _65or61or55inch, _65or61or55inch, _47or58inch, _65or61or55inch, _47or58inch, _40inch, _35inch) + +/// 若 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 - 方法-创建器 + +#define CGSizeMax CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) + +#define UIImageMake(img) [UIImage imageNamed:img] + +/// 使用文件名(不带后缀名,仅限png)创建一个UIImage对象,不会被系统缓存,用于不被复用的图片,特别是大图 +#define UIImageMakeWithFile(name) UIImageMakeWithFileAndSuffix(name, @"png") +#define UIImageMakeWithFileAndSuffix(name, suffix) [UIImage imageWithContentsOfFile:[NSString stringWithFormat:@"%@/%@.%@", [[NSBundle mainBundle] resourcePath], name, suffix]] + +/// 字体相关的宏,用于快速创建一个字体对象,更多创建宏可查看 UIFont+QMUI.h +#define UIFontMake(size) [UIFont systemFontOfSize:size] +#define UIFontItalicMake(size) [UIFont italicSystemFontOfSize:size] /// 斜体只对数字和字母有效,中文无效 +#define UIFontBoldMake(size) [UIFont boldSystemFontOfSize:size] +#define UIFontBoldWithFont(_font) [UIFont boldSystemFontOfSize:_font.pointSize] + +/// UIColor 相关的宏,用于快速创建一个 UIColor 对象,更多创建的宏可查看 UIColor+QMUI.h +#define UIColorMake(r, g, b) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:1] +#define UIColorMakeWithRGBA(r, g, b, a) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:a/1.0] + + +#pragma mark - 数学计算 + +#define AngleWithDegrees(deg) (M_PI * (deg) / 180.0) + + +#pragma mark - 动画 + +#define QMUIViewAnimationOptionsCurveOut (7<<16) +#define QMUIViewAnimationOptionsCurveIn (8<<16) + +#pragma mark - 无障碍访问 +CG_INLINE void +AddAccessibilityLabel(NSObject *obj, NSString *label) { + obj.accessibilityLabel = label; +} + +CG_INLINE void +AddAccessibilityHint(NSObject *obj, NSString *hint) { + obj.accessibilityHint = hint; +} + + +#pragma mark - 其他 + +#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 fabs(floatValue) <= 0.001 ? 0 : floatValue; +} + +/** + * 基于指定的倍数,对传进来的 floatValue 进行像素取整。若指定倍数为0,则表示以当前设备的屏幕倍数为准。 + * + * 例如传进来 “2.1”,在 2x 倍数下会返回 2.5(0.5pt 对应 1px),在 3x 倍数下会返回 2.333(0.333pt 对应 1px)。 + */ +CG_INLINE CGFloat +flatSpecificScale(CGFloat floatValue, CGFloat scale) { + if (isinf(floatValue) || floatValue == CGFLOAT_MAX) return floatValue; + floatValue = removeFloatMin(floatValue); + scale = scale ?: ScreenScale; + // 这里因为浮点精度的问题,可能会出现一些偏差,例如 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; +} + +/** + * 基于当前设备的屏幕倍数,对传进来的 floatValue 进行像素取整。 + * + * 注意如果在 Core Graphic 绘图里使用时,要注意当前画布的倍数是否和设备屏幕倍数一致,若不一致,不可使用 flat() 函数,而应该用 flatSpecificScale + */ +CG_INLINE CGFloat +flat(CGFloat floatValue) { + return flatSpecificScale(floatValue, 0); +} + +/** + * 类似flat(),只不过 flat 是向上取整,而 floorInPixel 是向下取整 + */ +CG_INLINE CGFloat +floorInPixel(CGFloat floatValue) { + floatValue = removeFloatMin(floatValue); + CGFloat resultValue = floor(floatValue * ScreenScale) / ScreenScale; + return resultValue; +} + +CG_INLINE BOOL +between(CGFloat minimumValue, CGFloat value, CGFloat maximumValue) { + return minimumValue < value && value < maximumValue; +} + +CG_INLINE BOOL +betweenOrEqual(CGFloat minimumValue, CGFloat value, CGFloat maximumValue) { + return minimumValue <= value && value <= maximumValue; +} + +/** + * 调整给定的某个 CGFloat 值的小数点精度,超过精度的部分按四舍五入处理。 + * + * 例如 CGFloatToFixed(0.3333, 2) 会返回 0.33,而 CGFloatToFixed(0.6666, 2) 会返回 0.67 + * + * @warning 参数类型为 CGFloat,也即意味着不管传进来的是 float 还是 double 最终都会被强制转换成 CGFloat 再做计算 + * @warning 该方法无法解决浮点数精度运算的问题,如需做浮点数的 == 判断,可用下方的 CGFloatEqualToFloat() + */ +CG_INLINE CGFloat +CGFloatToFixed(CGFloat value, NSUInteger precision) { + NSString *formatString = [NSString stringWithFormat:@"%%.%@f", @(precision)]; + NSString *toString = [NSString stringWithFormat:formatString, value]; + #if CGFLOAT_IS_DOUBLE + CGFloat result = [toString doubleValue]; + #else + CGFloat result = [toString floatValue]; + #endif + 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) { + return flat((parent - child) / 2.0); +} + +/// 检测某个数值如果为 NaN 则将其转换为 0,避免布局中出现 crash +CG_INLINE CGFloat +CGFloatSafeValue(CGFloat value) { + return isnan(value) ? 0 : value; +} + +#pragma mark - CGPoint + +/// 两个point相加 +CG_INLINE CGPoint +CGPointUnion(CGPoint point1, CGPoint point2) { + return CGPointMake(flat(point1.x + point2.x), flat(point1.y + point2.y)); +} + +/// 获取rect的center,包括rect本身的x/y偏移 +CG_INLINE CGPoint +CGPointGetCenterWithRect(CGRect rect) { + return CGPointMake(flat(CGRectGetMidX(rect)), flat(CGRectGetMidY(rect))); +} + +CG_INLINE CGPoint +CGPointGetCenterWithSize(CGSize size) { + return CGPointMake(flat(size.width / 2.0), flat(size.height / 2.0)); +} + +CG_INLINE CGPoint +CGPointToFixed(CGPoint point, NSUInteger precision) { + CGPoint result = CGPointMake(CGFloatToFixed(point.x, precision), CGFloatToFixed(point.y, precision)); + return result; +} + +CG_INLINE CGPoint +CGPointRemoveFloatMin(CGPoint point) { + CGPoint result = CGPointMake(removeFloatMin(point.x), removeFloatMin(point.y)); + return result; +} + +#pragma mark - UIEdgeInsets + +/// 获取UIEdgeInsets在水平方向上的值 +CG_INLINE CGFloat +UIEdgeInsetsGetHorizontalValue(UIEdgeInsets insets) { + return insets.left + insets.right; +} + +/// 获取UIEdgeInsets在垂直方向上的值 +CG_INLINE CGFloat +UIEdgeInsetsGetVerticalValue(UIEdgeInsets insets) { + return insets.top + insets.bottom; +} + +/// 将两个UIEdgeInsets合并为一个 +CG_INLINE UIEdgeInsets +UIEdgeInsetsConcat(UIEdgeInsets insets1, UIEdgeInsets insets2) { + insets1.top += insets2.top; + insets1.left += insets2.left; + insets1.bottom += insets2.bottom; + insets1.right += insets2.right; + return insets1; +} + +CG_INLINE UIEdgeInsets +UIEdgeInsetsSetTop(UIEdgeInsets insets, CGFloat top) { + insets.top = flat(top); + return insets; +} + +CG_INLINE UIEdgeInsets +UIEdgeInsetsSetLeft(UIEdgeInsets insets, CGFloat left) { + insets.left = flat(left); + return insets; +} +CG_INLINE UIEdgeInsets +UIEdgeInsetsSetBottom(UIEdgeInsets insets, CGFloat bottom) { + insets.bottom = flat(bottom); + return insets; +} + +CG_INLINE UIEdgeInsets +UIEdgeInsetsSetRight(UIEdgeInsets insets, CGFloat right) { + insets.right = flat(right); + return insets; +} + +CG_INLINE UIEdgeInsets +UIEdgeInsetsToFixed(UIEdgeInsets insets, NSUInteger precision) { + UIEdgeInsets result = UIEdgeInsetsMake(CGFloatToFixed(insets.top, precision), CGFloatToFixed(insets.left, precision), CGFloatToFixed(insets.bottom, precision), CGFloatToFixed(insets.right, precision)); + return result; +} + +CG_INLINE UIEdgeInsets +UIEdgeInsetsRemoveFloatMin(UIEdgeInsets insets) { + UIEdgeInsets result = UIEdgeInsetsMake(removeFloatMin(insets.top), removeFloatMin(insets.left), removeFloatMin(insets.bottom), removeFloatMin(insets.right)); + return result; +} + +#pragma mark - CGSize + +/// 判断一个 CGSize 是否存在 NaN +CG_INLINE BOOL +CGSizeIsNaN(CGSize size) { + return isnan(size.width) || isnan(size.height); +} + +/// 判断一个 CGSize 是否存在 infinite +CG_INLINE BOOL +CGSizeIsInf(CGSize size) { + return isinf(size.width) || isinf(size.height); +} + +/// 判断一个 CGSize 是否为空(宽或高为0) +CG_INLINE BOOL +CGSizeIsEmpty(CGSize size) { + return size.width <= 0 || size.height <= 0; +} + +/// 判断一个 CGSize 是否合法(例如不带无穷大的值、不带非法数字) +CG_INLINE BOOL +CGSizeIsValidated(CGSize size) { + return !CGSizeIsEmpty(size) && !CGSizeIsInf(size) && !CGSizeIsNaN(size); +} + +/// 将一个 CGSize 像素对齐 +CG_INLINE CGSize +CGSizeFlatted(CGSize size) { + return CGSizeMake(flat(size.width), flat(size.height)); +} + +/// 将一个 CGSize 以 pt 为单位向上取整 +CG_INLINE CGSize +CGSizeCeil(CGSize size) { + return CGSizeMake(ceil(size.width), ceil(size.height)); +} + +/// 将一个 CGSize 以 pt 为单位向下取整 +CG_INLINE CGSize +CGSizeFloor(CGSize size) { + return CGSizeMake(floor(size.width), floor(size.height)); +} + +CG_INLINE CGSize +CGSizeToFixed(CGSize size, NSUInteger precision) { + CGSize result = CGSizeMake(CGFloatToFixed(size.width, precision), CGFloatToFixed(size.height, precision)); + return result; +} + +CG_INLINE CGSize +CGSizeRemoveFloatMin(CGSize size) { + CGSize result = CGSizeMake(removeFloatMin(size.width), removeFloatMin(size.height)); + return result; +} + +#pragma mark - CGRect + +/// 判断一个 CGRect 是否存在 NaN +CG_INLINE BOOL +CGRectIsNaN(CGRect rect) { + return isnan(rect.origin.x) || isnan(rect.origin.y) || isnan(rect.size.width) || isnan(rect.size.height); +} + +/// 系统提供的 CGRectIsInfinite 接口只能判断 CGRectInfinite 的情况,而该接口可以用于判断 INFINITY 的值 +CG_INLINE BOOL +CGRectIsInf(CGRect rect) { + return isinf(rect.origin.x) || isinf(rect.origin.y) || isinf(rect.size.width) || isinf(rect.size.height); +} + +/// 判断一个 CGRect 是否合法(例如不带无穷大的值、不带非法数字) +CG_INLINE BOOL +CGRectIsValidated(CGRect rect) { + return !CGRectIsNull(rect) && !CGRectIsInfinite(rect) && !CGRectIsNaN(rect) && !CGRectIsInf(rect); +} + +/// 检测某个 CGRect 如果存在数值为 NaN 的则将其转换为 0,避免布局中出现 crash +CG_INLINE CGRect +CGRectSafeValue(CGRect rect) { + return CGRectMake(CGFloatSafeValue(CGRectGetMinX(rect)), CGFloatSafeValue(CGRectGetMinY(rect)), CGFloatSafeValue(CGRectGetWidth(rect)), CGFloatSafeValue(CGRectGetHeight(rect))); +} + +/// 创建一个像素对齐的CGRect +CG_INLINE CGRect +CGRectFlatMake(CGFloat x, CGFloat y, CGFloat width, CGFloat height) { + return CGRectMake(flat(x), flat(y), flat(width), flat(height)); +} + +/// 对CGRect的x/y、width/height都调用一次flat,以保证像素对齐 +CG_INLINE CGRect +CGRectFlatted(CGRect rect) { + return CGRectMake(flat(rect.origin.x), flat(rect.origin.y), flat(rect.size.width), flat(rect.size.height)); +} + +/// 计算目标点 targetPoint 围绕坐标点 coordinatePoint 通过 transform 之后此点的坐标 +CG_INLINE CGPoint +CGPointApplyAffineTransformWithCoordinatePoint(CGPoint coordinatePoint, CGPoint targetPoint, CGAffineTransform t) { + CGPoint p; + p.x = (targetPoint.x - coordinatePoint.x) * t.a + (targetPoint.y - coordinatePoint.y) * t.c + coordinatePoint.x; + p.y = (targetPoint.x - coordinatePoint.x) * t.b + (targetPoint.y - coordinatePoint.y) * t.d + coordinatePoint.y; + p.x += t.tx; + p.y += t.ty; + return p; +} + +/// 系统的 CGRectApplyAffineTransform 只会按照 anchorPoint 为 (0, 0) 的方式去计算,但通常情况下我们面对的是 UIView/CALayer,它们默认的 anchorPoint 为 (.5, .5),所以增加这个函数,在计算 transform 时可以考虑上 anchorPoint 的影响 +CG_INLINE CGRect +CGRectApplyAffineTransformWithAnchorPoint(CGRect rect, CGAffineTransform t, CGPoint anchorPoint) { + CGFloat width = CGRectGetWidth(rect); + CGFloat height = CGRectGetHeight(rect); + CGPoint oPoint = CGPointMake(rect.origin.x + width * anchorPoint.x, rect.origin.y + height * anchorPoint.y); + CGPoint top_left = CGPointApplyAffineTransformWithCoordinatePoint(oPoint, CGPointMake(rect.origin.x, rect.origin.y), t); + CGPoint bottom_left = CGPointApplyAffineTransformWithCoordinatePoint(oPoint, CGPointMake(rect.origin.x, rect.origin.y + height), t); + CGPoint top_right = CGPointApplyAffineTransformWithCoordinatePoint(oPoint, CGPointMake(rect.origin.x + width, rect.origin.y), t); + CGPoint bottom_right = CGPointApplyAffineTransformWithCoordinatePoint(oPoint, CGPointMake(rect.origin.x + width, rect.origin.y + height), t); + CGFloat minX = MIN(MIN(MIN(top_left.x, bottom_left.x), top_right.x), bottom_right.x); + CGFloat maxX = MAX(MAX(MAX(top_left.x, bottom_left.x), top_right.x), bottom_right.x); + CGFloat minY = MIN(MIN(MIN(top_left.y, bottom_left.y), top_right.y), bottom_right.y); + CGFloat maxY = MAX(MAX(MAX(top_left.y, bottom_left.y), top_right.y), bottom_right.y); + CGFloat newWidth = maxX - minX; + CGFloat newHeight = maxY - minY; + CGRect result = CGRectMake(minX, minY, newWidth, newHeight); + return result; +} + +/// 为一个CGRect叠加scale计算 +CG_INLINE CGRect +CGRectApplyScale(CGRect rect, CGFloat scale) { + return CGRectFlatted(CGRectMake(CGRectGetMinX(rect) * scale, CGRectGetMinY(rect) * scale, CGRectGetWidth(rect) * scale, CGRectGetHeight(rect) * scale)); +} + +/// 计算view的水平居中,传入父view和子view的frame,返回子view在水平居中时的x值 +CG_INLINE CGFloat +CGRectGetMinXHorizontallyCenterInParentRect(CGRect parentRect, CGRect childRect) { + return flat((CGRectGetWidth(parentRect) - CGRectGetWidth(childRect)) / 2.0); +} + +/// 计算view的垂直居中,传入父view和子view的frame,返回子view在垂直居中时的y值 +CG_INLINE CGFloat +CGRectGetMinYVerticallyCenterInParentRect(CGRect parentRect, CGRect childRect) { + return flat((CGRectGetHeight(parentRect) - CGRectGetHeight(childRect)) / 2.0); +} + +/// 返回值:同一个坐标系内,想要layoutingRect和已布局完成的referenceRect保持垂直居中时,layoutingRect的originY +CG_INLINE CGFloat +CGRectGetMinYVerticallyCenter(CGRect referenceRect, CGRect layoutingRect) { + return CGRectGetMinY(referenceRect) + CGRectGetMinYVerticallyCenterInParentRect(referenceRect, layoutingRect); +} + +/// 返回值:同一个坐标系内,想要layoutingRect和已布局完成的referenceRect保持水平居中时,layoutingRect的originX +CG_INLINE CGFloat +CGRectGetMinXHorizontallyCenter(CGRect referenceRect, CGRect layoutingRect) { + return CGRectGetMinX(referenceRect) + CGRectGetMinXHorizontallyCenterInParentRect(referenceRect, layoutingRect); +} + +/// 为给定的rect往内部缩小insets的大小(系统那个方法的命名太难联想了,所以定义了一个新函数) +CG_INLINE CGRect +CGRectInsetEdges(CGRect rect, UIEdgeInsets insets) { + return UIEdgeInsetsInsetRect(rect, insets); +} + +/// 传入size,返回一个x/y为0的CGRect +CG_INLINE CGRect +CGRectMakeWithSize(CGSize size) { + return CGRectMake(0, 0, size.width, size.height); +} + +CG_INLINE CGRect +CGRectFloatTop(CGRect rect, CGFloat top) { + rect.origin.y = top; + return rect; +} + +CG_INLINE CGRect +CGRectFloatBottom(CGRect rect, CGFloat bottom) { + rect.origin.y = bottom - CGRectGetHeight(rect); + return rect; +} + +CG_INLINE CGRect +CGRectFloatRight(CGRect rect, CGFloat right) { + rect.origin.x = right - CGRectGetWidth(rect); + return rect; +} + +CG_INLINE CGRect +CGRectFloatLeft(CGRect rect, CGFloat left) { + rect.origin.x = left; + return rect; +} + +/// 保持rect的左边缘不变,改变其宽度,使右边缘靠在right上 +CG_INLINE CGRect +CGRectLimitRight(CGRect rect, CGFloat rightLimit) { + rect.size.width = rightLimit - rect.origin.x; + return rect; +} + +/// 保持rect右边缘不变,改变其宽度和origin.x,使其左边缘靠在left上。只适合那种右边缘不动的view +/// 先改变origin.x,让其靠在offset上 +/// 再改变size.width,减少同样的宽度,以抵消改变origin.x带来的view移动,从而保证view的右边缘是不动的 +CG_INLINE CGRect +CGRectLimitLeft(CGRect rect, CGFloat leftLimit) { + CGFloat subOffset = leftLimit - rect.origin.x; + rect.origin.x = leftLimit; + rect.size.width = rect.size.width - subOffset; + return rect; +} + +/// 限制rect的宽度,超过最大宽度则截断,否则保持rect的宽度不变 +CG_INLINE CGRect +CGRectLimitMaxWidth(CGRect rect, CGFloat maxWidth) { + CGFloat width = CGRectGetWidth(rect); + rect.size.width = width > maxWidth ? maxWidth : width; + return rect; +} + +CG_INLINE CGRect +CGRectSetX(CGRect rect, CGFloat x) { + rect.origin.x = flat(x); + return rect; +} + +CG_INLINE CGRect +CGRectSetY(CGRect rect, CGFloat y) { + rect.origin.y = flat(y); + return rect; +} + +CG_INLINE CGRect +CGRectSetXY(CGRect rect, CGFloat x, CGFloat y) { + rect.origin.x = flat(x); + rect.origin.y = flat(y); + return rect; +} + +CG_INLINE CGRect +CGRectSetWidth(CGRect rect, CGFloat width) { + if (width < 0) { + return rect; + } + rect.size.width = flat(width); + return rect; +} + +CG_INLINE CGRect +CGRectSetHeight(CGRect rect, CGFloat height) { + if (height < 0) { + return rect; + } + rect.size.height = flat(height); + return rect; +} + +CG_INLINE CGRect +CGRectSetSize(CGRect rect, CGSize size) { + rect.size = CGSizeFlatted(size); + return rect; +} + +CG_INLINE CGRect +CGRectToFixed(CGRect rect, NSUInteger precision) { + CGRect result = CGRectMake(CGFloatToFixed(CGRectGetMinX(rect), precision), + CGFloatToFixed(CGRectGetMinY(rect), precision), + CGFloatToFixed(CGRectGetWidth(rect), precision), + CGFloatToFixed(CGRectGetHeight(rect), precision)); + return result; +} + +CG_INLINE CGRect +CGRectRemoveFloatMin(CGRect rect) { + CGRect result = CGRectMake(removeFloatMin(CGRectGetMinX(rect)), + removeFloatMin(CGRectGetMinY(rect)), + removeFloatMin(CGRectGetWidth(rect)), + removeFloatMin(CGRectGetHeight(rect))); + return result; +} + +/// outerRange 是否包含了 innerRange +CG_INLINE BOOL +NSContainingRanges(NSRange outerRange, NSRange innerRange) { + if (innerRange.location >= outerRange.location && outerRange.location + outerRange.length >= innerRange.location + innerRange.length) { + return YES; + } + return NO; +} + +#endif /* QMUICommonDefines_h */ diff --git a/QMUI/QMUIKit/QMUICore/QMUIConfiguration.h b/QMUI/QMUIKit/QMUICore/QMUIConfiguration.h new file mode 100644 index 00000000..872cee9a --- /dev/null +++ b/QMUI/QMUIKit/QMUICore/QMUIConfiguration.h @@ -0,0 +1,322 @@ +/** + * 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. + */ + +// +// QMUIConfiguration.h +// qmui +// +// Created by QMUI Team on 15/3/29. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/// 所有配置表都应该实现的 protocol +/// All configuration templates should implement this protocal +@protocol QMUIConfigurationTemplateProtocol + +@required +/// 应用配置表的设置 +/// Applies configurations +- (void)applyConfigurationTemplate; + +@optional +/// 当返回 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; + +@end + +/** + * 维护项目全局 UI 配置的单例,通过业务项目自己的 QMUIConfigurationTemplate 来为这个单例赋值,而业务代码里则通过 QMUIConfigurationMacros.h 文件里的宏来使用这些值。 + * A singleton that contains various UI configurations. Use `QMUIConfigurationTemplate` to set values; Use macros in `QMUIConfigurationMacros.h` to get values. + */ +@interface QMUIConfiguration : NSObject + +/// 标志当前项目是否有使用配置表功能 +@property(nonatomic, assign, readonly) BOOL active; + +#pragma mark - Global Color + +@property(nonatomic, strong) UIColor *clearColor; +@property(nonatomic, strong) UIColor *whiteColor; +@property(nonatomic, strong) UIColor *blackColor; +@property(nonatomic, strong) UIColor *grayColor; +@property(nonatomic, strong) UIColor *grayDarkenColor; +@property(nonatomic, strong) UIColor *grayLightenColor; +@property(nonatomic, strong) UIColor *redColor; +@property(nonatomic, strong) UIColor *greenColor; +@property(nonatomic, strong) UIColor *blueColor; +@property(nonatomic, strong) UIColor *yellowColor; + +@property(nonatomic, strong) UIColor *linkColor; +@property(nonatomic, strong) UIColor *disabledColor; +@property(nonatomic, strong, nullable) UIColor *backgroundColor; +@property(nonatomic, strong) UIColor *maskDarkColor; +@property(nonatomic, strong) UIColor *maskLightColor; +@property(nonatomic, strong) UIColor *separatorColor; +@property(nonatomic, strong) UIColor *separatorDashedColor; +@property(nonatomic, strong) UIColor *placeholderColor; + +@property(nonatomic, strong) UIColor *testColorRed; +@property(nonatomic, strong) UIColor *testColorGreen; +@property(nonatomic, strong) UIColor *testColorBlue; + +#pragma mark - UIControl + +@property(nonatomic, assign) CGFloat controlHighlightedAlpha; +@property(nonatomic, assign) CGFloat controlDisabledAlpha; + +#pragma mark - UIButton + +@property(nonatomic, assign) CGFloat buttonHighlightedAlpha; +@property(nonatomic, assign) CGFloat buttonDisabledAlpha; +@property(nonatomic, strong, nullable) UIColor *buttonTintColor; + +#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; +@property(nonatomic, strong, nullable) UIColor *navBarLargeTitleColor; +@property(nonatomic, strong, nullable) UIFont *navBarLargeTitleFont; +@property(nonatomic, assign) UIOffset navBarBackButtonTitlePositionAdjustment; +@property(nonatomic, assign) BOOL sizeNavBarBackIndicatorImageAutomatically; +@property(nonatomic, strong, nullable) UIImage *navBarBackIndicatorImage; +@property(nonatomic, strong) UIImage *navBarCloseButtonImage; + +@property(nonatomic, assign) CGFloat navBarLoadingMarginRight; +@property(nonatomic, assign) CGFloat navBarAccessoryViewMarginLeft; +@property(nonatomic, assign) UIActivityIndicatorViewStyle navBarActivityIndicatorViewStyle; +@property(nonatomic, strong) UIImage *navBarAccessoryViewTypeDisclosureIndicatorImage; + +#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, 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) 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) UIImage *searchBarTextFieldBackgroundImage; +@property(nonatomic, strong, nullable) UIColor *searchBarTextFieldBorderColor; +@property(nonatomic, strong, nullable) UIImage *searchBarBackgroundImage; +@property(nonatomic, strong, nullable) UIColor *searchBarTintColor; +@property(nonatomic, strong, nullable) UIColor *searchBarTextColor; +@property(nonatomic, strong, nullable) UIColor *searchBarPlaceholderColor; +@property(nonatomic, strong, nullable) UIFont *searchBarFont; +/// 搜索框放大镜icon的图片,大小必须为14x14pt,否则会失真(系统的限制) +/// The magnifier icon in search bar. Size must be 14 x 14pt to avoid being distorted. +@property(nonatomic, strong, nullable) UIImage *searchBarSearchIconImage; +@property(nonatomic, strong, nullable) UIImage *searchBarClearIconImage; +@property(nonatomic, assign) CGFloat searchBarTextFieldCornerRadius; + +#pragma mark - TableView / TableViewCell + +@property(nonatomic, assign) BOOL tableViewEstimatedHeightEnabled; + +@property(nonatomic, strong, nullable) UIColor *tableViewBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableSectionIndexColor; +@property(nonatomic, strong, nullable) UIColor *tableSectionIndexBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableSectionIndexTrackingBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableViewSeparatorColor; + +@property(nonatomic, assign) CGFloat tableViewCellNormalHeight; +@property(nonatomic, strong, nullable) UIColor *tableViewCellTitleLabelColor; +@property(nonatomic, strong, nullable) UIColor *tableViewCellDetailLabelColor; +@property(nonatomic, strong, nullable) UIColor *tableViewCellBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableViewCellSelectedBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableViewCellWarningBackgroundColor; +@property(nonatomic, strong, nullable) UIImage *tableViewCellDisclosureIndicatorImage; +@property(nonatomic, strong, nullable) UIImage *tableViewCellCheckmarkImage; +@property(nonatomic, strong, nullable) UIImage *tableViewCellDetailButtonImage; +@property(nonatomic, assign) CGFloat tableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator; + +@property(nonatomic, strong, nullable) UIColor *tableViewSectionHeaderBackgroundColor; +@property(nonatomic, strong, nullable) UIColor *tableViewSectionFooterBackgroundColor; +@property(nonatomic, strong, nullable) UIFont *tableViewSectionHeaderFont; +@property(nonatomic, strong, nullable) UIFont *tableViewSectionFooterFont; +@property(nonatomic, strong, nullable) UIColor *tableViewSectionHeaderTextColor; +@property(nonatomic, strong, nullable) UIColor *tableViewSectionFooterTextColor; +@property(nonatomic, assign) UIEdgeInsets tableViewSectionHeaderAccessoryMargins; +@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; +@property(nonatomic, strong, nullable) UIColor *tableViewGroupedSectionFooterTextColor; +@property(nonatomic, assign) UIEdgeInsets tableViewGroupedSectionHeaderAccessoryMargins; +@property(nonatomic, assign) UIEdgeInsets tableViewGroupedSectionFooterAccessoryMargins; +@property(nonatomic, assign) CGFloat tableViewGroupedSectionHeaderDefaultHeight; +@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 badgeOffset; +@property(nonatomic, assign) CGPoint badgeOffsetLandscape; + +@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) 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 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 ++ (instancetype _Nullable )sharedInstance; +- (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 new file mode 100644 index 00000000..ef74deb8 --- /dev/null +++ b/QMUI/QMUIKit/QMUICore/QMUIConfiguration.m @@ -0,0 +1,1084 @@ +/** + * 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. + */ + +// +// QMUIConfiguration.m +// qmui +// +// Created by QMUI Team on 15/3/29. +// + +#import "QMUIConfiguration.h" +#import "QMUICore.h" +#import "UIImage+QMUI.h" +#import "NSString+QMUI.h" +#import "UIViewController+QMUI.h" +#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 { + static dispatch_once_t pred; + static QMUIConfiguration *sharedInstance; + dispatch_once(&pred, ^{ + sharedInstance = [[QMUIConfiguration alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + [self initDefaultConfiguration]; + } + return self; +} + +static BOOL QMUI_hasAppliedInitialTemplate; +- (void)applyInitialTemplate { + // XCTest 无法加载配置表,因此没有寻找 classes 的必要 + // https://github.com/Tencent/QMUI_iOS/issues/1312 + if (QMUI_hasAppliedInitialTemplate || IS_XCTEST) { + return; + } + + // 自动寻找并应用模板 + // Automatically look for templates and apply them + // @see https://github.com/Tencent/QMUI_iOS/issues/264 + Protocol *protocol = @protocol(QMUIConfigurationTemplateProtocol); + 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; + } + } + } + } + + if (classes) free(classes); + + QMUI_hasAppliedInitialTemplate = YES; +} + +#pragma mark - Initialize default values + +- (void)initDefaultConfiguration { + + #pragma mark - Global Color + + 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); + self.redColor = UIColorMake(250, 58, 58); + self.greenColor = UIColorMake(159, 214, 97); + self.blueColor = UIColorMake(49, 189, 243); + self.yellowColor = UIColorMake(255, 207, 71); + + self.linkColor = UIColorMake(56, 116, 171); + self.disabledColor = self.grayColor; + self.maskDarkColor = UIColorMakeWithRGBA(0, 0, 0, .35f); + self.maskLightColor = UIColorMakeWithRGBA(255, 255, 255, .5f); + self.separatorColor = UIColorMake(222, 224, 226); + self.separatorDashedColor = UIColorMake(17, 17, 17); + self.placeholderColor = UIColorMake(196, 200, 208); + + self.testColorRed = UIColorMakeWithRGBA(255, 0, 0, .3); + self.testColorGreen = UIColorMakeWithRGBA(0, 255, 0, .3); + self.testColorBlue = UIColorMakeWithRGBA(0, 0, 255, .3); + + #pragma mark - UIControl + + self.controlHighlightedAlpha = 0.5f; + self.controlDisabledAlpha = 0.5f; + + #pragma mark - UIButton + + self.buttonHighlightedAlpha = self.controlHighlightedAlpha; + self.buttonDisabledAlpha = self.controlDisabledAlpha; + self.buttonTintColor = self.blueColor; + + #pragma mark - UITextField & UITextView + + self.textFieldTextInsets = UIEdgeInsetsMake(0, 7, 0, 7); + + #pragma mark - NavigationBar + + self.navBarHighlightedAlpha = 0.2f; + self.navBarDisabledAlpha = 0.2f; + self.sizeNavBarBackIndicatorImageAutomatically = YES; + self.navBarLoadingMarginRight = 3; + self.navBarAccessoryViewMarginLeft = 5; + self.navBarActivityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; + + // XCTest 会在 dispatch_once 里访问 UIScreen 引发死锁,所以屏蔽掉 + // https://github.com/Tencent/QMUI_iOS/issues/1479 + if (!IS_XCTEST) { + self.navBarCloseButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavClose size:CGSizeMake(16, 16) tintColor:self.navBarTintColor]; + self.navBarAccessoryViewTypeDisclosureIndicatorImage = [[UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:self.navBarTitleColor] qmui_imageWithOrientation:UIImageOrientationDown]; + } + + + #pragma mark - Toolbar + + self.toolBarHighlightedAlpha = 0.4f; + self.toolBarDisabledAlpha = 0.4f; + + #pragma mark - SearchBar + + self.searchBarPlaceholderColor = self.placeholderColor; + self.searchBarTextFieldCornerRadius = 2.0; + + #pragma mark - TableView / TableViewCell + + self.tableViewEstimatedHeightEnabled = YES; + + self.tableViewSeparatorColor = self.separatorColor; + + self.tableViewCellNormalHeight = UITableViewAutomaticDimension; + self.tableViewCellSelectedBackgroundColor = UIColorMake(238, 239, 241); + self.tableViewCellWarningBackgroundColor = self.yellowColor; + self.tableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator = 12; + + self.tableViewSectionHeaderBackgroundColor = UIColorMake(244, 244, 244); + self.tableViewSectionFooterBackgroundColor = UIColorMake(244, 244, 244); + self.tableViewSectionHeaderFont = UIFontBoldMake(12); + self.tableViewSectionFooterFont = UIFontBoldMake(12); + self.tableViewSectionHeaderTextColor = self.grayDarkenColor; + self.tableViewSectionFooterTextColor = self.grayColor; + self.tableViewSectionHeaderAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); + 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; + self.tableViewGroupedSectionFooterTextColor = self.grayColor; + self.tableViewGroupedSectionHeaderAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); + self.tableViewGroupedSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); + self.tableViewGroupedSectionHeaderDefaultHeight = UITableViewAutomaticDimension; + 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.supportedOrientationMask = UIInterfaceOrientationMaskAll; + self.needsBackBarButtonItemTitle = YES; + self.preventConcurrentNavigationControllerTransitions = 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 { + [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; + // 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 { + [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 { + [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 { + [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 { + [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 { + [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 { + 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 { + [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 { + [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 { + 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 { + _sizeNavBarBackIndicatorImageAutomatically = sizeNavBarBackIndicatorImageAutomatically; + if (sizeNavBarBackIndicatorImageAutomatically && self.navBarBackIndicatorImage && !CGSizeEqualToSize(self.navBarBackIndicatorImage.size, kUINavigationBarBackIndicatorImageSize)) { + self.navBarBackIndicatorImage = self.navBarBackIndicatorImage;// 重新设置一次,以触发自动调整大小 + } +} + +- (void)setNavBarBackIndicatorImage:(UIImage *)navBarBackIndicatorImage { + [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 (navBarBackIndicatorImage && self.sizeNavBarBackIndicatorImageAutomatically) { + CGSize systemBackIndicatorImageSize = kUINavigationBarBackIndicatorImageSize; + CGSize customBackIndicatorImageSize = _navBarBackIndicatorImage.size; + if (!CGSizeEqualToSize(customBackIndicatorImageSize, systemBackIndicatorImageSize)) { + CGFloat imageExtensionVerticalFloat = CGFloatGetCenter(systemBackIndicatorImageSize.height, customBackIndicatorImageSize.height); + _navBarBackIndicatorImage = [[_navBarBackIndicatorImage qmui_imageWithSpacingExtensionInsets:UIEdgeInsetsMake(imageExtensionVerticalFloat, + 0, + imageExtensionVerticalFloat, + 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; + } + +#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 { + [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; + // 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 { + [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 { + [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 { + [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)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 { + [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 { + [QMUIConfiguration performAction:^{ + _tabBarBackgroundImage = tabBarBackgroundImage; + self.tabBarAppearance.backgroundImage = tabBarBackgroundImage; + [self updateTabBarAppearance]; + } ifValueChanged:_tabBarBackgroundImage newValue:tabBarBackgroundImage]; +} + +- (void)setTabBarShadowImageColor:(UIColor *)tabBarShadowImageColor { + [QMUIConfiguration performAction:^{ + _tabBarShadowImageColor = tabBarShadowImageColor; + self.tabBarAppearance.shadowColor = tabBarShadowImageColor; + [self updateTabBarAppearance]; + } ifValueChanged:_tabBarShadowImageColor newValue:tabBarShadowImageColor]; +} + +- (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)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 { + [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 new file mode 100644 index 00000000..2ecfa753 --- /dev/null +++ b/QMUI/QMUIKit/QMUICore/QMUIConfigurationMacros.h @@ -0,0 +1,274 @@ +/** + * 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. + */ + +// +// QMUIConfigurationMacros.h +// qmui +// +// Created by QMUI Team on 14-7-2. +// + +#import "QMUIConfiguration.h" + + +/** + * 提供一系列方便书写的宏,以便在代码里读取配置表的各种属性。 + * @warning 请不要在 + load 方法里调用 QMUIConfigurationTemplate 或 QMUIConfigurationMacros 提供的宏,那个时机太早,可能导致 crash + * @waining 维护时,如果需要增加一个宏,则需要定义一个新的 QMUIConfiguration 属性。 + */ + + +// 单例的宏 + +#define QMUICMI ({[[QMUIConfiguration sharedInstance] applyInitialTemplate];[QMUIConfiguration sharedInstance];}) + +/// 标志当前项目是否正使用配置表功能 +#define QMUICMIActivated [QMUICMI active] + +#pragma mark - Global Color + +// 基础颜色 +#define UIColorClear [QMUICMI clearColor] +#define UIColorWhite [QMUICMI whiteColor] +#define UIColorBlack [QMUICMI blackColor] +#define UIColorGray [QMUICMI grayColor] +#define UIColorGrayDarken [QMUICMI grayDarkenColor] +#define UIColorGrayLighten [QMUICMI grayLightenColor] +#define UIColorRed [QMUICMI redColor] +#define UIColorGreen [QMUICMI greenColor] +#define UIColorBlue [QMUICMI blueColor] +#define UIColorYellow [QMUICMI yellowColor] + +// 功能颜色 +#define UIColorLink [QMUICMI linkColor] // 全局统一文字链接颜色 +#define UIColorDisabled [QMUICMI disabledColor] // 全局统一文字disabled颜色 +#define UIColorForBackground [QMUICMI backgroundColor] // 全局统一的背景色 +#define UIColorMask [QMUICMI maskDarkColor] // 全局统一的mask背景色 +#define UIColorMaskWhite [QMUICMI maskLightColor] // 全局统一的mask背景色,白色 +#define UIColorSeparator [QMUICMI separatorColor] // 全局分隔线颜色 +#define UIColorSeparatorDashed [QMUICMI separatorDashedColor] // 全局分隔线颜色(虚线) +#define UIColorPlaceholder [QMUICMI placeholderColor] // 全局的输入框的placeholder颜色 + +// 测试用的颜色 +#define UIColorTestRed [QMUICMI testColorRed] +#define UIColorTestGreen [QMUICMI testColorGreen] +#define UIColorTestBlue [QMUICMI testColorBlue] + +// 可操作的控件 +#pragma mark - UIControl + +#define UIControlHighlightedAlpha [QMUICMI controlHighlightedAlpha] // 一般control的Highlighted透明值 +#define UIControlDisabledAlpha [QMUICMI controlDisabledAlpha] // 一般control的Disable透明值 + +// 按钮 +#pragma mark - UIButton +#define ButtonHighlightedAlpha [QMUICMI buttonHighlightedAlpha] // 按钮Highlighted状态的透明度 +#define ButtonDisabledAlpha [QMUICMI buttonDisabledAlpha] // 按钮Disabled状态的透明度 +#define ButtonTintColor [QMUICMI buttonTintColor] // 普通按钮的颜色 + +#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] +#define NavBarLargeTitleColor [QMUICMI navBarLargeTitleColor] +#define NavBarLargeTitleFont [QMUICMI navBarLargeTitleFont] +#define NavBarBarBackButtonTitlePositionAdjustment [QMUICMI navBarBackButtonTitlePositionAdjustment] +#define NavBarBackIndicatorImage [QMUICMI navBarBackIndicatorImage] +#define SizeNavBarBackIndicatorImageAutomatically [QMUICMI sizeNavBarBackIndicatorImageAutomatically] +#define NavBarCloseButtonImage [QMUICMI navBarCloseButtonImage] + +#define NavBarLoadingMarginRight [QMUICMI navBarLoadingMarginRight] // titleView里左边的loading的右边距 +#define NavBarAccessoryViewMarginLeft [QMUICMI navBarAccessoryViewMarginLeft] // titleView里的accessoryView的左边距 +#define NavBarActivityIndicatorViewStyle [QMUICMI navBarActivityIndicatorViewStyle] // titleView loading 的style +#define NavBarAccessoryViewTypeDisclosureIndicatorImage [QMUICMI navBarAccessoryViewTypeDisclosureIndicatorImage] // titleView上倒三角的默认图片 + + +#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 TabBarStyle [QMUICMI tabBarStyle] +#define TabBarItemTitleFont [QMUICMI tabBarItemTitleFont] +#define TabBarItemTitleFontSelected [QMUICMI tabBarItemTitleFontSelected] +#define TabBarItemTitleColor [QMUICMI tabBarItemTitleColor] +#define TabBarItemTitleColorSelected [QMUICMI tabBarItemTitleColorSelected] +#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 SearchBarTextFieldBorderColor [QMUICMI searchBarTextFieldBorderColor] +#define SearchBarTextFieldBackgroundImage [QMUICMI searchBarTextFieldBackgroundImage] +#define SearchBarBackgroundImage [QMUICMI searchBarBackgroundImage] +#define SearchBarTintColor [QMUICMI searchBarTintColor] +#define SearchBarTextColor [QMUICMI searchBarTextColor] +#define SearchBarPlaceholderColor [QMUICMI searchBarPlaceholderColor] +#define SearchBarFont [QMUICMI searchBarFont] +#define SearchBarSearchIconImage [QMUICMI searchBarSearchIconImage] +#define SearchBarClearIconImage [QMUICMI searchBarClearIconImage] +#define SearchBarTextFieldCornerRadius [QMUICMI searchBarTextFieldCornerRadius] + + +#pragma mark - TableView / TableViewCell + +#define TableViewEstimatedHeightEnabled [QMUICMI tableViewEstimatedHeightEnabled] // 是否要开启全局 UITableView 的 estimatedRow(Section/Footer)Height + +#define TableViewBackgroundColor [QMUICMI tableViewBackgroundColor] // 普通列表的背景色 +#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 TableViewCellDisclosureIndicatorImage [QMUICMI tableViewCellDisclosureIndicatorImage] // 列表 cell 右边的箭头图片 +#define TableViewCellCheckmarkImage [QMUICMI tableViewCellCheckmarkImage] // 列表 cell 右边的打钩checkmark +#define TableViewCellDetailButtonImage [QMUICMI tableViewCellDetailButtonImage] // 列表 cell 右边的 i 按钮 +#define TableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator [QMUICMI tableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator] // 列表 cell 右边的 i 按钮和向右箭头之间的间距(仅当两者都使用了自定义图片并且同时显示时才生效) + +#define TableViewSectionHeaderBackgroundColor [QMUICMI tableViewSectionHeaderBackgroundColor] +#define TableViewSectionFooterBackgroundColor [QMUICMI tableViewSectionFooterBackgroundColor] +#define TableViewSectionHeaderFont [QMUICMI tableViewSectionHeaderFont] +#define TableViewSectionFooterFont [QMUICMI tableViewSectionFooterFont] +#define TableViewSectionHeaderTextColor [QMUICMI tableViewSectionHeaderTextColor] +#define TableViewSectionFooterTextColor [QMUICMI tableViewSectionFooterTextColor] +#define TableViewSectionHeaderAccessoryMargins [QMUICMI tableViewSectionHeaderAccessoryMargins] +#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] +#define TableViewGroupedSectionFooterTextColor [QMUICMI tableViewGroupedSectionFooterTextColor] +#define TableViewGroupedSectionHeaderAccessoryMargins [QMUICMI tableViewGroupedSectionHeaderAccessoryMargins] +#define TableViewGroupedSectionFooterAccessoryMargins [QMUICMI tableViewGroupedSectionFooterAccessoryMargins] +#define TableViewGroupedSectionHeaderDefaultHeight [QMUICMI tableViewGroupedSectionHeaderDefaultHeight] +#define TableViewGroupedSectionFooterDefaultHeight [QMUICMI tableViewGroupedSectionFooterDefaultHeight] +#define TableViewGroupedSectionHeaderContentInset [QMUICMI tableViewGroupedSectionHeaderContentInset] +#define TableViewGroupedSectionFooterContentInset [QMUICMI tableViewGroupedSectionFooterContentInset] +#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 BadgeOffset [QMUICMI badgeOffset] +#define BadgeOffsetLandscape [QMUICMI badgeOffsetLandscape] + +#define UpdatesIndicatorColor [QMUICMI updatesIndicatorColor] +#define UpdatesIndicatorSize [QMUICMI updatesIndicatorSize] +#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(仅 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 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 new file mode 100644 index 00000000..08efa51b --- /dev/null +++ b/QMUI/QMUIKit/QMUICore/QMUICore.h @@ -0,0 +1,21 @@ +/** + * 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. + */ + +// +// QMUICore.h +// qmui +// +// Created by QMUI Team on 2017/5/17. +// + +#import "QMUIHelper.h" +#import "QMUICommonDefines.h" +#import "QMUIRuntime.h" +#import "QMUILab.h" +#import "QMUIConfiguration.h" +#import "QMUIConfigurationMacros.h" diff --git a/QMUI/QMUIKit/QMUICore/QMUIHelper.h b/QMUI/QMUIKit/QMUICore/QMUIHelper.h new file mode 100644 index 00000000..1129501b --- /dev/null +++ b/QMUI/QMUIKit/QMUICore/QMUIHelper.h @@ -0,0 +1,303 @@ +/** + * 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. + */ + +// +// QMUIHelper.h +// qmui +// +// Created by QMUI Team on 14/10/25. +// + +#import +#import +#import "QMUICommonDefines.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QMUIHelper : NSObject + ++ (instancetype)sharedInstance; + +/** + 用一个 identifier 标记某一段 block,使其对应该 identifier 只会被运行一次 + @param block 要执行的一段逻辑 + @param identifier 唯一的标记,建议在 identifier 里添加当前这段业务的特有名称,例如用于 swizzle 的可以加“swizzled”前缀,以避免与其他业务共用同一个 identifier 引发 bug + */ ++ (BOOL)executeBlock:(void (NS_NOESCAPE ^)(void))block oncePerIdentifier:(NSString *)identifier; + +/** + 将 UIViewContentMode 转为对应的 CALayerContentsGravity + */ ++ (CALayerContentsGravity)layerContentsGravityWithContentMode:(UIViewContentMode)contentMode; +@end + +@interface QMUIHelper (Bundle) + +/// 获取 QMUIKit.framework Images.xcassets 内的图片资源 +/// @param name 图片名 ++ (nullable UIImage *)imageWithName:(NSString *)name; + +@end + +@interface QMUIHelper (SystemVersion) ++ (NSInteger)numbericOSVersion; ++ (NSComparisonResult)compareSystemVersion:(nonnull NSString *)currentVersion toVersion:(nonnull NSString *)targetVersion; ++ (BOOL)isCurrentSystemAtLeastVersion:(nonnull NSString *)targetVersion; ++ (BOOL)isCurrentSystemLowerThanVersion:(nonnull NSString *)targetVersion; +@end + +@interface QMUIHelper (DynamicType) + +/// 返回当前 contentSize 的 level,这个值可以在设置里面的“字体大小”查看,辅助功能里面有个“更大字体”可以设置更大的字体,不过这里我们这个接口将更大字体都做了统一,都返回“字体大小”里面最大值。 +/// Returns the level of contentSize +/// The value can be set in Settings - Display & Brightness - Text Size as well as in General - Accessibility - Larger Text +/// This method returns the value set by user or the maximum value in Text Size, whichever is smaller ++ (nonnull NSNumber *)preferredContentSizeLevel; + +/// 设置当前 cell 的高度,heights 是有七个数值的数组,对于不支持的iOS版本,则选择中间的值返回。 +/// Sets height of the cell; Heights consist of 7 numberic values; Returns the middle value on legacy iOS versions. ++ (CGFloat)heightForDynamicTypeCell:(nonnull NSArray *)heights; +@end + + +@interface QMUIHelper (Keyboard) + +/** + * 判断当前 App 里的键盘是否升起,默认为 NO + * Returns the visibility of the keybord. Default value is NO. + */ ++ (BOOL)isKeyboardVisible; + +/** + * 记录上一次键盘显示时的高度(基于整个 App 所在的 window 的坐标系),注意使用前用 `isKeyboardVisible` 判断键盘是否显示,因为即便是键盘被隐藏的情况下,调用 `lastKeyboardHeightInApplicationWindowWhenVisible` 也会得到高度值。 + */ ++ (CGFloat)lastKeyboardHeightInApplicationWindowWhenVisible; + +/** + * 获取当前键盘frame相关 + * @warning 注意iOS8以下的系统在横屏时得到的rect,宽度和高度相反了,所以不建议直接通过这个方法获取高度,而是使用keyboardHeightWithNotification:inView:,因为在后者的实现里会将键盘的rect转换坐标系,转换过程就会处理横竖屏旋转问题。 + */ ++ (CGRect)keyboardRectWithNotification:(nullable NSNotification *)notification; + +/// 获取当前键盘的高度,注意高度可能为0(例如第三方键盘会发出两次notification,其中第一次的高度就为0) ++ (CGFloat)keyboardHeightWithNotification:(nullable NSNotification *)notification; + +/** + * 获取当前键盘在屏幕上的可见高度,注意外接键盘(iPad那种)时,[QMUIHelper keyboardRectWithNotification]得到的键盘rect里有一部分是超出屏幕,不可见的,如果直接拿rect的高度来计算就会与意图相悖。 + * @param notification 接收到的键盘事件的UINotification对象 + * @param view 要得到的键盘高度是相对于哪个View的键盘高度,若为nil,则等同于调用[QMUIHelper keyboardHeightWithNotification:] + * @warning 如果view.window为空(当前View尚不可见),则会使用App默认的UIWindow来做坐标转换,可能会导致一些计算错误 + * @return 键盘在view里的可视高度 + */ ++ (CGFloat)keyboardHeightWithNotification:(nullable NSNotification *)notification inView:(nullable UIView *)view; + +/// 获取键盘显示/隐藏的动画时长,注意返回值可能为0 ++ (NSTimeInterval)keyboardAnimationDurationWithNotification:(nullable NSNotification *)notification; + +/// 获取键盘显示/隐藏的动画时间函数 ++ (UIViewAnimationCurve)keyboardAnimationCurveWithNotification:(nullable NSNotification *)notification; + +/// 获取键盘显示/隐藏的动画时间函数 ++ (UIViewAnimationOptions)keyboardAnimationOptionsWithNotification:(nullable NSNotification *)notification; +@end + + +@interface QMUIHelper (AudioSession) + +/** + * 听筒和扬声器的切换 + * + * @param speaker 是否转为扬声器,NO则听筒 + * @param temporary 决定使用kAudioSessionProperty_OverrideAudioRoute还是kAudioSessionProperty_OverrideCategoryDefaultToSpeaker,两者的区别请查看本组的博客文章:http://km.oa.com/group/gyui/articles/show/235957 + */ ++ (void)redirectAudioRouteWithSpeaker:(BOOL)speaker temporary:(BOOL)temporary; + +/** + * 设置category + * + * @param category 使用iOS7的category,iOS6的会自动适配 + */ ++ (void)setAudioSessionCategory:(nullable NSString *)category; +@end + +@interface QMUIHelper (UIGraphic) + +/// 获取一像素的大小 +@property(class, nonatomic, readonly) CGFloat pixelOne; + +/// 判断size是否超出范围 ++ (void)inspectContextSize:(CGSize)size; + +/// context是否合法 ++ (BOOL)inspectContextIfInvalidated:(CGContextRef)context; +@end + + +@interface QMUIHelper (Device) + +/// 如 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; + +@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 类型的设备 +/// @NEW_DEVICE_CHECKER +@property(class, nonatomic, readonly) BOOL isNotchedScreen; + +/// 将屏幕分为普通和紧凑两种,这个方法用于判断普通屏幕(也即大屏幕)。 +/// @note 注意,这里普通/紧凑的标准是 QMUI 自行制定的,与系统 UITraitCollection.horizontalSizeClass/verticalSizeClass 的值无关。只要是通常意义上的“大屏幕手机”(例如 Plus 系列)都会被视为 Regular Screen。 +/// @NEW_DEVICE_CHECKER +@property(class, nonatomic, readonly) BOOL isRegularScreen; + +/// iPhone 16 Pro Max +@property(class, nonatomic, readonly) BOOL is69InchScreen; + +/// iPhone 14 Pro Max +@property(class, nonatomic, readonly) BOOL is67InchScreenAndiPhone14Later; + +/// 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 +@property(class, nonatomic, readonly) BOOL is55InchScreen; + +/// iPhone 12 mini +@property(class, nonatomic, readonly) BOOL is54InchScreen; + +/// iPhone 8 +@property(class, nonatomic, readonly) BOOL is47InchScreen; + +/// iPhone 5 +@property(class, nonatomic, readonly) BOOL is40InchScreen; + +/// iPhone 4 +@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; + +/// 判断当前设备是否高性能设备,只会判断一次,以后都直接读取结果,所以没有性能问题 +@property(class, nonatomic, readonly) BOOL isHighPerformanceDevice; + +/// 系统设置里是否开启了“放大显示-试图-放大”,支持放大模式的 iPhone 设备可在官方文档中查询 https://support.apple.com/zh-cn/guide/iphone/iphd6804774e/ios +/// @NEW_DEVICE_CHECKER +@property(class, nonatomic, readonly) BOOL isZoomedMode; + +/// 当前设备是否拥有灵动岛 +/// @NEW_DEVICE_CHECKER +@property(class, nonatomic, readonly) BOOL isDynamicIslandDevice; + +/** + 在 iPad 分屏模式下可获得实际运行区域的窗口大小,如需适配 iPad 分屏,建议用这个方法来代替 [UIScreen mainScreen].bounds.size + @return 应用运行的窗口大小 + */ +@property(class, nonatomic, readonly) CGSize applicationSize; + +/** + 静态的状态栏高度,在状态栏不可见时也会根据机型返回状态栏的固定高度 + @NEW_DEVICE_CHECKER + */ +@property(class, nonatomic, readonly) CGFloat statusBarHeightConstant; + +/** + 静态的导航栏高度,在导航栏不可见时也会根据机型返回导航栏的固定高度 + */ +@property(class, nonatomic, readonly) CGFloat navigationBarMaxYConstant; + +@end + +@interface QMUIHelper (UIApplication) + +/** + * 把App的主要window置灰,用于浮层弹出时,请注意要在适当时机调用`resetDimmedApplicationWindow`恢复到正常状态 + */ ++ (void)dimmedApplicationWindow; + +/** + * 恢复对App的主要window的置灰操作,与`dimmedApplicationWindow`成对调用 + */ ++ (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 的系统动画操作。 + + @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 new file mode 100644 index 00000000..c313d23b --- /dev/null +++ b/QMUI/QMUIKit/QMUICore/QMUIHelper.m @@ -0,0 +1,1301 @@ +/** + * 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. + */ + +// +// QMUIHelper.m +// qmui +// +// Created by QMUI Team on 14/10/25. +// + +#import "QMUIHelper.h" +#import "QMUICore.h" +#import "NSNumber+QMUI.h" +#import "UIViewController+QMUI.h" +#import "NSString+QMUI.h" +#import "UIInterface+QMUI.h" +#import "NSObject+QMUI.h" +#import "NSArray+QMUI.h" +#import +#import +#import + +NSString *const kQMUIResourcesBundleName = @"QMUIResources"; + +@interface _QMUIPortraitViewController : UIViewController +@end + +@implementation _QMUIPortraitViewController + +- (BOOL)shouldAutorotate { + return NO; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + return UIInterfaceOrientationMaskPortrait; +} + +@end + +@interface QMUIHelper () + +@property(nonatomic, assign) BOOL shouldPreventAppearanceUpdating; +@end + +@implementation QMUIHelper (Bundle) + ++ (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; + } + UIImage *image = [UIImage imageNamed:name inBundle:resourceBundle compatibleWithTraitCollection:nil]; + return image; +} + +@end + + +@implementation QMUIHelper (DynamicType) + ++ (NSNumber *)preferredContentSizeLevel { + NSNumber *index = nil; + if ([UIApplication instancesRespondToSelector:@selector(preferredContentSizeCategory)]) { + NSString *contentSizeCategory = UIApplication.sharedApplication.preferredContentSizeCategory; + if ([contentSizeCategory isEqualToString:UIContentSizeCategoryExtraSmall]) { + index = [NSNumber numberWithInt:0]; + } else if ([contentSizeCategory isEqualToString:UIContentSizeCategorySmall]) { + index = [NSNumber numberWithInt:1]; + } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryMedium]) { + index = [NSNumber numberWithInt:2]; + } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryLarge]) { + index = [NSNumber numberWithInt:3]; + } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryExtraLarge]) { + index = [NSNumber numberWithInt:4]; + } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryExtraExtraLarge]) { + index = [NSNumber numberWithInt:5]; + } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) { + index = [NSNumber numberWithInt:6]; + } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityMedium]) { + index = [NSNumber numberWithInt:6]; + } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityLarge]) { + index = [NSNumber numberWithInt:6]; + } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) { + index = [NSNumber numberWithInt:6]; + } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) { + index = [NSNumber numberWithInt:6]; + } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) { + index = [NSNumber numberWithInt:6]; + } else{ + index = [NSNumber numberWithInt:6]; + } + } else { + index = [NSNumber numberWithInt:3]; + } + + return index; +} + ++ (CGFloat)heightForDynamicTypeCell:(NSArray *)heights { + NSNumber *index = [QMUIHelper preferredContentSizeLevel]; + return [((NSNumber *)[heights objectAtIndex:[index intValue]]) qmui_CGFloatValue]; +} +@end + +@implementation QMUIHelper (Keyboard) + +QMUISynthesizeBOOLProperty(keyboardVisible, setKeyboardVisible) +QMUISynthesizeCGFloatProperty(lastKeyboardHeight, setLastKeyboardHeight) + +- (void)handleKeyboardWillShow:(NSNotification *)notification { + self.keyboardVisible = YES; + self.lastKeyboardHeight = [QMUIHelper keyboardHeightWithNotification:notification]; +} + +- (void)handleKeyboardWillHide:(NSNotification *)notification { + self.keyboardVisible = NO; +} + ++ (BOOL)isKeyboardVisible { + BOOL visible = [QMUIHelper sharedInstance].keyboardVisible; + return visible; +} + ++ (CGFloat)lastKeyboardHeightInApplicationWindowWhenVisible { + return [QMUIHelper sharedInstance].lastKeyboardHeight; +} + ++ (CGRect)keyboardRectWithNotification:(NSNotification *)notification { + NSDictionary *userInfo = [notification userInfo]; + CGRect keyboardRect = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + // 注意iOS8以下的系统在横屏时得到的rect,宽度和高度相反了,所以不建议直接通过这个方法获取高度,而是使用keyboardHeightWithNotification:inView:,因为在后者的实现里会将键盘的rect转换坐标系,转换过程就会处理横竖屏旋转问题。 + return keyboardRect; +} + ++ (CGFloat)keyboardHeightWithNotification:(NSNotification *)notification { + return [QMUIHelper keyboardHeightWithNotification:notification inView:nil]; +} + ++ (CGFloat)keyboardHeightWithNotification:(nullable NSNotification *)notification inView:(nullable UIView *)view { + CGRect keyboardRect = [self keyboardRectWithNotification:notification]; + // iOS 13 分屏键盘 x 不是 0,不知道是系统 BUG 还是故意这样,先这样保护,再观察一下后面的 beta 版本 + if (IS_SPLIT_SCREEN_IPAD && keyboardRect.origin.x > 0) { + keyboardRect.origin.x = 0; + } + 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; +} + ++ (NSTimeInterval)keyboardAnimationDurationWithNotification:(NSNotification *)notification { + NSDictionary *userInfo = [notification userInfo]; + NSTimeInterval animationDuration = [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + return animationDuration; +} + ++ (UIViewAnimationCurve)keyboardAnimationCurveWithNotification:(NSNotification *)notification { + NSDictionary *userInfo = [notification userInfo]; + UIViewAnimationCurve curve = (UIViewAnimationCurve)[[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue]; + return curve; +} + ++ (UIViewAnimationOptions)keyboardAnimationOptionsWithNotification:(NSNotification *)notification { + UIViewAnimationOptions options = [QMUIHelper keyboardAnimationCurveWithNotification:notification]<<16; + return options; +} + +@end + + +@implementation QMUIHelper (AudioSession) + ++ (void)redirectAudioRouteWithSpeaker:(BOOL)speaker temporary:(BOOL)temporary { + if (![[AVAudioSession sharedInstance].category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) { + return; + } + if (temporary) { + [[AVAudioSession sharedInstance] overrideOutputAudioPort:speaker ? AVAudioSessionPortOverrideSpeaker : AVAudioSessionPortOverrideNone error:nil]; + } else { + [[AVAudioSession sharedInstance] setCategory:[AVAudioSession sharedInstance].category withOptions:speaker ? AVAudioSessionCategoryOptionDefaultToSpeaker : 0 error:nil]; + } +} + ++ (void)setAudioSessionCategory:(nullable NSString *)category { + + // 如果不属于系统category,返回 + if (category != AVAudioSessionCategoryAmbient && + category != AVAudioSessionCategorySoloAmbient && + category != AVAudioSessionCategoryPlayback && + category != AVAudioSessionCategoryRecord && + category != AVAudioSessionCategoryPlayAndRecord) + { + return; + } + + [[AVAudioSession sharedInstance] setCategory:category error:nil]; +} + +@end + + +@implementation QMUIHelper (UIGraphic) + +static CGFloat pixelOne = -1.0f; ++ (CGFloat)pixelOne { + if (pixelOne < 0) { + pixelOne = 1 / [[UIScreen mainScreen] scale]; + } + return pixelOne; +} + ++ (void)inspectContextSize:(CGSize)size { + if (!CGSizeIsValidated(size)) { + QMUIAssert(NO, @"QMUIHelper (UIGraphic)", @"QMUI CGPostError, %@:%d %s, 非法的size:%@\n%@", [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__, NSStringFromCGSize(size), [NSThread callStackSymbols]); + } +} + ++ (BOOL)inspectContextIfInvalidated:(CGContextRef)context { + if (!context) { + // 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 YES; +} + +@end + +@implementation QMUIHelper (Device) + ++ (NSString *)deviceModel { + if (IS_SIMULATOR) { + // Simulator doesn't return the identifier for the actual physical model, but returns it as an environment variable + // 模拟器不返回物理机器信息,但会通过环境变量的方式返回 + return [NSString stringWithFormat:@"%s", getenv("SIMULATOR_MODEL_IDENTIFIER")]; + } + + // 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 = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad ? 1 : 0; + } + return isIPad > 0; +} + +static NSInteger isIPod = -1; ++ (BOOL)isIPod { + if (isIPod < 0) { + NSString *string = [[UIDevice currentDevice] model]; + isIPod = [string rangeOfString:@"iPod touch"].location != NSNotFound ? 1 : 0; + } + return isIPod > 0; +} + +static NSInteger isIPhone = -1; ++ (BOOL)isIPhone { + if (isIPhone < 0) { + NSString *string = [[UIDevice currentDevice] model]; + isIPhone = [string rangeOfString:@"iPhone"].location != NSNotFound ? 1 : 0; + } + return isIPhone > 0; +} + +static NSInteger isSimulator = -1; ++ (BOOL)isSimulator { + if (isSimulator < 0) { +#if TARGET_OS_SIMULATOR + isSimulator = 1; +#else + isSimulator = 0; +#endif + } + 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 (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; + } + return isNotchedScreen > 0; +} + ++ (BOOL)isRegularScreen { + 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、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"] || [[QMUIHelper deviceModel] isEqualToString:@"iPhone12,1"])) ? 1 : 0; + } + return is61InchScreen > 0; +} + +static NSInteger is58InchScreen = -1; ++ (BOOL)is58InchScreen { + if (is58InchScreen < 0) { + // Both iPhone XS and iPhone X share the same actual screen sizes, so no need to compare identifiers + // iPhone XS 和 iPhone X 的物理尺寸是一致的,因此无需比较机器 Identifier + is58InchScreen = (DEVICE_WIDTH == self.screenSizeFor58Inch.width && DEVICE_HEIGHT == self.screenSizeFor58Inch.height) ? 1 : 0; + } + return is58InchScreen > 0; +} + +static NSInteger is55InchScreen = -1; ++ (BOOL)is55InchScreen { + if (is55InchScreen < 0) { + is55InchScreen = (DEVICE_WIDTH == self.screenSizeFor55Inch.width && DEVICE_HEIGHT == self.screenSizeFor55Inch.height) ? 1 : 0; + } + 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) { + is47InchScreen = (DEVICE_WIDTH == self.screenSizeFor47Inch.width && DEVICE_HEIGHT == self.screenSizeFor47Inch.height) ? 1 : 0; + } + return is47InchScreen > 0; +} + +static NSInteger is40InchScreen = -1; ++ (BOOL)is40InchScreen { + if (is40InchScreen < 0) { + is40InchScreen = (DEVICE_WIDTH == self.screenSizeFor40Inch.width && DEVICE_HEIGHT == self.screenSizeFor40Inch.height) ? 1 : 0; + } + return is40InchScreen > 0; +} + +static NSInteger is35InchScreen = -1; ++ (BOOL)is35InchScreen { + if (is35InchScreen < 0) { + is35InchScreen = (DEVICE_WIDTH == self.screenSizeFor35Inch.width && DEVICE_HEIGHT == self.screenSizeFor35Inch.height) ? 1 : 0; + } + 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); +} + ++ (CGSize)screenSizeFor58Inch { + return CGSizeMake(375, 812); +} + ++ (CGSize)screenSizeFor55Inch { + return CGSizeMake(414, 736); +} + ++ (CGSize)screenSizeFor54Inch { + return CGSizeMake(375, 812); +} + ++ (CGSize)screenSizeFor47Inch { + return CGSizeMake(375, 667); +} + ++ (CGSize)screenSizeFor40Inch { + return CGSizeMake(320, 568); +} + ++ (CGSize)screenSizeFor35Inch { + return CGSizeMake(320, 480); +} + +static CGFloat preferredLayoutWidth = -1; ++ (CGFloat)preferredLayoutAsSimilarScreenWidthForIPad { + if (preferredLayoutWidth < 0) { + NSArray *widths = @[@([self screenSizeFor65Inch].width), + @([self screenSizeFor58Inch].width), + @([self screenSizeFor40Inch].width)]; + preferredLayoutWidth = SCREEN_WIDTH; + 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) { + preferredLayoutWidth = widths[i].qmui_CGFloatValue; + continue; + } + } + } + return preferredLayoutWidth; +} + ++ (UIEdgeInsets)safeAreaInsetsForDeviceWithNotch { + if (![self isNotchedScreen]) { + return UIEdgeInsetsZero; + } + + if ([self isIPad]) { + return UIEdgeInsetsMake(24, 0, 20, 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)], + }, + + + // 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: + orientationKey = @(UIInterfaceOrientationLandscapeLeft); + break; + default: + 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; ++ (BOOL)isHighPerformanceDevice { + if (isHighPerformanceDevice < 0) { + NSString *model = [QMUIHelper deviceModel]; + NSString *identifier = [model qmui_stringMatchedByPattern:@"\\d+"]; + NSInteger version = identifier.integerValue; + if (IS_IPAD) { + isHighPerformanceDevice = version >= 5 ? 1 : 0;// iPad Air 2 + } else { + isHighPerformanceDevice = version >= 10 ? 1 : 0;// iPhone 8 + } + } + return isHighPerformanceDevice > 0; +} + ++ (BOOL)isZoomedMode { + if (!IS_IPHONE) { + return NO; + } + + CGFloat nativeScale = UIScreen.mainScreen.nativeScale; + CGFloat scale = UIScreen.mainScreen.scale; + + // 对于所有的 Plus 系列 iPhone,屏幕物理像素低于软件层面的渲染像素,不管标准模式还是放大模式,nativeScale 均小于 scale,所以需要特殊处理才能准确区分放大模式 + // https://www.paintcodeapp.com/news/ultimate-guide-to-iphone-resolutions + BOOL shouldBeDownsampledDevice = CGSizeEqualToSize(UIScreen.mainScreen.nativeBounds.size, CGSizeMake(1080, 1920)); + if (shouldBeDownsampledDevice) { + scale /= 1.15; + } + + 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; +} + ++ (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; +} + ++ (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; +} + ++ (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; + window.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed; + [window tintColorDidChange]; +} + ++ (void)resetDimmedApplicationWindow { + 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) + ++ (void)executeAnimationBlock:(__attribute__((noescape)) void (^)(void))animationBlock completionBlock:(__attribute__((noescape)) void (^)(void))completionBlock { + if (!animationBlock) return; + [CATransaction begin]; + [CATransaction setCompletionBlock:completionBlock]; + animationBlock(); + [CATransaction commit]; +} + +@end + +@implementation QMUIHelper (SystemVersion) + ++ (NSInteger)numbericOSVersion { + NSString *OSVersion = [[UIDevice currentDevice] systemVersion]; + NSArray *OSVersionArr = [OSVersion componentsSeparatedByString:@"."]; + + NSInteger numbericOSVersion = 0; + NSInteger pos = 0; + + while ([OSVersionArr count] > pos && pos < 3) { + numbericOSVersion += ([[OSVersionArr objectAtIndex:pos] integerValue] * pow(10, (4 - pos * 2))); + pos++; + } + + return numbericOSVersion; +} + ++ (NSComparisonResult)compareSystemVersion:(NSString *)currentVersion toVersion:(NSString *)targetVersion { + return [currentVersion compare:targetVersion options:NSNumericSearch]; +} + ++ (BOOL)isCurrentSystemAtLeastVersion:(NSString *)targetVersion { + return [QMUIHelper compareSystemVersion:[[UIDevice currentDevice] systemVersion] toVersion:targetVersion] == NSOrderedSame || [QMUIHelper compareSystemVersion:[[UIDevice currentDevice] systemVersion] toVersion:targetVersion] == NSOrderedDescending; +} + ++ (BOOL)isCurrentSystemLowerThanVersion:(NSString *)targetVersion { + return [QMUIHelper compareSystemVersion:[[UIDevice currentDevice] systemVersion] toVersion:targetVersion] == NSOrderedAscending; +} + +@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; + dispatch_once(&onceToken,^{ + instance = [[super allocWithZone:NULL] init]; + // 先设置默认值,不然可能变量的指针地址错误 + instance.keyboardVisible = NO; + instance.lastKeyboardHeight = 0; + 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; +} + ++ (id)allocWithZone:(struct _NSZone *)zone{ + return [self sharedInstance]; +} + +- (void)dealloc { + // QMUIHelper 若干个分类里有用到消息监听,所以在 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 new file mode 100644 index 00000000..7318f700 --- /dev/null +++ b/QMUI/QMUIKit/QMUICore/QMUILab.h @@ -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. + */ + +// +// QMUILab.h +// QMUIKit +// +// Created by MoLice on 2019/J/8. +// + +#ifndef QMUILab_h +#define QMUILab_h + +#import +#import +#import +#import "QMUICommonDefines.h" +#import "NSNumber+QMUI.h" +#import "QMUIWeakObjectContainer.h" + +/** + 以下系列宏用于在 Category 里添加 property 时,可以在 @implementation 里一句代码完成 getter/setter 的声明。暂不支持在 getter/setter 里添加自定义的逻辑,需要自定义的情况请继续使用 Code Snippet 生成的代码。 + 使用方式: + @code + @interface NSObject (CategoryName) + @property(nonatomic, strong) type *strongObj; + @property(nonatomic, weak) type *weakObj; + @property(nonatomic, assign) CGRect rectValue; + @end + + @implementation NSObject (CategoryName) + + // 注意 setter 不需要带冒号 + QMUISynthesizeIdStrongProperty(strongObj, setStrongObj) + QMUISynthesizeIdWeakProperty(weakObj, setWeakObj) + QMUISynthesizeCGRectProperty(rectValue, setRectValue) + + @end + @endcode + */ + +#pragma mark - Meta Marcos + +#define _QMUISynthesizeId(_getterName, _setterName, _policy) \ +_Pragma("clang diagnostic push") _Pragma(ClangWarningConcat("-Wmismatched-parameter-types")) _Pragma(ClangWarningConcat("-Wmismatched-return-types"))\ +static char kAssociatedObjectKey_##_getterName;\ +- (void)_setterName:(id)_getterName {\ + objc_setAssociatedObject(self, &kAssociatedObjectKey_##_getterName, _getterName, OBJC_ASSOCIATION_##_policy##_NONATOMIC);\ +}\ +\ +- (id)_getterName {\ + return objc_getAssociatedObject(self, &kAssociatedObjectKey_##_getterName);\ +}\ +_Pragma("clang diagnostic pop") + +#define _QMUISynthesizeWeakId(_getterName, _setterName) \ +_Pragma("clang diagnostic push") _Pragma(ClangWarningConcat("-Wmismatched-parameter-types")) _Pragma(ClangWarningConcat("-Wmismatched-return-types"))\ +static char kAssociatedObjectKey_##_getterName;\ +- (void)_setterName:(id)_getterName {\ + objc_setAssociatedObject(self, &kAssociatedObjectKey_##_getterName, [[QMUIWeakObjectContainer alloc] initWithObject:_getterName], OBJC_ASSOCIATION_RETAIN_NONATOMIC);\ +}\ +\ +- (id)_getterName {\ + return ((QMUIWeakObjectContainer *)objc_getAssociatedObject(self, &kAssociatedObjectKey_##_getterName)).object;\ +}\ +_Pragma("clang diagnostic pop") + +#define _QMUISynthesizeNonObject(_getterName, _setterName, _type, valueInitializer, valueGetter) \ +_Pragma("clang diagnostic push") _Pragma(ClangWarningConcat("-Wmismatched-parameter-types")) _Pragma(ClangWarningConcat("-Wmismatched-return-types"))\ +static char kAssociatedObjectKey_##_getterName;\ +- (void)_setterName:(_type)_getterName {\ + objc_setAssociatedObject(self, &kAssociatedObjectKey_##_getterName, [NSNumber valueInitializer:_getterName], OBJC_ASSOCIATION_RETAIN_NONATOMIC);\ +}\ +\ +- (_type)_getterName {\ + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_##_getterName)) valueGetter];\ +}\ +_Pragma("clang diagnostic pop") + + + + +#pragma mark - Object Marcos + +/// @property(nonatomic, strong) id xxx +#define QMUISynthesizeIdStrongProperty(_getterName, _setterName) _QMUISynthesizeId(_getterName, _setterName, RETAIN) + +/// @property(nonatomic, weak) id xxx +#define QMUISynthesizeIdWeakProperty(_getterName, _setterName) _QMUISynthesizeWeakId(_getterName, _setterName) + +/// @property(nonatomic, copy) id xxx +#define QMUISynthesizeIdCopyProperty(_getterName, _setterName) _QMUISynthesizeId(_getterName, _setterName, COPY) + + + +#pragma mark - NonObject Marcos + +/// @property(nonatomic, assign) Int xxx +#define QMUISynthesizeIntProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, int, numberWithInt, intValue) + +/// @property(nonatomic, assign) unsigned int xxx +#define QMUISynthesizeUnsignedIntProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, unsigned int, numberWithUnsignedInt, unsignedIntValue) + +/// @property(nonatomic, assign) float xxx +#define QMUISynthesizeFloatProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, float, numberWithFloat, floatValue) + +/// @property(nonatomic, assign) double xxx +#define QMUISynthesizeDoubleProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, double, numberWithDouble, doubleValue) + +/// @property(nonatomic, assign) BOOL xxx +#define QMUISynthesizeBOOLProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, BOOL, numberWithBool, boolValue) + +/// @property(nonatomic, assign) NSInteger xxx +#define QMUISynthesizeNSIntegerProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, NSInteger, numberWithInteger, integerValue) + +/// @property(nonatomic, assign) NSUInteger xxx +#define QMUISynthesizeNSUIntegerProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, NSUInteger, numberWithUnsignedInteger, unsignedIntegerValue) + +/// @property(nonatomic, assign) CGFloat xxx +#define QMUISynthesizeCGFloatProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, CGFloat, numberWithDouble, qmui_CGFloatValue) + +/// @property(nonatomic, assign) CGPoint xxx +#define QMUISynthesizeCGPointProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, CGPoint, valueWithCGPoint, CGPointValue) + +/// @property(nonatomic, assign) CGSize xxx +#define QMUISynthesizeCGSizeProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, CGSize, valueWithCGSize, CGSizeValue) + +/// @property(nonatomic, assign) CGRect xxx +#define QMUISynthesizeCGRectProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, CGRect, valueWithCGRect, CGRectValue) + +/// @property(nonatomic, assign) UIEdgeInsets xxx +#define QMUISynthesizeUIEdgeInsetsProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, UIEdgeInsets, valueWithUIEdgeInsets, UIEdgeInsetsValue) + +/// @property(nonatomic, assign) CGVector xxx +#define QMUISynthesizeCGVectorProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, CGVector, valueWithCGVector, CGVectorValue) + +/// @property(nonatomic, assign) CGAffineTransform xxx +#define QMUISynthesizeCGAffineTransformProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, CGAffineTransform, valueWithCGAffineTransform, CGAffineTransformValue) + +/// @property(nonatomic, assign) NSDirectionalEdgeInsets xxx +#define QMUISynthesizeNSDirectionalEdgeInsetsProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, NSDirectionalEdgeInsets, valueWithDirectionalEdgeInsets, NSDirectionalEdgeInsetsValue) + +/// @property(nonatomic, assign) UIOffset xxx +#define QMUISynthesizeUIOffsetProperty(_getterName, _setterName) _QMUISynthesizeNonObject(_getterName, _setterName, UIOffset, valueWithUIOffset, UIOffsetValue) + +#endif /* QMUILab_h */ diff --git a/QMUI/QMUIKit/QMUICore/QMUIRuntime.h b/QMUI/QMUIKit/QMUICore/QMUIRuntime.h new file mode 100644 index 00000000..2711433a --- /dev/null +++ b/QMUI/QMUIKit/QMUICore/QMUIRuntime.h @@ -0,0 +1,344 @@ +/** + * 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. + */ + +// +// QMUIRuntime.h +// QMUIKit +// +// Created by QMUI Team on 2018/8/14. +// + +#import +#import +#import +#import "NSObject+QMUI.h" +#import "NSMethodSignature+QMUI.h" +#import "QMUILog.h" + +/// 以高级语言的方式描述一个 objc_property_t 的各种属性,请使用 `+descriptorWithProperty` 生成对象后直接读取对象的各种值。 +@interface QMUIPropertyDescriptor : NSObject + +@property(nonatomic, strong) NSString *name; +@property(nonatomic, assign) SEL getter; +@property(nonatomic, assign) SEL setter; + +@property(nonatomic, assign) BOOL isAtomic; +@property(nonatomic, assign) BOOL isNonatomic; + +@property(nonatomic, assign) BOOL isAssign; +@property(nonatomic, assign) BOOL isWeak; +@property(nonatomic, assign) BOOL isStrong; +@property(nonatomic, assign) BOOL isCopy; + +@property(nonatomic, assign) BOOL isReadonly; +@property(nonatomic, assign) BOOL isReadwrite; + +@property(nonatomic, copy) NSString *type; + ++ (instancetype)descriptorWithProperty:(objc_property_t)property; + +@end + +#pragma mark - Method + +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; +} + +/** + * 如果 fromClass 里存在 originSelector,则这个函数会将 fromClass 里的 originSelector 与 toClass 里的 newSelector 交换实现。 + * 如果 fromClass 里不存在 originSelecotr,则这个函数会为 fromClass 增加方法 originSelector,并且该方法会使用 toClass 的 newSelector 方法的实现,而 toClass 的 newSelector 方法的实现则会被替换为空内容 + * @warning 注意如果 fromClass 里的 originSelector 是继承自父类并且 fromClass 也没有重写这个方法,这会导致实际上被替换的是父类,然后父类及父类的所有子类(也即 fromClass 的兄弟类)也受影响,因此使用时请谨记这一点。因此建议使用 OverrideImplementation 系列的方法去替换,尽量避免使用 ExchangeImplementations。 + * @param _fromClass 要被替换的 class,不能为空 + * @param _originSelector 要被替换的 class 的 selector,可为空,为空则相当于为 fromClass 新增这个方法 + * @param _toClass 要拿这个 class 的方法来替换 + * @param _newSelector 要拿 toClass 里的这个方法来替换 originSelector + * @return 是否成功替换(或增加) + */ +CG_INLINE BOOL +ExchangeImplementationsInTwoClasses(Class _fromClass, SEL _originSelector, Class _toClass, SEL _newSelector) { + if (!_fromClass || !_toClass) { + return NO; + } + + Method oriMethod = class_getInstanceMethod(_fromClass, _originSelector); + Method newMethod = class_getInstanceMethod(_toClass, _newSelector); + if (!newMethod) { + return NO; + } + + BOOL isAddedMethod = class_addMethod(_fromClass, _originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)); + if (isAddedMethod) { + // 如果 class_addMethod 成功了,说明之前 fromClass 里并不存在 originSelector,所以要用一个空的方法代替它,以避免 class_replaceMethod 后,后续 toClass 的这个方法被调用时可能会 crash + IMP oriMethodIMP = method_getImplementation(oriMethod) ?: imp_implementationWithBlock(^(id selfObject) {}); + const char *oriMethodTypeEncoding = method_getTypeEncoding(oriMethod) ?: "v@:"; + class_replaceMethod(_toClass, _newSelector, oriMethodIMP, oriMethodTypeEncoding); + } else { + method_exchangeImplementations(oriMethod, newMethod); + } + return YES; +} + +/// 交换同一个 class 里的 originSelector 和 newSelector 的实现,如果原本不存在 originSelector,则相当于给 class 新增一个叫做 originSelector 的方法 +CG_INLINE BOOL +ExchangeImplementations(Class _class, SEL _originSelector, SEL _newSelector) { + return ExchangeImplementationsInTwoClasses(_class, _originSelector, _class, _newSelector); +} + +/** + * 用 block 重写某个 class 的指定方法 + * @param targetClass 要重写的 class + * @param targetSelector 要重写的 class 里的实例方法,注意如果该方法不存在于 targetClass 里,则什么都不做 + * @param implementationBlock 该 block 必须返回一个 block,返回的 block 将被当成 targetSelector 的新实现,所以要在内部自己处理对 super 的调用,以及对当前调用方法的 self 的 class 的保护判断(因为如果 targetClass 的 targetSelector 是继承自父类的,targetClass 内部并没有重写这个方法,则我们这个函数最终重写的其实是父类的 targetSelector,所以会产生预期之外的 class 的影响,例如 targetClass 传进来 UIButton.class,则最终可能会影响到 UIView.class),implementationBlock 的参数里第一个为你要修改的 class,也即等同于 targetClass,第二个参数为你要修改的 selector,也即等同于 targetSelector,第三个参数是一个 block,用于获取 targetSelector 原本的实现,由于 IMP 可以直接当成 C 函数调用,所以可利用它来实现“调用 super”的效果,但由于 targetSelector 的参数个数、参数类型、返回值类型,都会影响 IMP 的调用写法,所以这个调用只能由业务自己写。 + */ +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){ + QMUILogWarn(([NSString stringWithFormat:@"%@", targetClass]), @"%@ 没有初始实现,%@\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; +} + +/** + * 用 block 重写某个 class 的某个无参数且返回值为 void 的方法,会自动在调用 block 之前先调用该方法原本的实现。 + * @param targetClass 要重写的 class + * @param targetSelector 要重写的 class 里的实例方法,注意如果该方法不存在于 targetClass 里,则什么都不做,注意该方法必须无参数,返回值为 void + * @param implementationBlock targetSelector 的自定义实现,直接将你的实现写进去即可,不需要管 super 的调用。参数 selfObject 代表当前正在调用这个方法的对象,也即 self 指针。 + */ +CG_INLINE BOOL +ExtendImplementationOfVoidMethodWithoutArguments(Class targetClass, SEL targetSelector, void (^implementationBlock)(__kindof NSObject *selfObject)) { + return OverrideImplementation(targetClass, targetSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + void (^block)(__unsafe_unretained __kindof NSObject *selfObject) = ^(__unsafe_unretained __kindof NSObject *selfObject) { + + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + implementationBlock(selfObject); + }; + #if __has_feature(objc_arc) + return block; + #else + return [block copy]; + #endif + }); +} + +/** + * 用 block 重写某个 class 的某个无参数且带返回值的方法,会自动在调用 block 之前先调用该方法原本的实现。 + * @param _targetClass 要重写的 class + * @param _targetSelector 要重写的 class 里的实例方法,注意如果该方法不存在于 targetClass 里,则什么都不做,注意该方法必须带一个参数,返回值不为空 + * @param _returnType 返回值的数据类型 + * @param _implementationBlock 格式为 ^_returnType(NSObject *selfObject, _returnType originReturnValue) {},内容即为 targetSelector 的自定义实现,直接将你的实现写进去即可,不需要管 super 的调用。第一个参数 selfObject 代表当前正在调用这个方法的对象,也即 self 指针;第二个参数 originReturnValue 代表 super 的返回值,具体类型请自行填写 + */ +#define ExtendImplementationOfNonVoidMethodWithoutArguments(_targetClass, _targetSelector, _returnType, _implementationBlock) OverrideImplementation(_targetClass, _targetSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {\ + return ^_returnType (__unsafe_unretained __kindof NSObject *selfObject) {\ + \ + _returnType (*originSelectorIMP)(id, SEL);\ + originSelectorIMP = (_returnType (*)(id, SEL))originalIMPProvider();\ + _returnType result = originSelectorIMP(selfObject, originCMD);\ + \ + return _implementationBlock(selfObject, result);\ + };\ + }); + +/** + * 用 block 重写某个 class 的带一个参数且返回值为 void 的方法,会自动在调用 block 之前先调用该方法原本的实现。 + * @param _targetClass 要重写的 class + * @param _targetSelector 要重写的 class 里的实例方法,注意如果该方法不存在于 targetClass 里,则什么都不做,注意该方法必须带一个参数,返回值为 void + * @param _argumentType targetSelector 的参数类型 + * @param _implementationBlock 格式为 ^(NSObject *selfObject, _argumentType firstArgv) {},内容即为 targetSelector 的自定义实现,直接将你的实现写进去即可,不需要管 super 的调用。第一个参数 selfObject 代表当前正在调用这个方法的对象,也即 self 指针;第二个参数 firstArgv 代表 targetSelector 被调用时传进来的第一个参数,具体的类型请自行填写 + */ +#define ExtendImplementationOfVoidMethodWithSingleArgument(_targetClass, _targetSelector, _argumentType, _implementationBlock) OverrideImplementation(_targetClass, _targetSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {\ + return ^(__unsafe_unretained __kindof NSObject *selfObject, _argumentType firstArgv) {\ + \ + void (*originSelectorIMP)(id, SEL, _argumentType);\ + originSelectorIMP = (void (*)(id, SEL, _argumentType))originalIMPProvider();\ + originSelectorIMP(selfObject, originCMD, firstArgv);\ + \ + _implementationBlock(selfObject, firstArgv);\ + };\ + }); + +#define ExtendImplementationOfVoidMethodWithTwoArguments(_targetClass, _targetSelector, _argumentType1, _argumentType2, _implementationBlock) OverrideImplementation(_targetClass, _targetSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {\ + return ^(__unsafe_unretained __kindof NSObject *selfObject, _argumentType1 firstArgv, _argumentType2 secondArgv) {\ + \ + void (*originSelectorIMP)(id, SEL, _argumentType1, _argumentType2);\ + originSelectorIMP = (void (*)(id, SEL, _argumentType1, _argumentType2))originalIMPProvider();\ + originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv);\ + \ + _implementationBlock(selfObject, firstArgv, secondArgv);\ + };\ + }); + +/** + * 用 block 重写某个 class 的带一个参数且带返回值的方法,会自动在调用 block 之前先调用该方法原本的实现。 + * @param targetClass 要重写的 class + * @param targetSelector 要重写的 class 里的实例方法,注意如果该方法不存在于 targetClass 里,则什么都不做,注意该方法必须带一个参数,返回值不为空 + * @param implementationBlock,格式为 ^_returnType (NSObject *selfObject, _argumentType firstArgv, _returnType originReturnValue){},内容也即 targetSelector 的自定义实现,直接将你的实现写进去即可,不需要管 super 的调用。第一个参数 selfObject 代表当前正在调用这个方法的对象,也即 self 指针;第二个参数 firstArgv 代表 targetSelector 被调用时传进来的第一个参数,具体的类型请自行填写;第三个参数 originReturnValue 代表 super 的返回值,具体类型请自行填写 + */ +#define ExtendImplementationOfNonVoidMethodWithSingleArgument(_targetClass, _targetSelector, _argumentType, _returnType, _implementationBlock) OverrideImplementation(_targetClass, _targetSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {\ + return ^_returnType (__unsafe_unretained __kindof NSObject *selfObject, _argumentType firstArgv) {\ + \ + _returnType (*originSelectorIMP)(id, SEL, _argumentType);\ + originSelectorIMP = (_returnType (*)(id, SEL, _argumentType))originalIMPProvider();\ + _returnType result = originSelectorIMP(selfObject, originCMD, firstArgv);\ + \ + return _implementationBlock(selfObject, firstArgv, result);\ + };\ + }); + +#define ExtendImplementationOfNonVoidMethodWithTwoArguments(_targetClass, _targetSelector, _argumentType1, _argumentType2, _returnType, _implementationBlock) OverrideImplementation(_targetClass, _targetSelector, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {\ + return ^_returnType (__unsafe_unretained __kindof NSObject *selfObject, _argumentType1 firstArgv, _argumentType2 secondArgv) {\ + \ + _returnType (*originSelectorIMP)(id, SEL, _argumentType1, _argumentType2);\ + originSelectorIMP = (_returnType (*)(id, SEL, _argumentType1, _argumentType2))originalIMPProvider();\ + _returnType result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv);\ + \ + return _implementationBlock(selfObject, firstArgv, secondArgv, result);\ + };\ + }); + +#pragma mark - Ivar + +/** + 用于判断一个给定的 type encoding(const char *)或者 Ivar 是哪种类型的系列函数。 + + 为了节省代码量,函数由宏展开生成,一个宏会展开为两个函数定义: + + 1. isXxxTypeEncoding(const char *),例如判断是否为 BOOL 类型的函数名为:isBOOLTypeEncoding() + 2. isXxxIvar(Ivar),例如判断是否为 BOOL 的 Ivar 的函数名为:isBOOLIvar() + + @see https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1 + */ +#define _QMUITypeEncodingDetectorGenerator(_TypeInFunctionName, _typeForEncode) \ + CG_INLINE BOOL is##_TypeInFunctionName##TypeEncoding(const char *typeEncoding) {\ + return strncmp(@encode(_typeForEncode), typeEncoding, strlen(@encode(_typeForEncode))) == 0;\ + }\ + CG_INLINE BOOL is##_TypeInFunctionName##Ivar(Ivar ivar) {\ + return is##_TypeInFunctionName##TypeEncoding(ivar_getTypeEncoding(ivar));\ + } + +_QMUITypeEncodingDetectorGenerator(Char, char) +_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 *) +_QMUITypeEncodingDetectorGenerator(Object, id) +_QMUITypeEncodingDetectorGenerator(Class, Class) +_QMUITypeEncodingDetectorGenerator(Selector, SEL) + +//CG_INLINE char getCharIvarValue(id object, Ivar ivar) { +// ptrdiff_t ivarOffset = ivar_getOffset(ivar); +// unsigned char * bytes = (unsigned char *)(__bridge void *)object; +// char value = *((char *)(bytes + ivarOffset)); +// return value; +//} + +#define _QMUIGetIvarValueGenerator(_TypeInFunctionName, _typeForEncode) \ + CG_INLINE _typeForEncode get##_TypeInFunctionName##IvarValue(id object, Ivar ivar) {\ + ptrdiff_t ivarOffset = ivar_getOffset(ivar);\ + unsigned char * bytes = (unsigned char *)(__bridge void *)object;\ + _typeForEncode value = *((_typeForEncode *)(bytes + ivarOffset));\ + return value;\ + } + +_QMUIGetIvarValueGenerator(Char, char) +_QMUIGetIvarValueGenerator(Int, int) +_QMUIGetIvarValueGenerator(Short, short) +_QMUIGetIvarValueGenerator(Long, long) +_QMUIGetIvarValueGenerator(LongLong, long long) +_QMUIGetIvarValueGenerator(UnsignedChar, unsigned char) +_QMUIGetIvarValueGenerator(UnsignedInt, unsigned int) +_QMUIGetIvarValueGenerator(UnsignedShort, unsigned short) +_QMUIGetIvarValueGenerator(UnsignedLong, unsigned long) +_QMUIGetIvarValueGenerator(UnsignedLongLong, unsigned long long) +_QMUIGetIvarValueGenerator(Float, float) +_QMUIGetIvarValueGenerator(Double, double) +_QMUIGetIvarValueGenerator(BOOL, BOOL) +_QMUIGetIvarValueGenerator(Character, char *) +_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 new file mode 100644 index 00000000..7cb5780c --- /dev/null +++ b/QMUI/QMUIKit/QMUICore/QMUIRuntime.m @@ -0,0 +1,237 @@ +/** + * 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. + */ + +// +// QMUIRuntime.m +// QMUIKit +// +// Created by QMUI Team on 2018/9/5. +// + +#import "QMUIRuntime.h" +#import "QMUICommonDefines.h" +#import "QMUIHelper.h" +#include +#include + +@implementation QMUIPropertyDescriptor + ++ (instancetype)descriptorWithProperty:(objc_property_t)property { + QMUIPropertyDescriptor *descriptor = [[self alloc] init]; + NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)]; + descriptor.name = propertyName; + + // getter + char *getterChar = property_copyAttributeValue(property, "G"); + descriptor.getter = NSSelectorFromString(getterChar != NULL ? [NSString stringWithUTF8String:getterChar] : propertyName); + if (getterChar != NULL) { + free(getterChar); + } + + // setter + char *setterChar = property_copyAttributeValue(property, "S"); + NSString *setterString = setterChar != NULL ? [NSString stringWithUTF8String:setterChar] : NSStringFromSelector(setterWithGetter(NSSelectorFromString(propertyName))); + descriptor.setter = NSSelectorFromString(setterString); + if (setterChar != NULL) { + free(setterChar); + } + + // atomic/nonatomic + char *attrValue_N = property_copyAttributeValue(property, "N"); + BOOL isAtomic = (attrValue_N == NULL); + descriptor.isAtomic = isAtomic; + descriptor.isNonatomic = !isAtomic; + if (attrValue_N != NULL) { + free(attrValue_N); + } + + // assign/weak/strong/copy + char *attrValue_isCopy = property_copyAttributeValue(property, "C"); + char *attrValue_isStrong = property_copyAttributeValue(property, "&"); + char *attrValue_isWeak = property_copyAttributeValue(property, "W"); + BOOL isCopy = attrValue_isCopy != NULL; + BOOL isStrong = attrValue_isStrong != NULL; + BOOL isWeak = attrValue_isWeak != NULL; + if (attrValue_isCopy != NULL) { + free(attrValue_isCopy); + } + if (attrValue_isStrong != NULL) { + free(attrValue_isStrong); + } + if (attrValue_isWeak != NULL) { + free(attrValue_isWeak); + } + descriptor.isCopy = isCopy; + descriptor.isStrong = isStrong; + descriptor.isWeak = isWeak; + descriptor.isAssign = !isCopy && !isStrong && !isWeak; + + // readonly/readwrite + char *attrValue_isReadonly = property_copyAttributeValue(property, "R"); + BOOL isReadonly = (attrValue_isReadonly != NULL); + if (attrValue_isReadonly != NULL) { + free(attrValue_isReadonly); + } + descriptor.isReadonly = isReadonly; + descriptor.isReadwrite = !isReadonly; + + // type + char *type = property_copyAttributeValue(property, "T"); + descriptor.type = [QMUIPropertyDescriptor typeWithEncodeString:[NSString stringWithUTF8String:type]]; + if (type != NULL) { + free(type); + } + + return descriptor; +} + +- (NSString *)description { + NSMutableString *result = [[NSMutableString alloc] init]; + [result appendString:@"@property("]; + if (self.isNonatomic) [result appendString:@"nonatomic, "]; + [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 != setterWithGetter(NSSelectorFromString(self.name))) [result appendFormat:@", setter=%@", NSStringFromSelector(self.setter)]; + [result appendString:@") "]; + [result appendString:self.type]; + [result appendString:@" "]; + [result appendString:self.name]; + [result appendString:@";"]; + return result.copy; +} + +#define _DetectTypeAndReturn(_type) if (strncmp(@encode(_type), typeEncoding, strlen(@encode(_type))) == 0) return @#_type; + ++ (NSString *)typeWithEncodeString:(NSString *)encodeString { + if ([encodeString containsString:@"@\""]) { + NSString *result = [encodeString substringWithRange:NSMakeRange(2, encodeString.length - 2 - 1)]; + if ([result containsString:@"<"] && [result containsString:@">"]) { + // protocol + if ([result hasPrefix:@"<"]) { + // id pointer + return [NSString stringWithFormat:@"id%@", result]; + } + } + // class + return [NSString stringWithFormat:@"%@ *", result]; + } + + const char *typeEncoding = encodeString.UTF8String; + _DetectTypeAndReturn(NSInteger) + _DetectTypeAndReturn(NSUInteger) + _DetectTypeAndReturn(int) + _DetectTypeAndReturn(short) + _DetectTypeAndReturn(long) + _DetectTypeAndReturn(long long) + _DetectTypeAndReturn(char) + _DetectTypeAndReturn(unsigned char) + _DetectTypeAndReturn(unsigned int) + _DetectTypeAndReturn(unsigned short) + _DetectTypeAndReturn(unsigned long) + _DetectTypeAndReturn(unsigned long long) + _DetectTypeAndReturn(CGFloat) + _DetectTypeAndReturn(float) + _DetectTypeAndReturn(double) + _DetectTypeAndReturn(void) + _DetectTypeAndReturn(char *) + _DetectTypeAndReturn(id) + _DetectTypeAndReturn(Class) + _DetectTypeAndReturn(SEL) + _DetectTypeAndReturn(BOOL) + + return 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 cb60014e..40a5be59 100644 --- a/QMUI/QMUIKit/QMUIKit.h +++ b/QMUI/QMUIKit/QMUIKit.h @@ -1,102 +1,698 @@ -// -// QMUIKit.h -// QMUIKit -// -// Created by zhoonchen on 16/9/9. -// Copyright © 2016年 QMUI Team. All rights reserved. -// +/** + * 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. + */ + +/// Automatically created by script in Build Phases #import -/// QMUICore -#import "QMUICore.h" +#ifndef QMUIKit_h +#define QMUIKit_h -/// QMUIKit -#import "QMUIVisualEffectView.h" -#import "QMUIButton.h" -#import "QMUILabel.h" -#import "QMUITextField.h" -#import "QMUITextView.h" -#import "QMUISearchBar.h" -#import "QMUITableViewCell.h" -#import "QMUITableView.h" -#import "QMUITableViewProtocols.h" -#import "QMUISegmentedControl.h" -#import "QMUICollectionViewPagingLayout.h" -#import "QMUITestView.h" +static NSString * const QMUI_VERSION = @"4.8.0"; -/// Category -#import "NSObject+QMUI.h" -#import "NSString+QMUI.h" -#import "NSAttributedString+QMUI.h" -#import "UIColor+QMUI.h" -#import "UIImage+QMUI.h" +#if __has_include("CAAnimation+QMUI.h") +#import "CAAnimation+QMUI.h" +#endif + +#if __has_include("CALayer+QMUI.h") #import "CALayer+QMUI.h" -#import "UIView+QMUI.h" -#import "UIFont+QMUI.h" -#import "UIBezierPath+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 + +#if __has_include("NSAttributedString+QMUI.h") +#import "NSAttributedString+QMUI.h" +#endif + +#if __has_include("NSCharacterSet+QMUI.h") +#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 + +#if __has_include("NSNumber+QMUI.h") +#import "NSNumber+QMUI.h" +#endif + +#if __has_include("NSObject+QMUI.h") +#import "NSObject+QMUI.h" +#endif + +#if __has_include("NSObject+QMUIMultipleDelegates.h") +#import "NSObject+QMUIMultipleDelegates.h" +#endif + +#if __has_include("NSParagraphStyle+QMUI.h") #import "NSParagraphStyle+QMUI.h" -#import "UILabel+QMUI.h" -#import "UIImageView+QMUI.h" -#import "UIControl+QMUI.h" -#import "UITextField+QMUI.h" -#import "UITextView+QMUI.h" -#import "UIButton+QMUI.h" -#import "UISearchBar+QMUI.h" -#import "UIScrollView+QMUI.h" -#import "QMUICellHeightCache.h" -#import "UITableView+QMUI.h" -#import "UICollectionView+QMUI.h" -#import "UITabBar+QMUI.h" -#import "UITabBarItem+QMUI.h" -#import "UIActivityIndicatorView+QMUI.h" -#import "UIWindow+QMUI.h" -#import "UIViewController+QMUI.h" -#import "UINavigationController+QMUI.h" -#import "UINavigationBar+Transition.h" -#import "UINavigationController+NavigationBarTransition.h" +#endif -/// UIComponents -#import "QMUIKeyboardManager.h" -#import "QMUIToastBackgroundView.h" -#import "QMUIToastContentView.h" -#import "QMUIToastAnimator.h" -#import "QMUIToastView.h" -#import "QMUITips.h" -#import "QMUIEmptyView.h" -#import "QMUINavigationTitleView.h" -#import "QMUIGridView.h" -#import "QMUIFloatLayoutView.h" -#import "QMUIZoomImageView.h" -#import "QMUIImagePreviewView.h" -#import "QMUIImagePreviewViewController.h" +#if __has_include("NSPointerArray+QMUI.h") +#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 + +#if __has_include("NSURL+QMUI.h") +#import "NSURL+QMUI.h" +#endif + +#if __has_include("QMUIAlbumViewController.h") +#import "QMUIAlbumViewController.h" +#endif + +#if __has_include("QMUIAlertController.h") +#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 + +#if __has_include("QMUIAssetsGroup.h") #import "QMUIAssetsGroup.h" -#import "QMUIImagePickerHelper.h" +#endif + +#if __has_include("QMUIAssetsManager.h") #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 + +#if __has_include("QMUICellHeightCache.h") +#import "QMUICellHeightCache.h" +#endif + +#if __has_include("QMUICellHeightKeyCache.h") +#import "QMUICellHeightKeyCache.h" +#endif + +#if __has_include("QMUICellSizeKeyCache.h") +#import "QMUICellSizeKeyCache.h" +#endif + +#if __has_include("QMUICheckbox.h") +#import "QMUICheckbox.h" +#endif + +#if __has_include("QMUICollectionViewPagingLayout.h") +#import "QMUICollectionViewPagingLayout.h" +#endif + +#if __has_include("QMUICommonDefines.h") +#import "QMUICommonDefines.h" +#endif + +#if __has_include("QMUICommonTableViewController.h") +#import "QMUICommonTableViewController.h" +#endif + +#if __has_include("QMUICommonViewController.h") +#import "QMUICommonViewController.h" +#endif + +#if __has_include("QMUIConfiguration.h") +#import "QMUIConfiguration.h" +#endif + +#if __has_include("QMUIConfigurationMacros.h") +#import "QMUIConfigurationMacros.h" +#endif + +#if __has_include("QMUIConsole.h") +#import "QMUIConsole.h" +#endif + +#if __has_include("QMUIConsoleToolbar.h") +#import "QMUIConsoleToolbar.h" +#endif + +#if __has_include("QMUIConsoleViewController.h") +#import "QMUIConsoleViewController.h" +#endif + +#if __has_include("QMUICore.h") +#import "QMUICore.h" +#endif + +#if __has_include("QMUIDialogViewController.h") +#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 + +#if __has_include("QMUIEmotionView.h") #import "QMUIEmotionView.h" -#import "QMUIQQEmotionManager.h" -#import "QMUIPieProgressView.h" -#import "QMUIPopupContainerView.h" -#import "QMUIPopupMenuView.h" -#import "QMUIModalPresentationViewController.h" -#import "QMUIAlertController.h" -#import "QMUIAlbumViewController.h" -#import "QMUIImagePickerViewController.h" +#endif + +#if __has_include("QMUIEmptyView.h") +#import "QMUIEmptyView.h" +#endif + +#if __has_include("QMUIFloatLayoutView.h") +#import "QMUIFloatLayoutView.h" +#endif + +#if __has_include("QMUIGridView.h") +#import "QMUIGridView.h" +#endif + +#if __has_include("QMUIHelper.h") +#import "QMUIHelper.h" +#endif + +#if __has_include("QMUIImagePickerCollectionViewCell.h") #import "QMUIImagePickerCollectionViewCell.h" +#endif + +#if __has_include("QMUIImagePickerHelper.h") +#import "QMUIImagePickerHelper.h" +#endif + +#if __has_include("QMUIImagePickerPreviewViewController.h") #import "QMUIImagePickerPreviewViewController.h" +#endif + +#if __has_include("QMUIImagePickerViewController.h") +#import "QMUIImagePickerViewController.h" +#endif + +#if __has_include("QMUIImagePreviewView.h") +#import "QMUIImagePreviewView.h" +#endif + +#if __has_include("QMUIImagePreviewViewController.h") +#import "QMUIImagePreviewViewController.h" +#endif + +#if __has_include("QMUIImagePreviewViewTransitionAnimator.h") +#import "QMUIImagePreviewViewTransitionAnimator.h" +#endif + +#if __has_include("QMUIKeyboardManager.h") +#import "QMUIKeyboardManager.h" +#endif + +#if __has_include("QMUILab.h") +#import "QMUILab.h" +#endif + +#if __has_include("QMUILabel.h") +#import "QMUILabel.h" +#endif + +#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") +#import "QMUILog+QMUIConsole.h" +#endif + +#if __has_include("QMUILog.h") +#import "QMUILog.h" +#endif + +#if __has_include("QMUILogItem.h") +#import "QMUILogItem.h" +#endif + +#if __has_include("QMUILogManagerViewController.h") +#import "QMUILogManagerViewController.h" +#endif + +#if __has_include("QMUILogNameManager.h") +#import "QMUILogNameManager.h" +#endif + +#if __has_include("QMUILogger+QMUIConfigurationTemplate.h") +#import "QMUILogger+QMUIConfigurationTemplate.h" +#endif + +#if __has_include("QMUILogger.h") +#import "QMUILogger.h" +#endif + +#if __has_include("QMUIMarqueeLabel.h") +#import "QMUIMarqueeLabel.h" +#endif + +#if __has_include("QMUIModalPresentationViewController.h") +#import "QMUIModalPresentationViewController.h" +#endif + +#if __has_include("QMUIMoreOperationController.h") #import "QMUIMoreOperationController.h" -#import "QMUIDialogViewController.h" +#endif + +#if __has_include("QMUIMultipleDelegates.h") +#import "QMUIMultipleDelegates.h" +#endif + +#if __has_include("QMUINavigationBarScrollingAnimator.h") +#import "QMUINavigationBarScrollingAnimator.h" +#endif + +#if __has_include("QMUINavigationBarScrollingSnapAnimator.h") +#import "QMUINavigationBarScrollingSnapAnimator.h" +#endif + +#if __has_include("QMUINavigationButton.h") +#import "QMUINavigationButton.h" +#endif + +#if __has_include("QMUINavigationController.h") +#import "QMUINavigationController.h" +#endif + +#if __has_include("QMUINavigationTitleView.h") +#import "QMUINavigationTitleView.h" +#endif + +#if __has_include("QMUIOrderedDictionary.h") #import "QMUIOrderedDictionary.h" -#import "QMUIMarqueeLabel.h" -#import "QMUISlider.h" +#endif + +#if __has_include("QMUIPieProgressView.h") +#import "QMUIPieProgressView.h" +#endif + +#if __has_include("QMUIPopupContainerView.h") +#import "QMUIPopupContainerView.h" +#endif + +#if __has_include("QMUIPopupMenuItem.h") +#import "QMUIPopupMenuItem.h" +#endif + +#if __has_include("QMUIPopupMenuItemView.h") +#import "QMUIPopupMenuItemView.h" +#endif + +#if __has_include("QMUIPopupMenuItemViewProtocol.h") +#import "QMUIPopupMenuItemViewProtocol.h" +#endif + +#if __has_include("QMUIPopupMenuView.h") +#import "QMUIPopupMenuView.h" +#endif + +#if __has_include("QMUIRuntime.h") +#import "QMUIRuntime.h" +#endif + +#if __has_include("QMUIScrollAnimator.h") +#import "QMUIScrollAnimator.h" +#endif + +#if __has_include("QMUISearchBar.h") +#import "QMUISearchBar.h" +#endif + +#if __has_include("QMUISearchController.h") +#import "QMUISearchController.h" +#endif + +#if __has_include("QMUISegmentedControl.h") +#import "QMUISegmentedControl.h" +#endif + +#if __has_include("QMUISheetPresentationNavigationBar.h") +#import "QMUISheetPresentationNavigationBar.h" +#endif + +#if __has_include("QMUISheetPresentationSupports.h") +#import "QMUISheetPresentationSupports.h" +#endif + +#if __has_include("QMUIStaticTableViewCellData.h") #import "QMUIStaticTableViewCellData.h" +#endif + +#if __has_include("QMUIStaticTableViewCellDataSource.h") #import "QMUIStaticTableViewCellDataSource.h" -#import "UITableView+QMUIStaticCell.h" +#endif -/// UIMainFrame -#import "QMUISearchController.h" -#import "QMUICommonViewController.h" -#import "QMUICommonTableViewController.h" -#import "QMUINavigationController.h" +#if __has_include("QMUITabBarViewController.h") #import "QMUITabBarViewController.h" +#endif + +#if __has_include("QMUITableView.h") +#import "QMUITableView.h" +#endif + +#if __has_include("QMUITableViewCell.h") +#import "QMUITableViewCell.h" +#endif + +#if __has_include("QMUITableViewHeaderFooterView.h") +#import "QMUITableViewHeaderFooterView.h" +#endif + +#if __has_include("QMUITableViewProtocols.h") +#import "QMUITableViewProtocols.h" +#endif + +#if __has_include("QMUITestView.h") +#import "QMUITestView.h" +#endif + +#if __has_include("QMUITextField.h") +#import "QMUITextField.h" +#endif + +#if __has_include("QMUITextView.h") +#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 + +#if __has_include("QMUIToastAnimator.h") +#import "QMUIToastAnimator.h" +#endif + +#if __has_include("QMUIToastBackgroundView.h") +#import "QMUIToastBackgroundView.h" +#endif + +#if __has_include("QMUIToastContentView.h") +#import "QMUIToastContentView.h" +#endif + +#if __has_include("QMUIToastView.h") +#import "QMUIToastView.h" +#endif + +#if __has_include("QMUIToolbarButton.h") +#import "QMUIToolbarButton.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 + +#if __has_include("UIActivityIndicatorView+QMUI.h") +#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 + +#if __has_include("UIBarItem+QMUIBadge.h") +#import "UIBarItem+QMUIBadge.h" +#endif + +#if __has_include("UIBezierPath+QMUI.h") +#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 + +#if __has_include("UICollectionView+QMUI.h") +#import "UICollectionView+QMUI.h" +#endif + +#if __has_include("UICollectionView+QMUICellSizeKeyCache.h") +#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 + +#if __has_include("UIFont+QMUI.h") +#import "UIFont+QMUI.h" +#endif + +#if __has_include("UIGestureRecognizer+QMUI.h") +#import "UIGestureRecognizer+QMUI.h" +#endif + +#if __has_include("UIImage+QMUI.h") +#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 + +#if __has_include("UIInterface+QMUI.h") +#import "UIInterface+QMUI.h" +#endif + +#if __has_include("UILabel+QMUI.h") +#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+QMUIBarProtocol.h") +#import "UINavigationBar+QMUIBarProtocol.h" +#endif + +#if __has_include("UINavigationController+NavigationBarTransition.h") +#import "UINavigationController+NavigationBarTransition.h" +#endif + +#if __has_include("UINavigationController+QMUI.h") +#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 + +#if __has_include("UISearchBar+QMUI.h") +#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 + +#if __has_include("UITableView+QMUI.h") +#import "UITableView+QMUI.h" +#endif + +#if __has_include("UITableView+QMUICellHeightKeyCache.h") +#import "UITableView+QMUICellHeightKeyCache.h" +#endif + +#if __has_include("UITableView+QMUIStaticCell.h") +#import "UITableView+QMUIStaticCell.h" +#endif + +#if __has_include("UITableViewCell+QMUI.h") +#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 + +#endif /* QMUIKit_h */ \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h new file mode 100644 index 00000000..e3c6a6f8 --- /dev/null +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h @@ -0,0 +1,97 @@ +/** + * 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. + */ + +// +// QMUICommonTableViewController.h +// qmui +// +// Created by QMUI Team on 14-6-24. +// + +#import "QMUICommonViewController.h" +#import "QMUITableView.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const QMUICommonTableViewControllerSectionHeaderIdentifier; +extern NSString *const QMUICommonTableViewControllerSectionFooterIdentifier; + +/** + * 可作为项目内所有 `UITableViewController` 的基类,注意是继承自 `QMUICommonViewController` 而不是 `UITableViewController`。 + * + * 一般通过 `initWithStyle:` 方法初始化,对于要生成 `UITableViewStylePlain` 类型的列表,推荐使用 `init` 方法。 + * + * 提供的功能包括: + * + * 1. 集成 `QMUISearchController`,可通过属性 `shouldShowSearchBar` 来快速为列表生成一个 searchBar 及 searchController,具体请查看 QMUICommonTableViewController (Search)。 + * 2. 支持仅设置 tableView:titleForHeaderInSection: 就能自动生成 sectionHeader 并且样式统一由配置表设置,无需编写 viewForHeaderInSection:、heightForHeaderInSection: 等方法。 + * 3. 自带一个 QMUIEmptyView,作为 tableView 的 subview,可用于显示 loading、空或错误提示语等。 + * + * @note emptyView 会从 tableHeaderView 的下方开始布局到 tableView 最底部,因此它会遮挡 tableHeaderView 之外的部分(比如 tableFooterView 和 cells ),你可以重写 layoutEmptyView 来改变这个布局方式 + * + * @see QMUISearchController + */ +@interface QMUICommonTableViewController : QMUICommonViewController + +- (instancetype)initWithStyle:(UITableViewStyle)style NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER; + +/** + * 初始化时调用的方法,会在两个 NS_DESIGNATED_INITIALIZER 方法中被调用,所以子类如果需要同时支持两个 NS_DESIGNATED_INITIALIZER 方法,则建议把初始化时要做的事情放到这个方法里。否则仅需重写要支持的那个 NS_DESIGNATED_INITIALIZER 方法即可。 + */ +- (void)didInitializeWithStyle:(UITableViewStyle)style NS_REQUIRES_SUPER; + +/// 获取当前的 `UITableViewStyle` +@property(nonatomic, assign, readonly) UITableViewStyle style; + +/// 当前的 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; + +@end + + +@interface QMUICommonTableViewController (QMUISubclassingHooks) + +/** + * 初始化 tableView,在 tableView getter 被调用时会触发,可重写这个方法并通过 self.tableView = xxx 来指定自定义的 tableView class,注意需要自行指定 dataSource 和 delegate,但不需要手动 add 到 self.view 上。一般情况下,有关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; + +/** + * 布局 tableView 的方法独立抽取出来,方便子类在需要自定义 tableView.frame 时能重写并且屏蔽掉 super 的代码。如果不独立一个方法而是放在 viewDidLayoutSubviews 里,子类就很难屏蔽 super 里对 tableView.frame 的修改。 + * 默认的实现是撑满 self.view,如果要自定义,可以写在这里而不调用 super,或者干脆重写这个方法但留空 + */ +- (void)layoutTableView; + +/** + * 是否需要在第一次进入界面时将tableHeaderView隐藏(通过调整self.tableView.contentOffset实现) + * + * 默认为NO + * + * @see QMUITableViewDelegate + */ +- (BOOL)shouldHideTableHeaderViewInitial; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m new file mode 100644 index 00000000..ae17a186 --- /dev/null +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m @@ -0,0 +1,323 @@ +/** + * 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. + */ + +// +// QMUICommonTableViewController.m +// qmui +// +// Created by QMUI Team on 14-6-24. +// + +#import "QMUICommonTableViewController.h" +#import "QMUICore.h" +#import "QMUITableView.h" +#import "QMUIEmptyView.h" +#import "QMUITableViewHeaderFooterView.h" +#import "UIScrollView+QMUI.h" +#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, assign) BOOL hasHideTableHeaderViewInitial; +@end + + +@implementation QMUICommonTableViewController + +- (instancetype)initWithStyle:(UITableViewStyle)style { + if (self = [super initWithNibName:nil bundle:nil]) { + [self didInitializeWithStyle:style]; + } + return self; +} + +- (instancetype)init { + return [self initWithStyle:UITableViewStylePlain]; +} + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + return [self init]; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self didInitializeWithStyle:UITableViewStylePlain]; + } + return self; +} + +- (void)didInitializeWithStyle:(UITableViewStyle)style { + _style = style; + self.hasHideTableHeaderViewInitial = NO; +} + +- (void)dealloc { + // 用下划线而不是self.xxx来访问tableView,避免dealloc时self.view尚未被加载,此时调用self.tableView反而会触发loadView + _tableView.dataSource = nil; + _tableView.delegate = nil; +} + +- (NSString *)description { +#ifdef DEBUG + if (![self isViewLoaded]) { + return [super description]; + } + + 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(\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 ? @"," : @""]; + } + [sectionCountString appendString:@"\t\t\t\t\t\t)"]; + result = [result stringByAppendingString:sectionCountString]; + } + return result; +#else + return [super description]; +#endif +} + +- (void)viewDidLoad { + [super viewDidLoad]; + if (self.tableView.backgroundColor) { + self.view.backgroundColor = self.tableView.backgroundColor;// 让 self.view 背景色跟随不同的 UITableViewStyle 走 + } +} + +- (void)initSubviews { + [super initSubviews]; + [self initTableView]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + if (!self.tableView.allowsMultipleSelection) { + [self qmui_animateAlongsideTransition:^(id _Nonnull context) { + [self.tableView qmui_clearsSelection]; + } completion:nil]; + } +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + [self layoutTableView]; + + [self hideTableHeaderViewInitialIfCanWithAnimated:NO force:NO]; + + [self layoutEmptyView]; +} + +#pragma mark - 工具方法 + +@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.adjustedContentInset.top + CGRectGetHeight(self.tableView.tableHeaderView.frame)); + [self.tableView setContentOffset:contentOffset animated:animated]; + self.hasHideTableHeaderViewInitial = YES; + } +} + +- (void)contentSizeCategoryDidChanged:(NSNotification *)notification { + [super contentSizeCategoryDidChanged:notification]; + if (self.viewLoaded) { + [self.tableView reloadData]; + } +} + +#pragma mark - 空列表视图 QMUIEmptyView + +- (void)handleTableViewContentInsetChangeEvent { + if (self.isEmptyViewShowing) { + [self layoutEmptyView]; + } +} + +- (void)showEmptyView { + [self.tableView addSubview:self.emptyView]; + [self layoutEmptyView]; +} + +// 注意,emptyView 的布局依赖于 tableView.contentInset,因此我们必须监听 tableView.contentInset 的变化以及时更新 emptyView 的布局 +- (BOOL)layoutEmptyView { + if (!_emptyView || !_emptyView.superview) { + return NO; + } + + UIEdgeInsets insets = self.tableView.adjustedContentInset; + + // 当存在 tableHeaderView 时,emptyView 的高度为 tableView 的高度减去 headerView 的高度 + if (self.tableView.tableHeaderView) { + self.emptyView.frame = CGRectMake(0, CGRectGetMaxY(self.tableView.tableHeaderView.frame), CGRectGetWidth(self.tableView.bounds) - UIEdgeInsetsGetHorizontalValue(insets), CGRectGetHeight(self.tableView.bounds) - UIEdgeInsetsGetVerticalValue(insets) - CGRectGetMaxY(self.tableView.tableHeaderView.frame)); + } else { + self.emptyView.frame = CGRectMake(0, 0, CGRectGetWidth(self.tableView.bounds) - UIEdgeInsetsGetHorizontalValue(insets), CGRectGetHeight(self.tableView.bounds) - UIEdgeInsetsGetVerticalValue(insets)); + } + return YES; +} + +#pragma mark - + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return 0; +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { + NSString *title = [self tableView:tableView realTitleForHeaderInSection:section]; + if (title) { + QMUITableViewHeaderFooterView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:QMUICommonTableViewControllerSectionHeaderIdentifier]; + headerView.parentTableView = tableView; + headerView.type = QMUITableViewHeaderFooterViewTypeHeader; + headerView.titleLabel.text = title; + return headerView; + } + return nil; +} + +- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section { + NSString *title = [self tableView:tableView realTitleForFooterInSection:section]; + if (title) { + QMUITableViewHeaderFooterView *footerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:QMUICommonTableViewControllerSectionFooterIdentifier]; + footerView.parentTableView = tableView; + footerView.type = QMUITableViewHeaderFooterViewTypeFooter; + footerView.titleLabel.text = title; + return footerView; + } + return nil; +} + +- (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) { + return UITableViewAutomaticDimension; + } + } + // 分别测试过 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) { + return UITableViewAutomaticDimension; + } + } + // 分别测试过 iOS 13 及以下的所有版本,最终总结,对于 Plain 类型的 tableView 而言,要去掉 header / footer 请使用 0,对于 Grouped 类型的 tableView 而言,要去掉 header / footer 请使用 CGFLOAT_MIN + return PreferredValueForTableViewStyle(tableView.style, 0, TableViewGroupedSectionFooterDefaultHeight, TableViewInsetGroupedSectionFooterDefaultHeight); +} + +// 是否有定义某个section的header title +- (NSString *)tableView:(UITableView *)tableView realTitleForHeaderInSection:(NSInteger)section { + if ([tableView.dataSource respondsToSelector:@selector(tableView:titleForHeaderInSection:)]) { + NSString *sectionTitle = [tableView.dataSource tableView:tableView titleForHeaderInSection:section]; + if (sectionTitle && sectionTitle.length > 0) { + return sectionTitle; + } + } + return nil; +} + +// 是否有定义某个section的footer title +- (NSString *)tableView:(UITableView *)tableView realTitleForFooterInSection:(NSInteger)section { + if ([tableView.dataSource respondsToSelector:@selector(tableView:titleForFooterInSection:)]) { + NSString *sectionFooter = [tableView.dataSource tableView:tableView titleForFooterInSection:section]; + if (sectionFooter && sectionFooter.length > 0) { + return sectionFooter; + } + } + return nil; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + return [[UITableViewCell alloc] init]; +} + +/** + * 监听 contentInset 的变化以及时更新 emptyView 的布局,详见 layoutEmptyView 方法的注释 + */ +- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView { + if (_tableView != scrollView) return; + [self handleTableViewContentInsetChangeEvent]; +} + +@end + + +@implementation QMUICommonTableViewController (QMUISubclassingHooks) + +- (void)initTableView { + if (!_tableView) { + 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; + } +} + +- (void)layoutTableView { + BOOL shouldChangeTableViewFrame = !CGRectEqualToRect(self.view.bounds, self.tableView.frame); + if (shouldChangeTableViewFrame) { + self.tableView.qmui_frameApplyTransform = self.view.bounds; + } +} + +- (BOOL)shouldHideTableHeaderViewInitial { + return NO; +} + +@end diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.h b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.h new file mode 100644 index 00000000..3275e0d9 --- /dev/null +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.h @@ -0,0 +1,191 @@ +/** + * 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. + */ + +// +// QMUICommonViewController.h +// qmui +// +// Created by QMUI Team on 14-6-22. +// + +#import +#import "QMUINavigationController.h" +#import "QMUIKeyboardManager.h" + +NS_ASSUME_NONNULL_BEGIN + +@class QMUINavigationTitleView; +@class QMUIEmptyView; + + +/** + * 可作为项目内所有 `UIViewController` 的基类,提供的功能包括: + * + * 1. 自带顶部标题控件 `QMUINavigationTitleView`,支持loading、副标题、下拉菜单,设置标题依然使用系统的 `-[UIViewController setTitle:]` 或 `-[UINavigationItem setTitle:]` 方法 + * + * 2. 自带空界面控件 `QMUIEmptyView`,支持显示loading、空文案、操作按钮 + * + * 3. 统一约定的常用接口,例如初始化 subview、设置顶部 `navigationItem`、底部 `toolbarItem`、响应系统的动态字体大小变化、...,从而保证相同类型的代码集中到同一个方法内,避免多人交叉维护时代码分散难以查找 + * + * 4. 配合 `QMUINavigationController` 使用时,可以得到 `willPopInNavigationControllerWithAnimated:`、`didPopInNavigationControllerWithAnimated:` 这两个时机 + * + * @see QMUINavigationTitleView + * @see QMUIEmptyView + */ +@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; + +/** + * 初始化时调用的方法,会在两个 NS_DESIGNATED_INITIALIZER 方法中被调用,所以子类如果需要同时支持两个 NS_DESIGNATED_INITIALIZER 方法,则建议把初始化时要做的事情放到这个方法里。否则仅需重写要支持的那个 NS_DESIGNATED_INITIALIZER 方法即可。 + */ +- (void)didInitialize NS_REQUIRES_SUPER; + +/** + * QMUICommonViewController默认都会增加一个QMUINavigationTitleView的titleView,然后重写了setTitle来间接设置titleView的值。所以设置title的时候就跟系统的接口一样:self.title = xxx。 + * + * 同时,QMUINavigationTitleView提供了更多的功能,具体可以参考QMUINavigationTitleView的文档。
+ * @see QMUINavigationTitleView + */ +@property(nullable, nonatomic, strong, readonly) QMUINavigationTitleView *titleView; + +/** + * 修改当前界面要支持的横竖屏方向,默认为 SupportedOrientationMask + */ +@property(nonatomic, assign) UIInterfaceOrientationMask supportedOrientationMask; + +/** + * 空列表控件,支持显示提示文字、loading、操作按钮,该属性懒加载 + */ +@property(nullable, nonatomic, strong) QMUIEmptyView *emptyView; + +/// 当前self.emptyView是否显示 +@property(nonatomic, assign, readonly, getter = isEmptyViewShowing) BOOL emptyViewShowing; + +/** + * 显示emptyView + * emptyView 的以下系列接口可以按需进行重写 + * + * @see QMUIEmptyView + */ +- (void)showEmptyView; + +/** + * 显示loading的emptyView + */ +- (void)showEmptyViewWithLoading; + +/** + * 显示带text、detailText、button的emptyView + */ +- (void)showEmptyViewWithText:(nullable NSString *)text + detailText:(nullable NSString *)detailText + buttonTitle:(nullable NSString *)buttonTitle + buttonAction:(nullable SEL)action; + +/** + * 显示带image、text、detailText、button的emptyView + */ +- (void)showEmptyViewWithImage:(nullable UIImage *)image + text:(nullable NSString *)text + detailText:(nullable NSString *)detailText + buttonTitle:(nullable NSString *)buttonTitle + buttonAction:(nullable SEL)action; + +/** + * 显示带loading、image、text、detailText、button的emptyView + */ +- (void)showEmptyViewWithLoading:(BOOL)showLoading + image:(nullable UIImage *)image + text:(nullable NSString *)text + detailText:(nullable NSString *)detailText + buttonTitle:(nullable NSString *)buttonTitle + buttonAction:(nullable SEL)action; + +/** + * 隐藏emptyView + */ +- (void)hideEmptyView; + +/** + * 布局emptyView,如果emptyView没有被初始化或者没被添加到界面上,则直接忽略掉。 + * + * 如果有特殊的情况,子类可以重写,实现自己的样式 + * + * @return YES表示成功进行一次布局,NO表示本次调用并没有进行布局操作(例如emptyView还没被初始化) + */ +- (BOOL)layoutEmptyView; + +@end + + +@interface QMUICommonViewController (QMUISubclassingHooks) + +/** + * 负责初始化和设置controller里面的view,也就是self.view的subView。目的在于分类代码,所以与view初始化的相关代码都写在这里。 + * + * @warning initSubviews只负责subviews的init,不负责布局。布局相关的代码应该写在 viewDidLayoutSubviews + */ +- (void)initSubviews NS_REQUIRES_SUPER; + +/** + * 负责设置和更新navigationItem,包括title、leftBarButtonItem、rightBarButtonItem。viewWillAppear 里面会自动调用,业务也可以在需要的时候自行调用。目的在于分类代码,所有与navigationItem相关的代码都写在这里。在需要修改navigationItem的时候都统一调用这个接口。 + */ +- (void)setupNavigationItems NS_REQUIRES_SUPER; + +/** + * 负责设置和更新toolbarItem。在viewWillAppear里面自动调用(因为toolbar是navigationController的,是每个界面公用的,所以必须在每个界面的viewWillAppear时更新,不能放在viewDidLoad里),允许手动调用。目的在于分类代码,所有与toolbarItem相关的代码都写在这里。在需要修改toolbarItem的时候都只调用这个接口。 + */ +- (void)setupToolbarItems NS_REQUIRES_SUPER; + +/** + * 动态字体的回调函数。 + * + * 交给子类重写,当系统字体发生变化的时候,会调用这个方法,一些font的设置或者reloadData可以放在里面 + * + * @param notification test + */ +- (void)contentSizeCategoryDidChanged:(NSNotification *)notification; + +@end + +@interface QMUICommonViewController (QMUINavigationController) + +/** + 从 QMUINavigationControllerAppearanceDelegate 系列接口获取当前界面希望的导航栏样式并设置到导航栏上 + */ +- (void)updateNavigationBarAppearance; + +@end + +/** + * 为了方便实现“点击空白区域降下键盘”的需求,QMUICommonViewController 内部集成一个 tap 手势对象并添加到 self.view 上,而业务只需要通过重写 -shouldHideKeyboardWhenTouchInView: 方法并根据当前被点击的 view 返回一个 BOOL 来控制键盘的显隐即可。 + * @note 为了避免不必要的事件拦截,集成的手势 hideKeyboardTapGestureRecognizer: + * 1. 默认的 enabled = NO。 + * 2. 如果当前 viewController 或其父类(非 QMUICommonViewController 那个层级的父类)没重写 -shouldHideKeyboardWhenTouchInView:,则永远 enabled = NO。 + * 3. 在键盘升起时,并且当前 viewController 重写了 -shouldHideKeyboardWhenTouchInView: 且处于可视状态下,此时手势的 enabled 才会被修改为 YES,并且在键盘消失时置为 NO。 + */ +@interface QMUICommonViewController (QMUIKeyboard) + +/// 在 viewDidLoad 内初始化,并且 gestureRecognizerShouldBegin: 必定返回 NO。 +@property(nullable, nonatomic, strong, readonly) UITapGestureRecognizer *hideKeyboardTapGestureRecognizer; +@property(nullable, nonatomic, strong, readonly) QMUIKeyboardManager *hideKeyboardManager; + +/** + * 当用户点击界面上某个非 UITextField、UITextView 的 view 时,如果此时键盘处于升起状态,则可通过重写这个方法并返回一个 YES 来达到“点击空白区域自动降下键盘”的需求。默认返回 NO,也即不处理键盘。 + * @note 注意如果被点击的 view 本身消耗了事件(iOS 11 下测试得到这种类型的所有系统的 view 仅有 UIButton 和 UISwitch),则这个方法并不会被触发。 + * @note 有可能参数传进去的 view 是某个 subview 的 subview,所以建议用 isDescendantOfView: 来判断是否点到了某个目标 subview + */ +- (BOOL)shouldHideKeyboardWhenTouchInView:(nullable UIView *)view; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.m b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.m new file mode 100644 index 00000000..24106ae7 --- /dev/null +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUICommonViewController.m @@ -0,0 +1,343 @@ +/** + * 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. + */ + +// +// QMUICommonViewController.m +// qmui +// +// Created by QMUI Team on 14-6-22. +// + +#import "QMUICommonViewController.h" +#import "QMUICore.h" +#import "QMUINavigationTitleView.h" +#import "QMUIEmptyView.h" +#import "NSString+QMUI.h" +#import "NSObject+QMUI.h" +#import "UIViewController+QMUI.h" +#import "UIGestureRecognizer+QMUI.h" +#import "UIView+QMUI.h" + +@interface QMUIViewControllerHideKeyboardDelegateObject : NSObject + +@property(nonatomic, weak) QMUICommonViewController *viewController; + +- (instancetype)initWithViewController:(QMUICommonViewController *)viewController; +@end + +@interface QMUICommonViewController () { + UITapGestureRecognizer *_hideKeyboardTapGestureRecognizer; + QMUIKeyboardManager *_hideKeyboardManager; + QMUIViewControllerHideKeyboardDelegateObject *_hideKeyboadDelegateObject; +} + +@property(nonatomic,strong,readwrite) QMUINavigationTitleView *titleView; +@end + +@implementation QMUICommonViewController + +#pragma mark - 生命周期 + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { + [self didInitialize]; + } + return self; +} + +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self didInitialize]; + } + return self; +} + +- (void)didInitialize { + self.titleView = [[QMUINavigationTitleView alloc] init]; + self.titleView.title = self.title;// 从 storyboard 初始化的话,可能带有 self.title 的值 + self.navigationItem.titleView = self.titleView; + + // 不管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)viewDidLoad { + [super viewDidLoad]; + if (!self.view.backgroundColor && QMUICMIActivated) {// nib 里可能设置了,所以做个 if 的判断 + self.view.backgroundColor = UIColorForBackground; + } + + // 点击空白区域降下键盘 QMUICommonViewController (QMUIKeyboard) + // 如果子类重写了才初始化这些对象(即便子类 return NO) + BOOL shouldEnabledKeyboardObject = [self qmui_hasOverrideMethod:@selector(shouldHideKeyboardWhenTouchInView:) ofSuperclass:[QMUICommonViewController class]]; + if (shouldEnabledKeyboardObject) { + _hideKeyboadDelegateObject = [[QMUIViewControllerHideKeyboardDelegateObject alloc] initWithViewController:self]; + + _hideKeyboardTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:nil action:NULL]; + self.hideKeyboardTapGestureRecognizer.delegate = _hideKeyboadDelegateObject; + self.hideKeyboardTapGestureRecognizer.enabled = NO; + [self.view addGestureRecognizer:self.hideKeyboardTapGestureRecognizer]; + + _hideKeyboardManager = [[QMUIKeyboardManager alloc] initWithDelegate:_hideKeyboadDelegateObject]; + } + + [self initSubviews]; +} + +- (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 (self.hideKeyboardManager && [QMUIKeyboardManager isKeyboardVisible]) { + self.hideKeyboardTapGestureRecognizer.enabled = YES; + } +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + [self layoutEmptyView]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + [self setupNavigationItems]; + [self setupToolbarItems]; +} + +#pragma mark - 空列表视图 QMUIEmptyView + +@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 { + [_emptyView removeFromSuperview]; +} + +- (BOOL)isEmptyViewShowing { + return _emptyView && _emptyView.superview; +} + +- (void)showEmptyViewWithLoading { + [self showEmptyView]; + [self.emptyView setImage:nil]; + [self.emptyView setLoadingViewHidden:NO]; + [self.emptyView setTextLabelText:nil]; + [self.emptyView setDetailTextLabelText:nil]; + [self.emptyView setActionButtonTitle:nil]; +} + +- (void)showEmptyViewWithText:(NSString *)text + detailText:(NSString *)detailText + buttonTitle:(NSString *)buttonTitle + buttonAction:(SEL)action { + [self showEmptyViewWithLoading:NO image:nil text:text detailText:detailText buttonTitle:buttonTitle buttonAction:action]; +} + +- (void)showEmptyViewWithImage:(UIImage *)image + text:(NSString *)text + detailText:(NSString *)detailText + buttonTitle:(NSString *)buttonTitle + buttonAction:(SEL)action { + [self showEmptyViewWithLoading:NO image:image text:text detailText:detailText buttonTitle:buttonTitle buttonAction:action]; +} + +- (void)showEmptyViewWithLoading:(BOOL)showLoading + image:(UIImage *)image + text:(NSString *)text + detailText:(NSString *)detailText + buttonTitle:(NSString *)buttonTitle + buttonAction:(SEL)action { + [self showEmptyView]; + [self.emptyView setLoadingViewHidden:!showLoading]; + [self.emptyView setImage:image]; + [self.emptyView setTextLabelText:text]; + [self.emptyView setDetailTextLabelText:detailText]; + [self.emptyView setActionButtonTitle:buttonTitle]; + [self.emptyView.actionButton removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents]; + [self.emptyView.actionButton addTarget:self action:action forControlEvents:UIControlEventTouchUpInside]; +} + +- (BOOL)layoutEmptyView { + if (_emptyView) { + // 由于为self.emptyView设置frame时会调用到self.view,为了避免导致viewDidLoad提前触发,这里需要判断一下self.view是否已经被初始化 + BOOL viewDidLoad = self.emptyView.superview && [self isViewLoaded]; + if (viewDidLoad) { + CGSize newEmptyViewSize = self.emptyView.superview.bounds.size; + CGSize oldEmptyViewSize = self.emptyView.frame.size; + if (!CGSizeEqualToSize(newEmptyViewSize, oldEmptyViewSize)) { + self.emptyView.qmui_frameApplyTransform = CGRectFlatMake(CGRectGetMinX(self.emptyView.frame), CGRectGetMinY(self.emptyView.frame), newEmptyViewSize.width, newEmptyViewSize.height); + } + return YES; + } + } + + return NO; +} + +#pragma mark - 屏幕旋转 + +- (BOOL)shouldAutorotate { + return YES; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + return self.supportedOrientationMask; +} + +@end + +@implementation QMUICommonViewController (QMUISubclassingHooks) + +- (void)initSubviews { + // 子类重写 +} + +- (void)setupNavigationItems { + // 子类重写 +} + +- (void)setupToolbarItems { + // 子类重写 +} + +- (void)contentSizeCategoryDidChanged:(NSNotification *)notification { + // 子类重写 +} + +@end + +@implementation QMUICommonViewController (QMUINavigationController) + +- (void)updateNavigationBarAppearance { + + UINavigationBar *navigationBar = self.navigationController.navigationBar; + if (!navigationBar) return; + + if ([self respondsToSelector:@selector(qmui_navigationBarBackgroundImage)]) { + [navigationBar setBackgroundImage:[self qmui_navigationBarBackgroundImage] forBarMetrics:UIBarMetricsDefault]; + } + if ([self respondsToSelector:@selector(qmui_navigationBarBarTintColor)]) { + navigationBar.barTintColor = [self qmui_navigationBarBarTintColor]; + } + if ([self respondsToSelector:@selector(qmui_navigationBarStyle)]) { + navigationBar.barStyle = [self qmui_navigationBarStyle]; + } + if ([self respondsToSelector:@selector(qmui_navigationBarShadowImage)]) { + navigationBar.shadowImage = [self qmui_navigationBarShadowImage]; + } + if ([self respondsToSelector:@selector(qmui_navigationBarTintColor)]) { + navigationBar.tintColor = [self qmui_navigationBarTintColor]; + } + if ([self respondsToSelector:@selector(qmui_titleViewTintColor)]) { + self.titleView.tintColor = [self qmui_titleViewTintColor]; + } +} + +#pragma mark - + +- (BOOL)preferredNavigationBarHidden { + return NavigationBarHiddenInitially; +} + +- (void)viewControllerKeepingAppearWhenSetViewControllersWithAnimated:(BOOL)animated { + // 通常和 viewWillAppear: 里做的事情保持一致 + [self setupNavigationItems]; + [self setupToolbarItems]; +} + +@end + +@implementation QMUICommonViewController (QMUIKeyboard) + +- (UITapGestureRecognizer *)hideKeyboardTapGestureRecognizer { + return _hideKeyboardTapGestureRecognizer; +} + +- (QMUIKeyboardManager *)hideKeyboardManager { + return _hideKeyboardManager; +} + +- (BOOL)shouldHideKeyboardWhenTouchInView:(UIView *)view { + // 子类重写,默认返回 NO,也即不主动干预键盘的状态 + return NO; +} + +@end + +@implementation QMUIViewControllerHideKeyboardDelegateObject + +- (instancetype)initWithViewController:(QMUICommonViewController *)viewController { + if (self = [super init]) { + self.viewController = viewController; + } + return self; +} + +#pragma mark - + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { + if (gestureRecognizer != self.viewController.hideKeyboardTapGestureRecognizer) { + return YES; + } + + if (![QMUIKeyboardManager isKeyboardVisible]) { + return NO; + } + + UIView *targetView = gestureRecognizer.qmui_targetView; + + // 点击了本身就是输入框的 view,就不要降下键盘了 + if ([targetView isKindOfClass:[UITextField class]] || [targetView isKindOfClass:[UITextView class]]) { + return NO; + } + + if ([self.viewController shouldHideKeyboardWhenTouchInView:targetView]) { + [self.viewController.view endEditing:YES]; + } + return NO; +} + +#pragma mark - + +- (void)keyboardWillShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + if (![self.viewController qmui_isViewLoadedAndVisible]) return; + self.viewController.hideKeyboardTapGestureRecognizer.enabled = YES; +} + +- (void)keyboardWillHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + self.viewController.hideKeyboardTapGestureRecognizer.enabled = NO; +} + +@end diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.h b/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.h new file mode 100644 index 00000000..9abca58c --- /dev/null +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.h @@ -0,0 +1,180 @@ +/** + * 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. + */ + +// +// QMUINavigationController.h +// qmui +// +// Created by QMUI Team on 14-6-24. +// + +#import + + +@interface QMUINavigationController : UINavigationController + +@end + +@interface QMUINavigationController (UISubclassingHooks) + +/** + * 每个界面Controller在即将展示的时候被调用,在`UINavigationController`的方法`navigationController:willShowViewController:animated:`中会自动被调用,同时因为如果把一个界面dismiss后回来此时并不会调用`navigationController:willShowViewController`,所以需要在`viewWillAppear`里面也会调用一次。 + */ +- (void)willShowViewController:(nonnull UIViewController *)viewController animated:(BOOL)animated NS_REQUIRES_SUPER; + +/** + * 同上 + */ +- (void)didShowViewController:(nonnull UIViewController *)viewController animated:(BOOL)animated NS_REQUIRES_SUPER; + +@end + + +/// 与 QMUINavigationController push/pop 相关的一些方法 +@protocol QMUINavigationControllerTransitionDelegate + +@optional + +/** + * 当前界面正处于手势返回的过程中,可自行通过 gestureRecognizer.state 来区分手势返回的各个阶段。手势返回有多个阶段(手势返回开始、拖拽过程中、松手并成功返回、松手但不切换界面),不同阶段的 viewController 的状态可能不一样。 + * @param navigationController 当前正在手势返回的 QMUINavigationController,由于某些阶段下无法通过 vc.navigationController 获取到 nav 的引用,所以直接传一个参数 + * @param gestureRecognizer 手势对象 + * @param viewControllerWillDisappear 手势返回中顶部的那个 vc + * @param viewControllerWillAppear 手势返回中背后的那个 vc + */ +- (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: 方法会被调用: + * 1. popViewControllerAnimated: + * 2. popToViewController:animated: + * 3. popToRootViewControllerAnimated: + * 4. setViewControllers:animated: + * + * 此时 self 仍存在于 self.navigationController.viewControllers 堆栈内。 + * + * 在 ARC 环境下,viewController 可能被放在 autorelease 池中,因此 viewController 被pop后不一定立即被销毁,所以一些对实时性要求很高的内存管理逻辑可以写在这里(而不是写在dealloc内) + * + * @warning 不要尝试将 willPopInNavigationControllerWithAnimated: 视为点击返回按钮的回调,因为导致 viewController 被 pop 的情况不止点击返回按钮这一途径。系统的返回按钮是无法添加回调的,只能使用自定义的返回按钮。 + */ +- (void)willPopInNavigationControllerWithAnimated:(BOOL)animated; + +/** + * 在 self.navigationController 进行以下 4 个操作后,相应的 viewController 的 didPopInNavigationControllerWithAnimated: 方法会被调用: + * 1. popViewControllerAnimated: + * 2. popToViewController:animated: + * 3. popToRootViewControllerAnimated: + * 4. setViewControllers:animated: + * + * 此时 self.navigationController 仍有值,但 self 已经不在 viewControllers 数组内。 + * + * @warning 这个方法被调用并不意味着 self 最终一定会被 pop 掉,例如手势返回被触发时就会调用这个方法,但如果中途取消手势,self 依然会回到 viewControllers 内。 + */ +- (void)didPopInNavigationControllerWithAnimated:(BOOL)animated; + +/** + * 当通过 setViewControllers:animated: 来修改 viewController 的堆栈时,如果参数 viewControllers.lastObject 与当前的 self.viewControllers.lastObject 不相同,则意味着会产生界面的切换,这种情况系统会自动调用两个切换的界面的生命周期方法,但如果两者相同,则意味着并不会产生界面切换,此时之前就已经在显示的那个 viewController 的 viewWillAppear:、viewDidAppear: 并不会被调用,那如果用户确实需要在这个时候修改一些界面元素,则找不到一个时机。所以这个方法就是提供这样一个时机给用户修改界面元素。 + */ +- (void)viewControllerKeepingAppearWhenSetViewControllersWithAnimated:(BOOL)animated; + +@end + + +/// 与 QMUINavigationController 外观样式相关的方法 +@protocol QMUINavigationControllerAppearanceDelegate + +@optional + +/// 设置 titleView 的 tintColor +- (nullable UIColor *)qmui_titleViewTintColor; + +/// 设置导航栏的背景图,默认为 NavBarBackgroundImage +- (nullable UIImage *)qmui_navigationBarBackgroundImage; + +/// 设置导航栏底部的分隔线图片,默认为 NavBarShadowImage,必须在 navigationBar 设置了背景图后才有效(系统限制如此) +- (nullable UIImage *)qmui_navigationBarShadowImage; + +/// 设置当前导航栏的 barTintColor,默认为 NavBarBarTintColor +- (nullable UIColor *)qmui_navigationBarBarTintColor; + +/// 设置当前导航栏的 barStyle,默认为 NavBarStyle +- (UIBarStyle)qmui_navigationBarStyle; + +/// 设置当前导航栏的 UIBarButtonItem 的 tintColor,默认为NavBarTintColor +- (nullable UIColor *)qmui_navigationBarTintColor; + +/// 设置系统返回按钮title,如果返回nil则使用系统默认的返回按钮标题。当实现了这个方法时,会无视配置表 NeedsBackBarButtonItemTitle 的值 +- (nullable NSString *)qmui_backBarButtonItemTitleWithPreviousViewController:(nullable UIViewController *)viewController; + +@end + + +/// 与 QMUINavigationController 控制 navigationBar 显隐/动画相关的方法 +@protocol QMUICustomNavigationBarTransitionDelegate + +@optional + +/// 设置每个界面导航栏的显示/隐藏,为了减少对项目的侵入性,默认不开启这个接口的功能,只有当 shouldCustomizeNavigationBarTransitionIfHideable 返回 YES 时才会开启此功能。如果需要全局开启,那么就在 Controller 基类里面返回 YES;如果是老项目并不想全局使用此功能,那么则可以在单独的界面里面开启。 +- (BOOL)preferredNavigationBarHidden; + +/** + * 当切换界面时,如果不同界面导航栏的显隐状态不同,可以通过 shouldCustomizeNavigationBarTransitionIfHideable 设置是否需要接管导航栏的显示和隐藏。从而不需要在各自的界面的 viewWillAppear 和 viewWillDisappear 里面去管理导航栏的状态。 + * @see UINavigationController+NavigationBarTransition.h + * @see preferredNavigationBarHidden + */ +- (BOOL)shouldCustomizeNavigationBarTransitionIfHideable; + +/** + * 设置导航栏转场的时候是否需要使用自定义的 push / pop transition 效果。
+ * 如果前后两个界面 controller 返回的 key 不一致,那么则说明需要自定义。
+ * 不实现这个方法,或者实现了但返回 nil,都视为希望使用默认样式。
+ * @warning 四个老接口 shouldCustomNavigationBarTransitionxxx 已经废弃不建议使用,不过还是会支持,建议都是用新接口 + * @see UINavigationController+NavigationBarTransition.h + * @see 配置表有开关 AutomaticCustomNavigationBarTransitionStyle 支持自动判断样式,无需实现这个方法 + */ +- (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 + */ +- (nullable UIColor *)containerViewBackgroundColorWhenTransitioning; + +@end + + +/** + * 配合 QMUINavigationController 使用,当 navController 里的 UIViewController 实现了这个协议时,则可得到协议里各个方法的功能。 + * QMUICommonViewController、QMUICommonTableViewController 默认实现了这个协议,所以子类无需再手动实现一遍。 + */ +@protocol QMUINavigationControllerDelegate + +@end diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.m b/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.m new file mode 100644 index 00000000..bedcc0ac --- /dev/null +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUINavigationController.m @@ -0,0 +1,652 @@ +/** + * 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. + */ + +// +// QMUINavigationController.m +// qmui +// +// Created by QMUI Team on 14-6-24. +// + +#import "QMUINavigationController.h" +#import "QMUICore.h" +#import "QMUINavigationTitleView.h" +#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 + +- (void)qmui_viewControllerDidInvokeViewWillAppear:(UIViewController *)viewController; + +@end + +@interface _QMUINavigationControllerDelegator : NSObject + +@property(nonatomic, weak) QMUINavigationController *navigationController; +@end + +@interface QMUINavigationController () + +@property(nonatomic, strong) _QMUINavigationControllerDelegator *delegator; + +/// 记录当前是否正在 push/pop 界面的动画过程,如果动画尚未结束,不应该继续 push/pop 其他界面。 +/// 在 getter 方法里会根据配置表开关 PreventConcurrentNavigationControllerTransitions 的值来控制这个属性是否生效。 +@property(nonatomic, assign) BOOL isViewControllerTransiting; + +/// 即将要被pop的controller +@property(nonatomic, weak) UIViewController *viewControllerPopping; + +@end + +@interface UIViewController (QMUINavigationControllerTransition) + +@property(nonatomic, weak) id qmui_viewWillAppearNotifyDelegate; + +@end + +@implementation UIViewController (QMUINavigationControllerTransition) + ++ (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) { + + // 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]; + } + }; + }); + + 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; + }; + }); + + 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; + }; + }); + }); +} + +static char kAssociatedObjectKey_qmui_viewWillAppearNotifyDelegate; +- (void)setQmui_viewWillAppearNotifyDelegate:(id)qmui_viewWillAppearNotifyDelegate { + objc_setAssociatedObject(self, &kAssociatedObjectKey_qmui_viewWillAppearNotifyDelegate, [[QMUIWeakObjectContainer alloc] initWithObject:qmui_viewWillAppearNotifyDelegate], OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (id)qmui_viewWillAppearNotifyDelegate { + QMUIWeakObjectContainer *weakContainer = objc_getAssociatedObject(self, &kAssociatedObjectKey_qmui_viewWillAppearNotifyDelegate); + if (weakContainer.isQMUIWeakObjectContainer) { + id notifyDelegate = [weakContainer object]; + return notifyDelegate; + } + return nil; +} + +@end + +@implementation QMUINavigationController + +#pragma mark - 生命周期函数 && 基类方法重写 + +- (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; + + BeginIgnoreDeprecatedWarning + [self didInitialize]; + EndIgnoreDeprecatedWarning +} + +- (void)didInitialize { +} + +- (void)dealloc { + self.delegate = nil; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + // 手势允许多次addTarget + [self.interactivePopGestureRecognizer addTarget:self action:@selector(handleInteractivePopGestureRecognizer:)]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + [self willShowViewController:self.topViewController animated:animated]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [self didShowViewController:self.topViewController animated:animated]; +} + +- (UIViewController *)popViewControllerAnimated:(BOOL)animated { + if (self.viewControllers.count < 2) { + // 只剩 1 个 viewController 或者不存在 viewController 时,调用 popViewControllerAnimated: 后不会有任何变化,所以不需要触发 willPop / didPop + return [super popViewControllerAnimated:animated]; + } + + UIViewController *viewController = [self topViewController]; + self.viewControllerPopping = viewController; + + if (animated) { + self.viewControllerPopping.qmui_viewWillAppearNotifyDelegate = self; + + self.isViewControllerTransiting = YES; + } + + if ([viewController respondsToSelector:@selector(willPopInNavigationControllerWithAnimated:)]) { + [((UIViewController *)viewController) willPopInNavigationControllerWithAnimated:animated]; + } + + // QMUILog(@"NavigationItem", @"call popViewControllerAnimated:%@, current viewControllers = %@", StringFromBOOL(animated), self.viewControllers); + + viewController = [super popViewControllerAnimated:animated]; + + // QMUILog(@"NavigationItem", @"pop viewController: %@", viewController); + + if ([viewController respondsToSelector:@selector(didPopInNavigationControllerWithAnimated:)]) { + [((UIViewController *)viewController) didPopInNavigationControllerWithAnimated:animated]; + } + return viewController; +} + +- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated { + if (!viewController || self.topViewController == viewController) { + // 当要被 pop 到的 viewController 已经处于最顶层时,调用 super 默认也是什么都不做,所以直接 return 掉 + return [super popToViewController:viewController animated:animated]; + } + + self.viewControllerPopping = self.topViewController; + + if (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]; + if (viewControllerPopping == viewController) { + break; + } + + if ([viewControllerPopping respondsToSelector:@selector(willPopInNavigationControllerWithAnimated:)]) { + BOOL animatedArgument = i == self.viewControllers.count - 1 ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop + [((UIViewController *)viewControllerPopping) willPopInNavigationControllerWithAnimated:animatedArgument]; + } + } + + NSArray *poppedViewControllers = [super popToViewController:viewController animated:animated]; + + // did pop + for (NSInteger i = poppedViewControllers.count - 1; i >= 0; i--) { + UIViewController *viewControllerPopped = poppedViewControllers[i]; + if ([viewControllerPopped respondsToSelector:@selector(didPopInNavigationControllerWithAnimated:)]) { + BOOL animatedArgument = i == poppedViewControllers.count - 1 ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop + [((UIViewController *)viewControllerPopped) didPopInNavigationControllerWithAnimated:animatedArgument]; + } + } + + return poppedViewControllers; +} + +- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated { + // 在配合 tabBarItem 使用的情况下,快速重复点击相同 item 可能会重复调用 popToRootViewControllerAnimated:,而此时其实已经处于 rootViewController 了,就没必要继续走后续的流程,否则一些变量会得不到重置。 + if (self.topViewController == self.qmui_rootViewController) { + return nil; + } + + self.viewControllerPopping = self.topViewController; + + if (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]; + if ([viewControllerPopping respondsToSelector:@selector(willPopInNavigationControllerWithAnimated:)]) { + BOOL animatedArgument = i == self.viewControllers.count - 1 ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop + [((UIViewController *)viewControllerPopping) willPopInNavigationControllerWithAnimated:animatedArgument]; + } + } + + NSArray * poppedViewControllers = [super popToRootViewControllerAnimated:animated]; + + // did pop + for (NSInteger i = poppedViewControllers.count - 1; i >= 0; i--) { + UIViewController *viewControllerPopped = poppedViewControllers[i]; + if ([viewControllerPopped respondsToSelector:@selector(didPopInNavigationControllerWithAnimated:)]) { + BOOL animatedArgument = i == poppedViewControllers.count - 1 ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop + [((UIViewController *)viewControllerPopped) didPopInNavigationControllerWithAnimated:animatedArgument]; + } + } + return poppedViewControllers; +} + +- (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated { + UIViewController *topViewController = self.topViewController; + + // will pop + NSMutableArray *viewControllersPopping = self.viewControllers.mutableCopy; + [viewControllersPopping removeObjectsInArray:viewControllers]; + [viewControllersPopping enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(UIViewController * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if ([obj respondsToSelector:@selector(willPopInNavigationControllerWithAnimated:)]) { + BOOL animatedArgument = obj == topViewController ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop + [((UIViewController *)obj) willPopInNavigationControllerWithAnimated:animatedArgument]; + } + }]; + + // 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 + [viewControllersPopping enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(UIViewController * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if ([obj respondsToSelector:@selector(didPopInNavigationControllerWithAnimated:)]) { + BOOL animatedArgument = obj == topViewController ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop + [((UIViewController *)obj) didPopInNavigationControllerWithAnimated:animatedArgument]; + } + }]; + + // 操作前后如果 topViewController 没发生变化,则为它调用一个特殊的时机 + if (topViewController == viewControllers.lastObject) { + if ([topViewController respondsToSelector:@selector(viewControllerKeepingAppearWhenSetViewControllersWithAnimated:)]) { + [((UIViewController *)topViewController) viewControllerKeepingAppearWhenSetViewControllersWithAnimated:animated]; + } + } +} + +- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { + if (!viewController) return; + + 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; + } + + // 增加 self.view.window 作为判断条件是因为当 UINavigationController 不可见时(例如上面盖着一个 present 起来的 vc,或者 nav 所在的 tabBar 切到别的 tab 去了),pushViewController 会被执行,但 navigationController:didShowViewController:animated: 的 delegate 不会被触发,导致 isViewControllerTransiting 的标志位无法正确恢复,所以做个保护。 + // https://github.com/Tencent/QMUI_iOS/issues/261 + if (animated && self.isViewLoaded && self.view.window) { + self.isViewControllerTransiting = YES; + } + + // 在 push 前先设置好返回按钮的文字 + [self updateBackItemTitleWithCurrentViewController:self.topViewController nextViewController:viewController]; + + [super pushViewController:viewController animated:animated]; + + // 某些情况下 push 操作可能会被系统拦截,实际上该 push 并不生效,这种情况下应当恢复相关标志位,否则会影响后续的 push 操作 + // https://github.com/Tencent/QMUI_iOS/issues/426 + if (![self.viewControllers containsObject:viewController]) { + self.isViewControllerTransiting = NO; + } +} + +- (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 { + // 如果配置表里这个开关关闭,则为了使 isViewControllerTransiting 功能失效,强制返回 NO + if (!PreventConcurrentNavigationControllerTransitions) { + return NO; + } + return _isViewControllerTransiting; +} + +// 接管系统手势返回的回调 +- (void)handleInteractivePopGestureRecognizer:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer { + UIGestureRecognizerState state = gestureRecognizer.state; + + UIViewController *viewControllerWillDisappear = [self.transitionCoordinator viewControllerForKey:UITransitionContextFromViewControllerKey]; + UIViewController *viewControllerWillAppear = [self.transitionCoordinator viewControllerForKey:UITransitionContextToViewControllerKey]; + + viewControllerWillDisappear.qmui_poppingByInteractivePopGestureRecognizer = YES; + viewControllerWillDisappear.qmui_willAppearByInteractivePopGestureRecognizer = NO; + + viewControllerWillAppear.qmui_poppingByInteractivePopGestureRecognizer = NO; + viewControllerWillAppear.qmui_willAppearByInteractivePopGestureRecognizer = YES; + + if (state == UIGestureRecognizerStateBegan) { + // UIGestureRecognizerStateBegan 对应 viewWillAppear:,只要在 viewWillAppear: 里的修改都是安全的,但只要过了 viewWillAppear:,后续的修改都是不安全的,所以这里用 dispatch 的方式将标志位的赋值放到 viewWillAppear: 的下一个 Runloop 里 + dispatch_async(dispatch_get_main_queue(), ^{ + viewControllerWillDisappear.qmui_navigationControllerPopGestureRecognizerChanging = YES; + viewControllerWillAppear.qmui_navigationControllerPopGestureRecognizerChanging = YES; + }); + } else if (state > UIGestureRecognizerStateChanged) { + viewControllerWillDisappear.qmui_navigationControllerPopGestureRecognizerChanging = NO; + viewControllerWillAppear.qmui_navigationControllerPopGestureRecognizerChanging = NO; + } + + if (state == UIGestureRecognizerStateEnded) { + if (self.transitionCoordinator.cancelled) { + QMUILog(NSStringFromClass(self.class), @"interactivePopGestureRecognizer canceled"); + UIViewController *temp = viewControllerWillDisappear; + viewControllerWillDisappear = viewControllerWillAppear; + viewControllerWillAppear = temp; + } else { + 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]; + } + + if ([viewControllerWillAppear respondsToSelector:@selector(navigationController:poppingByInteractiveGestureRecognizer:viewControllerWillDisappear:viewControllerWillAppear:)]) { + [((UIViewController *)viewControllerWillAppear) navigationController:self poppingByInteractiveGestureRecognizer:gestureRecognizer viewControllerWillDisappear:viewControllerWillDisappear viewControllerWillAppear:viewControllerWillAppear]; + } + EndIgnoreDeprecatedWarning +} + +- (void)qmui_viewControllerDidInvokeViewWillAppear:(UIViewController *)viewController { + viewController.qmui_viewWillAppearNotifyDelegate = nil; + [self.delegator navigationController:self willShowViewController:self.viewControllerPopping animated:YES]; + self.viewControllerPopping = nil; + self.isViewControllerTransiting = NO; +} + +#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 childViewControllerForStatusBarWithCustomBlock:^BOOL(UIViewController *vc) { + return vc.qmui_prefersStatusBarHiddenBlock || [vc qmui_hasOverrideUIKitMethod:@selector(prefersStatusBarHidden)]; + }]; +} + +- (UIViewController *)childViewControllerForStatusBarStyle { + 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 - 屏幕旋转 + +- (BOOL)shouldAutorotate { + return [self.visibleViewController qmui_hasOverrideUIKitMethod:_cmd] ? [self.visibleViewController shouldAutorotate] : YES; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + // 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 + // https://github.com/Tencent/QMUI_iOS/issues/632 + UIViewController *visibleViewController = self.visibleViewController; + if (!visibleViewController || visibleViewController.isBeingDismissed || [visibleViewController isKindOfClass:UIAlertController.class]) { + visibleViewController = self.topViewController; + } + return [visibleViewController qmui_hasOverrideUIKitMethod:_cmd] ? [visibleViewController supportedInterfaceOrientations] : SupportedOrientationMask; +} + +#pragma mark - HomeIndicator + +- (UIViewController *)childViewControllerForHomeIndicatorAutoHidden { + return self.topViewController; +} + +@end + + +@implementation QMUINavigationController (UISubclassingHooks) + +- (void)willShowViewController:(UIViewController *)viewController animated:(BOOL)animated { + // 子类可以重写 +} + +- (void)didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { + // 子类可以重写 +} + +@end + +@implementation _QMUINavigationControllerDelegator + +#pragma mark - + +- (void)navigationController:(QMUINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated { + [navigationController willShowViewController:viewController animated:animated]; +} + +- (void)navigationController:(QMUINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { + navigationController.viewControllerPopping = nil; + [navigationController didShowViewController:viewController animated:animated]; +} + +@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 new file mode 100644 index 00000000..948b5210 --- /dev/null +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.h @@ -0,0 +1,37 @@ +/** + * 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. + */ + +// +// QMUITabBarViewController.h +// qmui +// +// Created by QMUI Team on 15/3/29. +// + +#import + +/** + * 建议作为项目里 tabBarController 的基类,内部处理了几件事情: + * 1. 配合配置表修改 tabBar 的样式。 + * 2. 管理界面支持显示的方向。 + * + * @warning 当你需要实现“tabBarController 首页那几个界面显示 tabBar,而 push 进去的所有子界面都隐藏 tabBar”的效果时,可将配置表里的 HidesBottomBarWhenPushedInitially 改为 YES,然后手动将 tabBarController 首页的那几个界面的 hidesBottomBarWhenPushed 属性改为 NO,即可实现。 + * + * 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 + * + */ +@interface QMUITabBarViewController : UITabBarController + +/** + * 初始化时调用的方法,会在 initWithNibName:bundle: 和 initWithCoder: 这两个指定的初始化方法中被调用,所以子类如果需要同时支持两个初始化方法,则建议把初始化时要做的事情放到这个方法里。否则仅需重写要支持的那个初始化方法即可。 + * Initialization method. Will be called in `initWithNibName:bundle:` and `initWithCoder:`. Implement this method to be called in both initializers. + */ +- (void)didInitialize NS_REQUIRES_SUPER; +@end diff --git a/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.m b/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.m new file mode 100644 index 00000000..25848e6c --- /dev/null +++ b/QMUI/QMUIKit/QMUIMainFrame/QMUITabBarViewController.m @@ -0,0 +1,93 @@ +/** + * 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. + */ + +// +// QMUITabBarViewController.m +// qmui +// +// Created by QMUI Team on 15/3/29. +// + +#import "QMUITabBarViewController.h" +#import "QMUICore.h" +#import "UIViewController+QMUI.h" + +@implementation QMUITabBarViewController + +- (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 { + // subclass hooking +} + +#pragma mark - StatusBar + +// 如果 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; +} + +// 只有 childViewController 没声明自己的状态栏样式时才会走到这里 +- (UIStatusBarStyle)preferredStatusBarStyle { + if (QMUICMIActivated) { + return DefaultStatusBarStyle; + } + return [super preferredStatusBarStyle]; +} + +#pragma mark - 屏幕旋转 + +- (BOOL)shouldAutorotate { + return self.presentedViewController ? [self.presentedViewController shouldAutorotate] : ([self.selectedViewController qmui_hasOverrideUIKitMethod:_cmd] ? [self.selectedViewController shouldAutorotate] : YES); +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + + // 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 + // 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 [visibleViewController qmui_hasOverrideUIKitMethod:_cmd] ? [visibleViewController supportedInterfaceOrientations] : SupportedOrientationMask; +} + +#pragma mark - HomeIndicator + +- (UIViewController *)childViewControllerForHomeIndicatorAutoHidden { + return self.selectedViewController; +} + +@end 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/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/Contents.json new file mode 100644 index 00000000..83b1bb8f --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_console_clear.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file 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/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/Contents.json new file mode 100644 index 00000000..d2770a5e --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_console_filter.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file 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/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter_selected.imageset/QMUI_console_filter_selected.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter_selected.imageset/QMUI_console_filter_selected.pdf new file mode 100644 index 00000000..7eeb9266 Binary files /dev/null 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/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/Contents.json b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/Contents.json new file mode 100644 index 00000000..8253ead1 --- /dev/null +++ b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "QMUI_hiddenAlbum.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/QMUI_hiddenAlbum.pdf b/QMUI/QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/QMUI_hiddenAlbum.pdf new file mode 100644 index 00000000..3cb89d58 Binary files /dev/null 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/UIComponents/AssetLibrary/QMUIAsset.h b/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAsset.h deleted file mode 100644 index ea41f953..00000000 --- a/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAsset.h +++ /dev/null @@ -1,156 +0,0 @@ -// -// QMUIAsset.h -// qmui -// -// Created by Kayo Lee on 15/6/30. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import - -/// Asset 的类型 -typedef NS_ENUM(NSUInteger, QMUIAssetType) { - QMUIAssetTypeUnknow, // 未知类型的 Asset - QMUIAssetTypeImage, // 图片类型的 Asset - QMUIAssetTypeVideo, // 视频类型的 Asset - QMUIAssetTypeAudio NS_ENUM_AVAILABLE_IOS(8_0), // 音频类型的 Asset,仅被 PhotoKit 支持,因此只适用于 iOS 8.0 - QMUIAssetTypeLivePhoto NS_ENUM_AVAILABLE_IOS(9_1) // Live Photo 类型的 Asset,仅被 PhotoKit 支持,因此只适用于 iOS 9.1 -}; - -/// 从 iCloud 请求 Asset 大图的状态 -typedef NS_ENUM(NSUInteger, QMUIAssetDownloadStatus) { - QMUIAssetDownloadStatusSucceed, // 下载成功或资源本来已经在本地 - QMUIAssetDownloadStatusDownloading, // 下载中 - QMUIAssetDownloadStatusCanceled, // 取消下载 - QMUIAssetDownloadStatusFailed, // 下载失败 -}; - - -@class ALAsset; -@class PHAsset; - -@interface QMUIAsset : NSObject - -@property(nonatomic, assign, readonly) QMUIAssetType assetType; - -- (instancetype)initWithPHAsset:(PHAsset *)phAsset; - -- (instancetype)initWithALAsset:(ALAsset *)alAsset; - -@property(nonatomic, assign, readonly) QMUIAssetDownloadStatus downloadStatus; // 从 iCloud 下载资源大图的状态 -@property(nonatomic, assign) double downloadProgress; // 从 iCloud 下载资源大图的进度 -@property(nonatomic, assign) NSInteger requestID; // 从 iCloud 请求获得资源的大图的请求 ID - -/// Asset 的原图(包含系统相册“编辑”功能处理后的效果) -- (UIImage *)originImage; - -/** - * Asset 的缩略图 - * - * @param size 指定返回的缩略图的大小,仅在 iOS 8.0 及以上的版本有效,其他版本则调用 ALAsset 的接口由系统返回一个合适当前平台的图片 - * - * @return Asset 的缩略图 - */ -- (UIImage *)thumbnailWithSize:(CGSize)size; - -/** - * Asset 的预览图 - * - * @warning 仿照 ALAssetsLibrary 的做法输出与当前设备屏幕大小相同尺寸的图片,如果图片原图小于当前设备屏幕的尺寸,则只输出原图大小的图片 - * @return Asset 的全屏图 - */ -- (UIImage *)previewImage; - -/** - * 异步请求 Asset 的原图,包含了系统照片“编辑”功能处理后的效果(剪裁,旋转和滤镜等),可能会有网络请求 - * - * @param completion 完成请求后调用的 block,参数中包含了请求的原图以及图片信息,在 iOS 8.0 或以上版本中, - * 这个 block 会被多次调用,其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直到获取到高清图。 - * @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。 - * - * @wraning iOS 8.0 以下中并没有异步请求预览图的接口,因此实际上为同步请求,这时 block 中的第二个参数(图片信息)返回的为 nil。 - * - * @return 返回请求图片的请求 id - */ -- (NSInteger)requestOriginImageWithCompletion:(void (^)(UIImage *result, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler; - -/** - * 异步请求 Asset 的缩略图,不会产生网络请求 - * - * @param size 指定返回的缩略图的大小,仅在 iOS 8.0 及以上的版本有效,其他版本则调用 ALAsset 的接口由系统返回一个合适当前平台的图片 - * @param completion 完成请求后调用的 block,参数中包含了请求的缩略图以及图片信息,在 iOS 8.0 或以上版本中,这个 block 会被多次调用, - * 其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直到获取到高清图,这时 block 中的第二个参数(图片信息)返回的为 nil。 - * - * @return 返回请求图片的请求 id - */ -- (NSInteger)requestThumbnailImageWithSize:(CGSize)size completion:(void (^)(UIImage *result, NSDictionary *info))completion; - -/** - * 异步请求 Asset 的预览图,可能会有网络请求 - * - * @param completion 完成请求后调用的 block,参数中包含了请求的预览图以及图片信息,在 iOS 8.0 或以上版本中, - * 这个 block 会被多次调用,其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直到获取到高清图。 - * @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。 - * - * @wraning iOS 8.0 以下中并没有异步请求预览图的接口,因此实际上为同步请求,这时 block 中的第二个参数(图片信息)返回的为 nil。 - * - * @return 返回请求图片的请求 id - */ -- (NSInteger)requestPreviewImageWithCompletion:(void (^)(UIImage *result, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler; - -/** - * 异步请求 Live Photo,可能会有网络请求 - * - * @param completion 完成请求后调用的 block,参数中包含了请求的 Live Photo 以及相关信息,若 assetType 不是 QMUIAssetTypeLivePhoto 则为 nil - * @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。 - * - * @wraning iOS 9.1 以下中并没有 Live Photo,因此无法获取有效结果。 - * - * @return 返回请求图片的请求 id - */ -- (NSInteger)requestLivePhotoWithCompletion:(void (^)(PHLivePhoto *livePhoto, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler NS_AVAILABLE_IOS(9_1); - -/** - * 异步请求 AVPlayerItem,可能会有网络请求 - * - * @param completion 完成请求后调用的 block,参数中包含了请求的 AVPlayerItem 以及相关信息,若 assetType 不是 QMUIAssetTypeVideo 则为 nil - * @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。 - * - * @wraning iOS 8.0 以下中并没有异步请求 AVPlayerItem 的接口,因此实际上为同步请求,这时 block 中的第二个参数(AVPlayerItem 相关信息)返回的为 nil。 - * - * @return 返回请求 AVPlayerItem 的请求 id - */ -- (NSInteger)requestPlayerItemWithCompletion:(void (^)(AVPlayerItem *playerItem, NSDictionary *info))completion withProgressHandler:(PHAssetVideoProgressHandler)phProgressHandler; - -/** - * 异步请求图片的 Data - * - * @param completion 完成请求后调用的 block,参数中包含了请求的图片 Data(若 assetType 不是 QMUIAssetTypeImage 或 QMUIAssetTypeLivePhoto 则为 nil),以及该图片是否为 GIF 的判断值 - * - * @wraning iOS 8.0 以下中并没有异步请求 Data 的接口,因此实际上为同步请求,这时 block 中的第二个参数(图片信息)返回的为 nil。 - */ -- (void)requestImageData:(void (^)(NSData *imageData, NSDictionary *info, BOOL isGif))completion; - -/** - * 获取图片的 UIImageOrientation 值,仅 assetType 为 QMUIAssetTypeImage 或 QMUIAssetTypeLivePhoto 时有效 - */ -- (UIImageOrientation)imageOrientation; - -/** - * Asset 的标识,每个 QMUIAsset 的标识值不相同,该标识值经过 md5 处理,避免了特殊字符 - * - * @return Asset 的标识字符串 - */ -- (NSString *)assetIdentity; - -/// 更新下载资源的结果 -- (void)updateDownloadStatusWithDownloadResult:(BOOL)succeed; - -/** - * 获取 Asset 的体积(数据大小) - */ -- (void)assetSize:(void (^)(long long size))completion; - -- (NSTimeInterval)duration; - -@end diff --git a/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAsset.m b/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAsset.m deleted file mode 100644 index fc06a892..00000000 --- a/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAsset.m +++ /dev/null @@ -1,451 +0,0 @@ -// -// QMUIAsset.m -// qmui -// -// Created by Kayo Lee on 15/6/30. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIAsset.h" -#import -#import -#import -#import "QMUICore.h" -#import "QMUIAssetsManager.h" -#import "NSString+QMUI.h" - -static NSString * const kAssetInfoImageData = @"imageData"; -static NSString * const kAssetInfoOriginInfo = @"originInfo"; -static NSString * const kAssetInfoDataUTI = @"dataUTI"; -static NSString * const kAssetInfoOrientation = @"orientation"; -static NSString * const kAssetInfoSize = @"size"; - -@interface QMUIAsset () - -@property (nonatomic, assign, readwrite) QMUIAssetType assetType; - -@end - - -@implementation QMUIAsset { - BOOL _usePhotoKit; - - PHAsset *_phAsset; - - ALAsset *_alAsset; - ALAssetRepresentation *_alAssetRepresentation; - NSDictionary *_phAssetInfo; - float imageSize; - - NSString *_assetIdentityHash; -} - -- (instancetype)initWithPHAsset:(PHAsset *)phAsset { - if (self = [super init]) { - _phAsset = phAsset; - _usePhotoKit = YES; - - switch (phAsset.mediaType) { - case PHAssetMediaTypeImage: - if (phAsset.mediaSubtypes & PHAssetMediaSubtypePhotoLive) { - self.assetType = QMUIAssetTypeLivePhoto; - } else { - self.assetType = QMUIAssetTypeImage; - } - break; - case PHAssetMediaTypeVideo: - self.assetType = QMUIAssetTypeVideo; - break; - case PHAssetMediaTypeAudio: - self.assetType = QMUIAssetTypeAudio; - break; - default: - self.assetType = QMUIAssetTypeUnknow; - break; - } - } - return self; -} - -- (instancetype)initWithALAsset:(ALAsset *)alAsset { - if (self = [super init]) { - _alAsset = alAsset; - _alAssetRepresentation = [alAsset defaultRepresentation]; - _usePhotoKit = NO; - - NSString *propertyType = [alAsset valueForProperty:ALAssetPropertyType]; - if ([propertyType isEqualToString:ALAssetTypePhoto]) { - self.assetType = QMUIAssetTypeImage; - } else if ([propertyType isEqualToString:ALAssetTypeVideo]) { - self.assetType = QMUIAssetTypeVideo; - } else { - self.assetType = QMUIAssetTypeUnknow; - } - } - return self; -} - -- (UIImage *)originImage { - __block UIImage *resultImage = nil; - if (_usePhotoKit) { - PHImageRequestOptions *phImageRequestOptions = [[PHImageRequestOptions alloc] init]; - phImageRequestOptions.synchronous = YES; - [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset - targetSize:PHImageManagerMaximumSize - contentMode:PHImageContentModeDefault - options:phImageRequestOptions - resultHandler:^(UIImage *result, NSDictionary *info) { - resultImage = result; - }]; - } else { - CGImageRef fullResolutionImageRef = [_alAssetRepresentation fullResolutionImage]; - // 通过 fullResolutionImage 获取到的的高清图实际上并不带上在照片应用中使用“编辑”处理的效果,需要额外在 AlAssetRepresentation 中获取这些信息 - NSString *adjustment = [[_alAssetRepresentation metadata] objectForKey:@"AdjustmentXMP"]; - if (adjustment) { - // 如果有在照片应用中使用“编辑”效果,则需要获取这些编辑后的滤镜,手工叠加到原图中 - NSData *xmpData = [adjustment dataUsingEncoding:NSUTF8StringEncoding]; - CIImage *tempImage = [CIImage imageWithCGImage:fullResolutionImageRef]; - - NSError *error; - NSArray *filterArray = [CIFilter filterArrayFromSerializedXMP:xmpData - inputImageExtent:tempImage.extent - error:&error]; - CIContext *context = [CIContext contextWithOptions:nil]; - if (filterArray && !error) { - for (CIFilter *filter in filterArray) { - [filter setValue:tempImage forKey:kCIInputImageKey]; - tempImage = [filter outputImage]; - } - fullResolutionImageRef = [context createCGImage:tempImage fromRect:[tempImage extent]]; - } - } - // 生成最终返回的 UIImage,同时把图片的 orientation 也补充上去 - resultImage = [UIImage imageWithCGImage:fullResolutionImageRef scale:[_alAssetRepresentation scale] orientation:(UIImageOrientation)[_alAssetRepresentation orientation]]; - } - return resultImage; -} - -- (UIImage *)thumbnailWithSize:(CGSize)size { - __block UIImage *resultImage; - if (_usePhotoKit) { - PHImageRequestOptions *phImageRequestOptions = [[PHImageRequestOptions alloc] init]; - phImageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact; - // 在 PHImageManager 中,targetSize 等 size 都是使用 px 作为单位,因此需要对targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片 - [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset - targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale) - contentMode:PHImageContentModeAspectFill options:phImageRequestOptions - resultHandler:^(UIImage *result, NSDictionary *info) { - resultImage = result; - }]; - } else { - CGImageRef thumbnailImageRef = [_alAsset thumbnail]; - if (thumbnailImageRef) { - resultImage = [UIImage imageWithCGImage:thumbnailImageRef]; - } - } - return resultImage; -} - -- (UIImage *)previewImage { - __block UIImage *resultImage = nil; - if (_usePhotoKit) { - PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init]; - imageRequestOptions.synchronous = YES; - [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset - targetSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT) - contentMode:PHImageContentModeAspectFill - options:imageRequestOptions - resultHandler:^(UIImage *result, NSDictionary *info) { - resultImage = result; - }]; - } else { - CGImageRef fullScreenImageRef = [_alAssetRepresentation fullScreenImage]; - resultImage = [UIImage imageWithCGImage:fullScreenImageRef]; - } - return resultImage; -} - -- (NSInteger)requestOriginImageWithCompletion:(void (^)(UIImage *result, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler { - if (_usePhotoKit) { - PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init]; - imageRequestOptions.networkAccessAllowed = YES; // 允许访问网络 - imageRequestOptions.progressHandler = phProgressHandler; - return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) { - if (completion) { - completion(result, info); - } - }]; - } else { - if (completion) { - completion([self originImage], nil); - } - return 0; - } -} - -- (NSInteger)requestThumbnailImageWithSize:(CGSize)size completion:(void (^)(UIImage *result, NSDictionary *info))completion { - if (_usePhotoKit) { - PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init]; - imageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeFast; - // 在 PHImageManager 中,targetSize 等 size 都是使用 px 作为单位,因此需要对targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片 - return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale) contentMode:PHImageContentModeAspectFill options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) { - if (completion) { - completion(result, info); - } - }]; - - } else { - if (completion) { - completion([self thumbnailWithSize:size], nil); - } - return 0; - } -} - -- (NSInteger)requestPreviewImageWithCompletion:(void (^)(UIImage *result, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler { - if (_usePhotoKit) { - PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init]; - imageRequestOptions.networkAccessAllowed = YES; // 允许访问网络 - imageRequestOptions.progressHandler = phProgressHandler; - return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT) contentMode:PHImageContentModeAspectFill options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) { - if (completion) { - completion(result, info); - } - }]; - } else { - if (completion) { - completion([self previewImage], nil); - } - return 0; - } -} - -- (NSInteger)requestLivePhotoWithCompletion:(void (^)(PHLivePhoto *livePhoto, NSDictionary *info))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler { - if (_usePhotoKit && [[PHCachingImageManager class] instancesRespondToSelector:@selector(requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler:)]) { - PHLivePhotoRequestOptions *livePhotoRequestOptions = [[PHLivePhotoRequestOptions alloc] init]; - livePhotoRequestOptions.networkAccessAllowed = YES; // 允许访问网络 - livePhotoRequestOptions.progressHandler = phProgressHandler; - return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestLivePhotoForAsset:_phAsset targetSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT) contentMode:PHImageContentModeDefault options:livePhotoRequestOptions resultHandler:^(PHLivePhoto * _Nullable livePhoto, NSDictionary * _Nullable info) { - if (completion) { - completion(livePhoto, info); - } - }]; - } else { - return 0; - } -} - -- (NSInteger)requestPlayerItemWithCompletion:(void (^)(AVPlayerItem *playerItem, NSDictionary *info))completion withProgressHandler:(PHAssetVideoProgressHandler)phProgressHandler { - if (_usePhotoKit && [[PHCachingImageManager class] instancesRespondToSelector:@selector(requestPlayerItemForVideo:options:resultHandler:)]) { - PHVideoRequestOptions *videoRequestOptions = [[PHVideoRequestOptions alloc] init]; - videoRequestOptions.networkAccessAllowed = YES; // 允许访问网络 - videoRequestOptions.progressHandler = phProgressHandler; - return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestPlayerItemForVideo:_phAsset options:videoRequestOptions resultHandler:^(AVPlayerItem * _Nullable playerItem, NSDictionary * _Nullable info) { - if (completion) { - completion(playerItem, info); - } - }]; - } else { - NSURL *url = [_alAssetRepresentation url]; - AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:url]; - if (completion) { - completion(playerItem, nil); - } - return 0; - } -} - -- (void)requestImageData:(void (^)(NSData *imageData, NSDictionary *info, BOOL isGif))completion { - if (self.assetType != QMUIAssetTypeImage && self.assetType != QMUIAssetTypeLivePhoto) { - if (completion) { - completion(nil, nil, NO); - } - return; - } - if (_usePhotoKit) { - if (!_phAssetInfo) { - // PHAsset 的 UIImageOrientation 需要调用过 requestImageDataForAsset 才能获取 - [self requestPhAssetInfo:^(NSDictionary *phAssetInfo) { - _phAssetInfo = phAssetInfo; - if (completion) { - NSString *dataUTI = phAssetInfo[kAssetInfoDataUTI]; - BOOL isGif = [dataUTI isEqualToString:(__bridge NSString *)kUTTypeGIF]; - NSDictionary *originInfo = phAssetInfo[kAssetInfoOriginInfo]; - /** - * 这里不在主线程执行,若用户在该 block 中操作 UI 时会产生一些问题, - * 为了避免这种情况,这里该 block 主动放到主线程执行。 - */ - dispatch_async(dispatch_get_main_queue(), ^{ - completion(phAssetInfo[kAssetInfoImageData], originInfo, isGif); - }); - } - }]; - } else { - if (completion) { - NSString *dataUTI = _phAssetInfo[kAssetInfoDataUTI]; - BOOL isGif = [dataUTI isEqualToString:(__bridge NSString *)kUTTypeGIF]; - NSDictionary *originInfo = _phAssetInfo[kAssetInfoOriginInfo]; - completion(_phAssetInfo[kAssetInfoImageData], originInfo, isGif); - } - } - } else { - if (completion) { - [self assetSize:^(long long size) { - // 获取 NSData 数据 - uint8_t *buffer = malloc((size_t)size); - NSError *error; - NSUInteger bytes = [_alAssetRepresentation getBytes:buffer fromOffset:0 length:(NSUInteger)size error:&error]; - NSData *imageData = [NSData dataWithBytes:buffer length:bytes]; - free(buffer); - // 判断是否为 GIF 图 - ALAssetRepresentation *gifRepresentation = [_alAsset representationForUTI: (__bridge NSString *)kUTTypeGIF]; - if (gifRepresentation) { - completion(imageData, nil, YES); - } else { - completion(imageData, nil, NO); - } - }]; - } - } -} - -- (UIImageOrientation)imageOrientation { - UIImageOrientation orientation; - if (self.assetType == QMUIAssetTypeImage || self.assetType == QMUIAssetTypeLivePhoto) { - if (_usePhotoKit) { - if (!_phAssetInfo) { - // PHAsset 的 UIImageOrientation 需要调用过 requestImageDataForAsset 才能获取 - [self requestImagePhAssetInfo:^(NSDictionary *phAssetInfo) { - _phAssetInfo = phAssetInfo; - } synchronous:YES]; - } - // 从 PhAssetInfo 中获取 UIImageOrientation 对应的字段 - orientation = (UIImageOrientation)[_phAssetInfo[kAssetInfoOrientation] integerValue]; - } else { - orientation = (UIImageOrientation)[[_alAsset valueForProperty:@"ALAssetPropertyOrientation"] integerValue]; - } - } else { - orientation = UIImageOrientationUp; - } - return orientation; -} - -- (NSString *)assetIdentity { - if (_assetIdentityHash) { - return _assetIdentityHash; - } - NSString *identity; - if (_usePhotoKit) { - identity = _phAsset.localIdentifier; - } else { - identity = [[_alAssetRepresentation url] absoluteString]; - } - // 系统输出的 identity 可能包含特殊字符,为了避免引起问题,统一使用 md5 转换 - _assetIdentityHash = [identity qmui_md5]; - return _assetIdentityHash; -} - -- (void)requestPhAssetInfo:(void (^)(NSDictionary *))completion { - if (!_phAsset && completion) { - completion(nil); - } - - if (self.assetType == QMUIAssetTypeVideo) { - [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestAVAssetForVideo:_phAsset options:NULL resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) { - if ([asset isKindOfClass:[AVURLAsset class]]) { - NSMutableDictionary *tempInfo = [[NSMutableDictionary alloc] init]; - if (info) { - [tempInfo setObject:info forKey:kAssetInfoOriginInfo]; - } - - AVURLAsset *urlAsset = (AVURLAsset*)asset; - NSNumber *size; - [urlAsset.URL getResourceValue:&size forKey:NSURLFileSizeKey error:nil]; - [tempInfo setObject:size forKey:kAssetInfoSize]; - if (completion) { - completion(tempInfo); - } - } - }]; - } else { - [self requestImagePhAssetInfo:^(NSDictionary *phAssetInfo) { - if (completion) { - completion(phAssetInfo); - } - } synchronous:NO]; - } -} - -- (void)requestImagePhAssetInfo:(void (^)(NSDictionary *))completion synchronous:(BOOL)synchronous { - PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init]; - imageRequestOptions.synchronous = synchronous; - imageRequestOptions.networkAccessAllowed = YES; - [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageDataForAsset:_phAsset options:imageRequestOptions resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) { - if (info) { - NSMutableDictionary *tempInfo = [[NSMutableDictionary alloc] init]; - if (imageData) { - [tempInfo setObject:imageData forKey:kAssetInfoImageData]; - [tempInfo setObject:@(imageData.length) forKey:kAssetInfoSize]; - } - - [tempInfo setObject:info forKey:kAssetInfoOriginInfo]; - if (dataUTI) { - [tempInfo setObject:dataUTI forKey:kAssetInfoDataUTI]; - } - [tempInfo setObject:@(orientation) forKey:kAssetInfoOrientation]; - if (completion) { - completion(tempInfo); - } - } - }]; -} - -- (void)setDownloadProgress:(double)downloadProgress { - _downloadProgress = downloadProgress; - _downloadStatus = QMUIAssetDownloadStatusDownloading; -} - -- (void)updateDownloadStatusWithDownloadResult:(BOOL)succeed { - _downloadStatus = succeed ? QMUIAssetDownloadStatusSucceed : QMUIAssetDownloadStatusFailed; -} - -- (void)assetSize:(void (^)(long long size))completion { - if (_usePhotoKit) { - if (!_phAssetInfo) { - // PHAsset 的 UIImageOrientation 需要调用过 requestImageDataForAsset 才能获取 - [self requestPhAssetInfo:^(NSDictionary *phAssetInfo) { - _phAssetInfo = phAssetInfo; - if (completion) { - /** - * 这里不在主线程执行,若用户在该 block 中操作 UI 时会产生一些问题, - * 为了避免这种情况,这里该 block 主动放到主线程执行。 - */ - dispatch_async(dispatch_get_main_queue(), ^{ - completion([phAssetInfo[kAssetInfoSize] longLongValue]); - }); - } - }]; - } else { - if (completion) { - completion([_phAssetInfo[kAssetInfoSize] longLongValue]); - } - } - } else { - if (completion) { - completion(_alAssetRepresentation.size); - } - } -} - -- (NSTimeInterval)duration { - if (self.assetType != QMUIAssetTypeVideo) { - return 0; - } - if (_usePhotoKit) { - return _phAsset.duration; - } else { - return [[_alAsset valueForProperty:ALAssetPropertyDuration] doubleValue]; - } -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsGroup.m b/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsGroup.m deleted file mode 100644 index f989f89b..00000000 --- a/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsGroup.m +++ /dev/null @@ -1,150 +0,0 @@ -// -// QMUIAssetsGroup.m -// qmui -// -// Created by Kayo Lee on 15/6/30. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIAssetsGroup.h" -#import "QMUICore.h" -#import "QMUIAsset.h" -#import "QMUIAssetsManager.h" - -@interface QMUIAssetsGroup() - -@property(nonatomic, strong, readwrite) ALAssetsGroup *alAssetsGroup; -@property(nonatomic, strong, readwrite) PHAssetCollection *phAssetCollection; -@property(nonatomic, strong, readwrite) PHFetchResult *phFetchResult; - -@end - -@implementation QMUIAssetsGroup { - BOOL _usePhotoKit; -} - -- (instancetype)initWithALAssetsGroup:(ALAssetsGroup *)alAssetsGroup { - self = [super init]; - if (self) { - self.alAssetsGroup = alAssetsGroup; - _usePhotoKit = NO; - } - return self; -} - -- (instancetype)initWithPHCollection:(PHAssetCollection *)phAssetCollection fetchAssetsOptions:(PHFetchOptions *)pHFetchOptions { - self = [super init]; - if (self) { - PHFetchResult *phFetchResult = [PHAsset fetchAssetsInAssetCollection:phAssetCollection options:pHFetchOptions]; - self.phFetchResult = phFetchResult; - self.phAssetCollection = phAssetCollection; - _usePhotoKit = YES; - } - return self; -} - -- (instancetype)initWithPHCollection:(PHAssetCollection *)phAssetCollection { - return [self initWithPHCollection:phAssetCollection fetchAssetsOptions:nil]; -} - -- (NSInteger)numberOfAssets { - if (_usePhotoKit) { - return self.phFetchResult.count; - } else { - return [self.alAssetsGroup numberOfAssets]; - } -} - -- (NSString *)name { - NSString *resultName = nil; - if (_usePhotoKit) { - resultName = self.phAssetCollection.localizedTitle; - } else { - resultName = [self.alAssetsGroup valueForProperty:ALAssetsGroupPropertyName]; - } - return NSLocalizedString(resultName, resultName); -} - -- (UIImage *)posterImageWithSize:(CGSize)size { - __block UIImage *resultImage; - if (_usePhotoKit) { - NSInteger count = self.phFetchResult.count; - if (count > 0) { - PHAsset *asset = self.phFetchResult[count - 1]; - PHImageRequestOptions *pHImageRequestOptions = [[PHImageRequestOptions alloc] init]; - pHImageRequestOptions.synchronous = YES; // 同步请求 - pHImageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact; - // targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片 - [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:asset targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale) contentMode:PHImageContentModeAspectFill options:pHImageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) { - resultImage = result; - }]; - } - } else { - CGImageRef posterImageRef = [self.alAssetsGroup posterImage]; - if (posterImageRef) { - resultImage = [UIImage imageWithCGImage:posterImageRef]; - } - } - return resultImage; -} - -- (void)enumerateAssetsWithOptions:(QMUIAlbumSortType)albumSortType usingBlock:(void (^)(QMUIAsset *resultAsset))enumerationBlock { - if (_usePhotoKit) { - NSInteger resultCount = self.phFetchResult.count; - if (albumSortType == QMUIAlbumSortTypeReverse) { - for (NSInteger i = resultCount - 1; i >= 0; i--) { - PHAsset *pHAsset = self.phFetchResult[i]; - QMUIAsset *asset = [[QMUIAsset alloc] initWithPHAsset:pHAsset]; - if (enumerationBlock) { - enumerationBlock(asset); - } - } - } else { - for (NSInteger i = 0; i < resultCount; i++) { - PHAsset *pHAsset = self.phFetchResult[i]; - QMUIAsset *asset = [[QMUIAsset alloc] initWithPHAsset:pHAsset]; - if (enumerationBlock) { - enumerationBlock(asset); - } - } - } - /** - * For 循环遍历完毕,这时再调用一次 enumerationBlock,并传递 nil 作为实参,作为枚举资源结束的标记。 - * 该处理方式也是参照系统 ALAssetGroup 枚举结束的处理。 - */ - if (enumerationBlock) { - enumerationBlock(nil); - } - } else { - NSEnumerationOptions enumerationOptions; - if (albumSortType == QMUIAlbumSortTypeReverse) { - enumerationOptions = NSEnumerationReverse; - } else { - enumerationOptions = NSEnumerationConcurrent; - } - [self.alAssetsGroup enumerateAssetsWithOptions:enumerationOptions usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) { - if (result) { - QMUIAsset *asset = [[QMUIAsset alloc] initWithALAsset:result]; - if (enumerationBlock) { - enumerationBlock(asset); - } - } else { - /** - * ALAssetGroup 枚举结束。 - * 与上面 PHAssetsFetchResults 相似,再调用一次 enumerationBlock,并传递 nil 作为实参,作为枚举资源结束的标记。 - * 与 ALAssetGroup 本身处理枚举结束的方式保持一致。 - */ - if (enumerationBlock) { - enumerationBlock(nil); - } - } - }]; - - } -} - -- (void)enumerateAssetsUsingBlock:(void (^)(QMUIAsset *resultAsset))enumerationBlock { - [self enumerateAssetsWithOptions:QMUIAlbumSortTypePositive usingBlock:enumerationBlock]; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsManager.h b/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsManager.h deleted file mode 100644 index 86f4c0ec..00000000 --- a/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsManager.h +++ /dev/null @@ -1,169 +0,0 @@ -// -// QMUIAssetsManager.h -// qmui -// -// Created by Kayo Lee on 15/6/9. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import "QMUIAssetsGroup.h" - -#define EnforceUseAssetsLibraryForTest NO // 强制在 iOS 8.0 下也使用 ALAssetLibrary,用于调试 - -@class PHCachingImageManager; -@class QMUIAsset; - -/// Asset授权的状态 -typedef NS_ENUM(NSUInteger, QMUIAssetAuthorizationStatus) { - QMUIAssetAuthorizationStatusNotUsingPhotoKit, // 对于iOS7及以下不支持PhotoKit的系统,没有所谓的“授权状态”,所以定义一个特定的status用于表示这种情况 - QMUIAssetAuthorizationStatusNotDetermined, // 还不确定有没有授权 - QMUIAssetAuthorizationStatusAuthorized, // 已经授权 - QMUIAssetAuthorizationStatusNotAuthorized // 手动禁止了授权 -}; - -typedef void (^QMUIWriteAssetCompletionBlock)(QMUIAsset *asset, NSError *error); - - -/// 保存图片到指定相册,该方法是一个 C 方法,与系统 ALAssetLibrary 保存图片的 C 方法 UIImageWriteToSavedPhotosAlbum 对应,方便调用 -extern void QMUIImageWriteToSavedPhotosAlbumWithAlbumAssetsGroup(UIImage *image, QMUIAssetsGroup *albumAssetsGroup, QMUIWriteAssetCompletionBlock completionBlock); - -/// 保存视频到指定相册,该方法是一个 C 方法,与系统 ALAssetLibrary 保存图片的 C 方法 UISaveVideoAtPathToSavedPhotosAlbum 对应,方便调用 -extern void QMUISaveVideoAtPathToSavedPhotosAlbumWithAlbumAssetsGroup(NSString *videoPath, QMUIAssetsGroup *albumAssetsGroup, QMUIWriteAssetCompletionBlock completionBlock); - -/** - * 构建 QMUIAssetsManager 这个对象并提供单例的调用方式主要出于下面几点考虑: - * 1. 由于需要有同时兼顾 ALAssetsLibrary 和 PhotoKit 的保存图片方法,因此保存图片的方法变得比较复杂。 - * 这时有一个不同于 ALAssetsLibrary 和 PhotoKit 的对象去定义这些保存图片的方法会更便于管理这些方法。 - * 2. 如果使用 ALAssetsLibrary 保存图片,那么最终都会调用 ALAssetsLibrary 的一个实例方法, - * 而 init ALAssetsLibrary 消耗比较大,因此构建一个单例对象,在对象内部 init 一个 ALAssetsLibrary, - * 需要保存图片到指定相册时建议统一调用这个单例的方法,减少重复消耗。 - * 3. 与上面相似,使用 PhotoKit 获取图片,基本都需要一个 PHCachingImageManager 的实例,为了减少消耗, - * 所以 QMUIAssetsManager 单例内部也构建了一个 PHCachingImageManager,并且暴露给外面,方便获取 - * PHCachingImageManager 的实例。 - */ -@interface QMUIAssetsManager : NSObject - -/// 获取 QMUIAssetsManager 的单例 -+ (instancetype)sharedInstance; - -/// 获取当前应用的“照片”访问授权状态 -+ (QMUIAssetAuthorizationStatus)authorizationStatus; - -/** - * 调起系统询问是否授权访问“照片”的 UIAlertView - * @param handler 授权结束后调用的 block,默认不在主线程上执行,如果需要在 block 中修改 UI,记得dispatch到mainqueue - */ -+ (void)requestAuthorization:(void(^)(QMUIAssetAuthorizationStatus status))handler; - -/** - * 获取所有的相册,在 iOS 8.0 及以上版本中,同时在该方法内部调用了 PhotoKit,可以获取如个人收藏,最近添加,自拍这类“智能相册” - * - * @param contentType 相册的内容类型,设定了内容类型后,所获取的相册中只包含对应类型的资源 - * @param showEmptyAlbum 是否显示空相册(经过 contentType 过滤后仍为空的相册) - * @param showSmartAlbumIfSupported 是否显示"智能相册"(仅 iOS 8.0 及以上版本可以显示“智能相册”) - * @param enumerationBlock 参数 resultAssetsGroup 表示每次枚举时对应的相册。枚举所有相册结束后,enumerationBlock 会被再调用一次, - * 这时 resultAssetsGroup 的值为 nil。可以以此作为判断枚举结束的标记。 - */ -- (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbumIfSupported:(BOOL)showSmartAlbumIfSupported usingBlock:(void (^)(QMUIAssetsGroup *resultAssetsGroup))enumerationBlock; - -/// 获取所有相册,默认在 iOS 8.0 及以上系统中显示系统的“智能相册”,不显示空相册(经过 contentType 过滤后为空的相册) -- (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType usingBlock:(void (^)(QMUIAssetsGroup *resultAssetsGroup))enumerationBlock; - -/** - * 保存图片或视频到指定的相册 - * - * @warning 无论用户保存到哪个自行创建的相册,系统都会在“相机胶卷”相册中同时保存这个图片。 - * 因为系统没有把图片和视频直接保存到指定相册的接口,都只能先保存到“相机胶卷”,从而生成了 Asset 对象, - * 再把 Asset 对象添加到指定相册中,从而达到保存资源到指定相册的效果。 - * 即使调用 PhotoKit 保存图片或视频到指定相册的新接口也是如此,并且官方 PhotoKit SampleCode 中例子也是表现如此, - * 因此这应该是一个合符官方预期的表现。 - * @warning 在支持“智能相册”的系统版本(iOS 8.0 及以上版本)也中无法通过该方法把图片保存到“智能相册”, - * “智能相册”只能由系统控制资源的增删。 - */ -- (void)saveImageWithImageRef:(CGImageRef)imageRef albumAssetsGroup:(QMUIAssetsGroup *)albumAssetsGroup orientation:(UIImageOrientation)orientation completionBlock:(QMUIWriteAssetCompletionBlock)completionBlock; - -- (void)saveVideoWithVideoPathURL:(NSURL *)videoPathURL albumAssetsGroup:(QMUIAssetsGroup *)albumAssetsGroup completionBlock:(QMUIWriteAssetCompletionBlock)completionBlock; - -/// 强制刷新单例中的 ALAssetLibrary,但你的相册资源发生改变时(创建或删除相册)可以手工调用该方法及时更新 -- (void)refreshAssetsLibrary; - -/// 获取一个 ALAssetsLibrary 的实例 -- (ALAssetsLibrary *)alAssetsLibrary; - -/// 获取一个 PHCachingImageManager 的实例 -- (PHCachingImageManager *)phCachingImageManager; - -@end - - -@interface PHPhotoLibrary (QMUI) - -/** - * 根据 contentType 的值产生一个合适的 PHFetchOptions,并把内容以资源创建日期排序,创建日期较新的资源排在前面 - * - * @param contentType 相册的内容类型 - * - * @return 返回一个合适的 PHFetchOptions - */ -+ (PHFetchOptions *)createFetchOptionsWithAlbumContentType:(QMUIAlbumContentType)contentType; - -/** - * 获取所有相册 - * - * @param contentType 相册的内容类型,设定了内容类型后,所获取的相册中只包含对应类型的资源 - * @param showEmptyAlbum 是否显示空相册(经过 contentType 过滤后仍为空的相册) - * @param showSmartAlbum 是否显示“智能相册” - * - * @return 返回包含所有合适相册的数组 - */ -+ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum; - -/// 获取一个 PHAssetCollection 中创建日期最新的资源 -+ (PHAsset *)fetchLatestAssetWithAssetCollection:(PHAssetCollection *)assetCollection; - -/** - * 保存图片或视频到指定的相册 - * - * @warning 无论用户保存到哪个自行创建的相册,系统都会在“相机胶卷”相册中同时保存这个图片。 - * 原因请参考 QMUIAssetsManager 对象的保存图片和视频方法的注释。 - * @warning 无法通过该方法把图片保存到“智能相册”,“智能相册”只能由系统控制资源的增删。 - */ -- (void)addImageToAlbum:(CGImageRef)imageRef albumAssetCollection:(PHAssetCollection *)albumAssetCollection orientation:(UIImageOrientation)orientation completionHandler:(void(^)(BOOL success, NSDate *creationDate, NSError *error))completionHandler; - -- (void)addVideoToAlbum:(NSURL *)videoPathURL albumAssetCollection:(PHAssetCollection *)albumAssetCollection completionHandler:(void(^)(BOOL success, NSDate *creationDate, NSError *error))completionHandler; - -@end - - -@interface ALAssetsLibrary (QMUI) - -/** - * 获取所有相册 - * - * @param contentType 相册的内容类型,设定了内容类型后,所获取的相册中只包含对应类型的资源 - * @param enumerationBlock 参数 group 表示每次枚举时对应的相册。枚举所有相册结束后,enumerationBlock 会被再调用一次, - * 这时 group 的值为 nil。可以以此作为判断枚举结束的标记。 - */ -- (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType usingBlock:(ALAssetsLibraryGroupsEnumerationResultsBlock)enumerationBlock; - -/** - * 保存图片或视频到指定的相册 - * - * @warning 无论用户保存到哪个自行创建的相册,系统都会在“相机胶卷”相册中同时保存这个图片。 - * 原因请参考 QMUIAssetsManager 对象的保存图片和视频方法的注释。 - * 如果直接调用该接口保存图片或视频到“相机胶卷”中,并不会产生重复保存。 - */ -- (void)writeImageToSavedPhotosAlbum:(CGImageRef)imageRef albumAssetsGroup:(ALAssetsGroup *)albumAssetsGroup orientation:(UIImageOrientation)orientation completionBlock:(ALAssetsLibraryWriteImageCompletionBlock)completionBlock; - -- (void)writeVideoAtPathToSavedPhotosAlbum:(NSURL *)videoPathURL albumAssetsGroup:(ALAssetsGroup *)albumAssetsGroup completionBlock:(ALAssetsLibraryWriteImageCompletionBlock)completionBlock; - -@end diff --git a/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsManager.m b/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsManager.m deleted file mode 100644 index e5701905..00000000 --- a/QMUI/QMUIKit/UIComponents/AssetLibrary/QMUIAssetsManager.m +++ /dev/null @@ -1,519 +0,0 @@ -// -// QMUIAssetsManager.m -// qmui -// -// Created by Kayo Lee on 15/6/9. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIAssetsManager.h" -#import "QMUICore.h" -#import "QMUIAsset.h" - -void QMUIImageWriteToSavedPhotosAlbumWithAlbumAssetsGroup(UIImage *image, QMUIAssetsGroup *albumAssetsGroup, QMUIWriteAssetCompletionBlock completionBlock) { - [[QMUIAssetsManager sharedInstance] saveImageWithImageRef:image.CGImage albumAssetsGroup:albumAssetsGroup orientation:image.imageOrientation completionBlock:completionBlock]; -} - -void QMUISaveVideoAtPathToSavedPhotosAlbumWithAlbumAssetsGroup(NSString *videoPath, QMUIAssetsGroup *albumAssetsGroup, QMUIWriteAssetCompletionBlock completionBlock) { - [[QMUIAssetsManager sharedInstance] saveVideoWithVideoPathURL:[NSURL fileURLWithPath:videoPath] albumAssetsGroup:albumAssetsGroup completionBlock:completionBlock]; -} - - - -@implementation QMUIAssetsManager { - ALAssetsLibrary *_alAssetsLibrary; - PHCachingImageManager *_phCachingImageManager; - BOOL _usePhotoKit; -} - -+ (QMUIAssetsManager *)sharedInstance { - static dispatch_once_t onceToken; - static QMUIAssetsManager *instance = nil; - dispatch_once(&onceToken,^{ - instance = [[super allocWithZone:NULL] init]; - }); - return instance; -} - -/** - * 重写 +allocWithZone 方法,使得在给对象分配内存空间的时候,就指向同一份数据 - */ - -+ (id)allocWithZone:(struct _NSZone *)zone { - return [self sharedInstance]; -} - -- (instancetype)init { - if (self = [super init]) { - _usePhotoKit = EnforceUseAssetsLibraryForTest ? NO : ((IOS_VERSION >= 8.0) ? YES : NO); - if (!_usePhotoKit) { - _alAssetsLibrary = [[ALAssetsLibrary alloc] init]; - } - } - return self; -} - -- (BOOL)usePhotoKit { - return _usePhotoKit; -} - -+ (QMUIAssetAuthorizationStatus)authorizationStatus { - __block QMUIAssetAuthorizationStatus status; - if ([[QMUIAssetsManager sharedInstance] usePhotoKit]) { - // 获取当前应用对照片的访问授权状态 - PHAuthorizationStatus authorizationStatus = [PHPhotoLibrary authorizationStatus]; - if (authorizationStatus == PHAuthorizationStatusRestricted || authorizationStatus == PHAuthorizationStatusDenied) { - status = QMUIAssetAuthorizationStatusNotAuthorized; - } else if (authorizationStatus == PHAuthorizationStatusNotDetermined) { - status = QMUIAssetAuthorizationStatusNotDetermined; - } else { - status = QMUIAssetAuthorizationStatusAuthorized; - } - } else { - // 获取当前应用对照片的访问授权状态 - ALAuthorizationStatus authorizationStatus = [ALAssetsLibrary authorizationStatus]; - if (authorizationStatus == ALAuthorizationStatusRestricted || authorizationStatus == ALAuthorizationStatusDenied) { - status = QMUIAssetAuthorizationStatusNotAuthorized; - } else if (authorizationStatus == ALAuthorizationStatusNotDetermined) { - status = QMUIAssetAuthorizationStatusNotDetermined; - } else { - status = QMUIAssetAuthorizationStatusAuthorized; - } - } - return status; -} - -+ (void)requestAuthorization:(void(^)(QMUIAssetAuthorizationStatus status))handler { - if ([[QMUIAssetsManager sharedInstance] usePhotoKit]) { - [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus phStatus) { - QMUIAssetAuthorizationStatus status; - if (phStatus == PHAuthorizationStatusRestricted || phStatus == PHAuthorizationStatusDenied) { - status = QMUIAssetAuthorizationStatusNotAuthorized; - } else if (phStatus == PHAuthorizationStatusNotDetermined) { - status = QMUIAssetAuthorizationStatusNotDetermined; - } else { - status = QMUIAssetAuthorizationStatusAuthorized; - } - if (handler) { - handler(status); - } - }]; - } else { - [[QMUIAssetsManager sharedInstance].alAssetsLibrary enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:nil failureBlock:nil]; - if (handler) { - handler(QMUIAssetAuthorizationStatusNotUsingPhotoKit); - } - } -} - -- (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbumIfSupported:(BOOL)showSmartAlbumIfSupported usingBlock:(void (^)(QMUIAssetsGroup *resultAssetsGroup))enumerationBlock { - if (_usePhotoKit) { - // 根据条件获取所有合适的相册,并保存到临时数组中 - 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]; - QMUIAssetsGroup *assetsGroup = [[QMUIAssetsGroup alloc] initWithPHCollection:phAssetCollection fetchAssetsOptions:phFetchOptions]; - if (enumerationBlock) { - enumerationBlock(assetsGroup); - } - } - - /** - * 所有结果遍历完毕,这时再调用一次 enumerationBlock,并传递 nil 作为实参,作为枚举相册结束的标记。 - * 该处理方式也是参照系统 ALAssetsLibrary enumerateGroupsWithTypes 枚举结束的处理。 - */ - if (enumerationBlock) { - enumerationBlock(nil); - } - - } else { - [_alAssetsLibrary enumerateAllAlbumsWithAlbumContentType:contentType usingBlock:^(ALAssetsGroup *group, BOOL *stop) { - if (group) { - QMUIAssetsGroup *assetsGroup = [[QMUIAssetsGroup alloc] initWithALAssetsGroup:group]; - if (enumerationBlock) { - enumerationBlock(assetsGroup); - } - } else { - /** - * 枚举结束,与上面 PhotoKit 中的处理相似,再调用一次 enumerationBlock,并传递 nil 作为实参,作为枚举相册结束的标记。 - * 与系统 ALAssetsLibrary enumerateGroupsWithTypes 本身处理枚举结束的方式保持一致。 - */ - if (enumerationBlock) { - enumerationBlock(nil); - } - } - }]; - } -} - -- (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType usingBlock:(void (^)(QMUIAssetsGroup *resultAssetsGroup))enumerationBlock { - [self enumerateAllAlbumsWithAlbumContentType:contentType showEmptyAlbum:NO showSmartAlbumIfSupported:YES usingBlock:enumerationBlock]; -} - -- (void)saveImageWithImageRef:(CGImageRef)imageRef albumAssetsGroup:(QMUIAssetsGroup *)albumAssetsGroup orientation:(UIImageOrientation)orientation completionBlock:(QMUIWriteAssetCompletionBlock)completionBlock { - if (_usePhotoKit) { - PHAssetCollection *albumPhAssetCollection = albumAssetsGroup.phAssetCollection; - // 把图片加入到指定的相册对应的 PHAssetCollection - [[PHPhotoLibrary sharedPhotoLibrary] addImageToAlbum:imageRef - albumAssetCollection:albumPhAssetCollection - orientation:orientation - completionHandler:^(BOOL success, NSDate *creationDate, NSError *error) { - if (success) { - PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; - fetchOptions.predicate = [NSPredicate predicateWithFormat:@"creationDate = %@", creationDate]; - PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:albumPhAssetCollection options:fetchOptions]; - PHAsset *phAsset = fetchResult.lastObject; - QMUIAsset *asset = [[QMUIAsset alloc] initWithPHAsset:phAsset]; - completionBlock(asset, error); - } else { - QMUILog(@"Get PHAsset of image error: %@", error); - completionBlock(nil, error); - } - }]; - } else { - ALAssetsGroup *assetGroup = albumAssetsGroup.alAssetsGroup; - [_alAssetsLibrary writeImageToSavedPhotosAlbum:imageRef - albumAssetsGroup:assetGroup - orientation:orientation - completionBlock:^ (NSURL *assetURL, NSError *error) { - [_alAssetsLibrary assetForURL:assetURL - resultBlock:^(ALAsset *asset) { - QMUIAsset *resultAsset = [[QMUIAsset alloc] initWithALAsset:asset]; - completionBlock(resultAsset, error); - } failureBlock:^(NSError *error) { - QMUILog(@"Get ALAsset of image error : %@", error); - completionBlock(nil, error); - }]; - }]; - - } -} - -- (void)saveVideoWithVideoPathURL:(NSURL *)videoPathURL albumAssetsGroup:(QMUIAssetsGroup *)albumAssetsGroup completionBlock:(QMUIWriteAssetCompletionBlock)completionBlock { - if (_usePhotoKit) { - PHAssetCollection *albumPhAssetCollection = albumAssetsGroup.phAssetCollection; - // 把视频加入到指定的相册对应的 PHAssetCollection - [[PHPhotoLibrary sharedPhotoLibrary] addVideoToAlbum:videoPathURL - albumAssetCollection:albumPhAssetCollection - completionHandler:^(BOOL success, NSDate *creationDate, NSError *error) { - if (success) { - PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; - fetchOptions.predicate = [NSPredicate predicateWithFormat:@"creationDate = %@", creationDate]; - PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:albumPhAssetCollection options:fetchOptions]; - PHAsset *phAsset = fetchResult.lastObject; - QMUIAsset *asset = [[QMUIAsset alloc] initWithPHAsset:phAsset]; - completionBlock(asset, error); - } else { - QMUILog(@"Get PHAsset of video Error: %@", error); - completionBlock(nil, error); - } - }]; - } else { - ALAssetsGroup *assetsGroup = albumAssetsGroup.alAssetsGroup; - - [_alAssetsLibrary writeVideoAtPathToSavedPhotosAlbum:videoPathURL - albumAssetsGroup:assetsGroup - completionBlock:^ (NSURL *assetURL, NSError *error) { - [_alAssetsLibrary assetForURL:assetURL - resultBlock:^(ALAsset *asset) { - QMUIAsset *resultAsset = [[QMUIAsset alloc] initWithALAsset:asset]; - completionBlock(resultAsset, error); - } failureBlock:^(NSError *error) { - QMUILog(@"Get ALAsset of video error: %@", error); - completionBlock(nil, error); - }]; - }]; - - } -} - -- (void)refreshAssetsLibrary { - _alAssetsLibrary = [[ALAssetsLibrary alloc] init]; -} - -- (ALAssetsLibrary *)alAssetsLibrary { - return _alAssetsLibrary; -} - -- (PHCachingImageManager *)phCachingImageManager { - if (!_phCachingImageManager) { - _phCachingImageManager = [[PHCachingImageManager alloc] init]; - } - return _phCachingImageManager; -} - -@end - - -@implementation PHPhotoLibrary (QMUI) - -+ (PHFetchOptions *)createFetchOptionsWithAlbumContentType:(QMUIAlbumContentType)contentType { - PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; - // 根据输入的内容类型过滤相册内的资源 - switch (contentType) { - case QMUIAlbumContentTypeOnlyPhoto: - fetchOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType = %i", PHAssetMediaTypeImage]; - break; - - case QMUIAlbumContentTypeOnlyVideo: - fetchOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType = %i",PHAssetMediaTypeVideo]; - break; - - case QMUIAlbumContentTypeOnlyAudio: - fetchOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType = %i",PHAssetMediaTypeAudio]; - break; - - default: - break; - } - return fetchOptions; -} - -+ (NSArray *)fetchAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType showEmptyAlbum:(BOOL)showEmptyAlbum showSmartAlbum:(BOOL)showSmartAlbum { - NSMutableArray *tempAlbumsArray = [[NSMutableArray alloc] init]; - - // 创建一个 PHFetchOptions,用于创建 QMUIAssetsGroup 对资源的排序和类型进行控制 - PHFetchOptions *fetchOptions = [PHPhotoLibrary createFetchOptionsWithAlbumContentType:contentType]; - - PHFetchResult *fetchResult; - if (showSmartAlbum) { - // 允许显示系统的“智能相册” - // 获取保存了所有“智能相册”的 PHFetchResult - fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAny options:nil]; - } else { - // 不允许显示系统的智能相册,但由于在 PhotoKit 中,“相机胶卷”也属于“智能相册”,因此这里从“智能相册”中单独获取到“相机胶卷” - fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeSmartAlbumUserLibrary options:nil]; - } - // 循环遍历相册列表 - for (NSInteger i = 0; i < fetchResult.count; i++) { - // 获取一个相册 - PHCollection *collection = fetchResult[i]; - if ([collection isKindOfClass:[PHAssetCollection class]]) { - PHAssetCollection *assetCollection = (PHAssetCollection *)collection; - // 获取相册内的资源对应的 fetchResult,用于判断根据内容类型过滤后的资源数量是否大于 0,只有资源数量大于 0 的相册才会作为有效的相册显示 - PHFetchResult *currentFetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions]; - if (currentFetchResult.count > 0 || showEmptyAlbum) { - // 若相册不为空,或者允许显示空相册,则保存相册到结果数组 - // 判断如果是“相机胶卷”,则放到结果列表的第一位 - if (assetCollection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumUserLibrary) { - [tempAlbumsArray insertObject:assetCollection atIndex:0]; - } else { - [tempAlbumsArray addObject:assetCollection]; - } - } - } else { - NSAssert(NO, @"Fetch collection not PHCollection: %@", collection); - } - } - - // 获取所有用户自己建立的相册 - PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil]; - // 循环遍历用户自己建立的相册 - for (NSInteger i = 0; i < topLevelUserCollections.count; i++) { - // 获取一个相册 - PHCollection *collection = topLevelUserCollections[i]; - if ([collection isKindOfClass:[PHAssetCollection class]]) { - PHAssetCollection *assetCollection = (PHAssetCollection *)collection; - - if (showEmptyAlbum) { - // 允许显示空相册,直接保存相册到结果数组中 - [tempAlbumsArray addObject:assetCollection]; - } else { - // 不允许显示空相册,需要判断当前相册是否为空 - PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions]; - // 获取相册内的资源对应的 fetchResult,用于判断根据内容类型过滤后的资源数量是否大于 0 - if (fetchResult.count > 0) { - [tempAlbumsArray addObject:assetCollection]; - } - } - } - } - NSArray *resultAlbumsArray = [tempAlbumsArray copy]; - return resultAlbumsArray; -} - -+ (PHAsset *)fetchLatestAssetWithAssetCollection:(PHAssetCollection *)assetCollection { - PHFetchOptions *fetchOptions = [[PHFetchOptions alloc] init]; - // 按时间的先后对 PHAssetCollection 内的资源进行排序,最新的资源排在数组最后面 - fetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]]; - PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions]; - // 获取 PHAssetCollection 内最后一个资源,即最新的资源 - PHAsset *latestAsset = fetchResult.lastObject; - return latestAsset; -} - -- (void)addImageToAlbum:(CGImageRef)imageRef albumAssetCollection:(PHAssetCollection *)albumAssetCollection orientation:(UIImageOrientation)orientation completionHandler:(void(^)(BOOL success, NSDate *creationDate, NSError *error))completionHandler { - UIImage *targetImage = [UIImage imageWithCGImage:imageRef scale:ScreenScale orientation:orientation]; - __block NSDate *creationDate = nil; - [self performChanges:^{ - // 创建一个以图片生成新的 PHAsset,这时图片已经被添加到“相机胶卷” - - PHAssetChangeRequest *assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:targetImage]; - assetChangeRequest.creationDate = [NSDate date]; - creationDate = assetChangeRequest.creationDate; - - if (albumAssetCollection.assetCollectionType == PHAssetCollectionTypeAlbum) { - // 如果传入的相册类型为标准的相册(非“智能相册”和“时刻”),则把刚刚创建的 Asset 添加到传入的相册中。 - - // 创建一个改变 PHAssetCollection 的请求,并指定相册对应的 PHAssetCollection - PHAssetCollectionChangeRequest *assetCollectionChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:albumAssetCollection]; - /** - * 把 PHAsset 加入到对应的 PHAssetCollection 中,系统推荐的方法是调用 placeholderForCreatedAsset , - * 返回一个的 placeholder 来代替刚创建的 PHAsset 的引用,并把该引用加入到一个 PHAssetCollectionChangeRequest 中。 - */ - [assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]]; - } - - } completionHandler:^(BOOL success, NSError *error) { - if (!success) { - QMUILog(@"Creating asset of image error : %@", error); - } - - if (completionHandler) { - /** - * performChanges:completionHandler 不在主线程执行,若用户在该 block 中操作 UI 时会产生一些问题, - * 为了避免这种情况,这里该 block 主动放到主线程执行。 - */ - dispatch_async(dispatch_get_main_queue(), ^{ - completionHandler(success, creationDate, error); - }); - } - }]; -} - -- (void)addVideoToAlbum:(NSURL *)videoPathURL albumAssetCollection:(PHAssetCollection *)albumAssetCollection completionHandler:(void(^)(BOOL success, NSDate *creationDate, NSError *error))completionHandler { - __block NSDate *creationDate = nil; - [self performChanges:^{ - // 创建一个以视频生成新的 PHAsset 的请求 - PHAssetChangeRequest *assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:videoPathURL]; - assetChangeRequest.creationDate = [NSDate date]; - creationDate = assetChangeRequest.creationDate; - - if (albumAssetCollection.assetCollectionType == PHAssetCollectionTypeAlbum) { - // 如果传入的相册类型为标准的相册(非“智能相册”和“时刻”),则把刚刚创建的 Asset 添加到传入的相册中。 - - // 创建一个改变 PHAssetCollection 的请求,并指定相册对应的 PHAssetCollection - PHAssetCollectionChangeRequest *assetCollectionChangeRequest = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:albumAssetCollection]; - /** - * 把 PHAsset 加入到对应的 PHAssetCollection 中,系统推荐的方法是调用 placeholderForCreatedAsset , - * 返回一个的 placeholder 来代替刚创建的 PHAsset 的引用,并把该引用加入到一个 PHAssetCollectionChangeRequest 中。 - */ - [assetCollectionChangeRequest addAssets:@[[assetChangeRequest placeholderForCreatedAsset]]]; - } - - } completionHandler:^(BOOL success, NSError *error) { - if (!success) { - QMUILog(@"Creating asset of video error: %@", error); - } - - if (completionHandler) { - /** - * performChanges:completionHandler 不在主线程执行,若用户在该 block 中操作 UI 时会产生一些问题, - * 为了避免这种情况,这里该 block 主动放到主线程执行。 - */ - dispatch_async(dispatch_get_main_queue(), ^{ - completionHandler(success, creationDate, error); - }); - } - }]; -} - -@end - - -@implementation ALAssetsLibrary (QMUI) - -- (void)enumerateAllAlbumsWithAlbumContentType:(QMUIAlbumContentType)contentType usingBlock:(ALAssetsLibraryGroupsEnumerationResultsBlock)enumerationBlock { - [self enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop) { - if (group) { - // 根据输入的内容类型过滤相册内的资源,过滤后 group.numberOfAssets 的值会由自动被更新 - switch (contentType) { - case QMUIAlbumContentTypeOnlyPhoto: - [group setAssetsFilter:[ALAssetsFilter allPhotos]]; - break; - - case QMUIAlbumContentTypeOnlyVideo: - [group setAssetsFilter:[ALAssetsFilter allVideos]]; - break; - - default: - break; - } - if (group.numberOfAssets > 0) { - if (enumerationBlock) { - enumerationBlock(group, stop); - } - } - } else { - /** - * 枚举结束,再调用一次 enumerationBlock,并传递 nil 作为实参,作为枚举相册结束的标记。 - * 与系统 ALAssetsLibrary enumerateGroupsWithTypes 本身处理枚举结束的方式保持一致。 - */ - if (enumerationBlock) { - enumerationBlock(nil, stop); - } - } - } failureBlock:^(NSError *error) { - QMUILog(@"Asset group not found!error: %@", error); - }]; -} - -- (void)writeImageToSavedPhotosAlbum:(CGImageRef)imageRef albumAssetsGroup:(ALAssetsGroup *)albumAssetsGroup orientation:(UIImageOrientation)orientation completionBlock:(ALAssetsLibraryWriteImageCompletionBlock)completionBlock { - // 调用系统的添加照片的接口,把图片保存到相机胶卷,从而生成一个图片的 ALAsset - [self writeImageToSavedPhotosAlbum:imageRef - orientation:(ALAssetOrientation)orientation - completionBlock:^(NSURL *assetURL, NSError *error) { - if (error) { - if (completionBlock) { - completionBlock(assetURL, error); - } - } else { - // 把获取到的 ALAsset 添加到用户指定的相册中 - [self addAssetURL:assetURL - albumAssetsGroup:albumAssetsGroup - completionBlock:^(NSError *error) { - if (completionBlock) { - completionBlock(assetURL, error); - } - }]; - } - }]; -} - -- (void)writeVideoAtPathToSavedPhotosAlbum:(NSURL *)videoPathURL albumAssetsGroup:(ALAssetsGroup *)albumAssetsGroup completionBlock:(ALAssetsLibraryWriteImageCompletionBlock)completionBlock { - // 调用系统的添加照片的接口,把图片保存到相机胶卷,从而生成一个图片的 ALAsset - [self writeVideoAtPathToSavedPhotosAlbum:videoPathURL completionBlock:^(NSURL *assetURL, NSError *error) { - if (error) { - if (completionBlock) { - completionBlock(assetURL, error); - } - } else { - // 把获取到的 ALAsset 添加到用户指定的相册中 - [self addAssetURL:assetURL - albumAssetsGroup:albumAssetsGroup - completionBlock:^(NSError *error) { - if (completionBlock) { - completionBlock(assetURL, error); - } - }]; - } - }]; -} - -- (void)addAssetURL:(NSURL *)assetURL albumAssetsGroup:(ALAssetsGroup *)albumAssetsGroup completionBlock:(ALAssetsLibraryAccessFailureBlock)completionBlock { - [self assetForURL:assetURL - resultBlock:^(ALAsset *asset) { - [albumAssetsGroup addAsset:asset]; - completionBlock(nil); - } - failureBlock:^(NSError *error) { - completionBlock(error); - }]; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIAlbumViewController.h b/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIAlbumViewController.h deleted file mode 100644 index eee7ce7e..00000000 --- a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIAlbumViewController.h +++ /dev/null @@ -1,96 +0,0 @@ -// -// QMUIAlbumViewController.h -// qmui -// -// Created by Kayo Lee on 15/5/3. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import -#import "QMUICommonTableViewController.h" -#import "QMUITableViewCell.h" -#import "QMUIAssetsGroup.h" - -// 相册预览图的大小默认值 -extern const CGFloat QMUIAlbumViewControllerDefaultAlbumTableViewCellHeight; -// 相册名称的字号默认值 -extern const CGFloat QMUIAlbumTableViewCellDefaultAlbumNameFontSize; -// 相册资源数量的字号默认值 -extern const CGFloat QMUIAlbumTableViewCellDefaultAlbumAssetsNumberFontSize; -// 相册名称的 insets 默认值 -extern const UIEdgeInsets QMUIAlbumTableViewCellDefaultAlbumNameInsets; - - -@class QMUIImagePickerViewController; -@class QMUIAlbumViewController; -@class QMUITableViewCell; - -@protocol QMUIAlbumViewControllerDelegate - -@required -/// 点击相簿里某一行时,需要给一个 QMUIImagePickerViewController 对象用于展示九宫格图片列表 -- (QMUIImagePickerViewController *)imagePickerViewControllerForAlbumViewController:(QMUIAlbumViewController *)albumViewController; - -@optional -/** - * 取消查看相册列表后被调用 - */ -- (void)albumViewControllerDidCancel:(QMUIAlbumViewController *)albumViewController; - -/** - * 即将需要显示 Loading 时调用 - * - * @see shouldShowDefaultLoadingView - */ -- (void)albumViewControllerWillStartLoad:(QMUIAlbumViewController *)albumViewController; - -/** - * 即将需要隐藏 Loading 时调用 - * - * @see shouldShowDefaultLoadingView - */ -- (void)albumViewControllerWillFinishLoad:(QMUIAlbumViewController *)albumViewController; - -@end - - -@interface QMUIAlbumTableViewCell : QMUITableViewCell - -@property(nonatomic, assign) CGFloat albumNameFontSize UI_APPEARANCE_SELECTOR; // 相册名称的字号 -@property(nonatomic, assign) CGFloat albumAssetsNumberFontSize UI_APPEARANCE_SELECTOR; // 相册资源数量的字号 -@property(nonatomic, assign) UIEdgeInsets albumNameInsets UI_APPEARANCE_SELECTOR; // 相册名称的 insets - -@end - -/** - * 当前设备照片里的相簿列表,使用方式: - * 1. 使用 init 初始化。 - * 2. 指定一个 albumViewControllerDelegate,并实现 @required 方法。 - * - * @warning 注意,iOS 访问相册需要得到授权,建议先询问用户授权,通过了再进行 QMUIAlbumViewController 的初始化工作。关于授权的代码,可参考 QMUI Demo 项目里的 [QDImagePickerExampleViewController authorizationPresentAlbumViewControllerWithTitle] 方法。 - * @see [QMUIAssetsManager requestAuthorization:] - */ -@interface QMUIAlbumViewController : QMUICommonTableViewController - -@property(nonatomic, assign) CGFloat albumTableViewCellHeight UI_APPEARANCE_SELECTOR; // 相册列表 cell 的高度,同时也是相册预览图的宽高 - -@property(nonatomic, weak) id albumViewControllerDelegate; - -@property(nonatomic, assign) QMUIAlbumContentType contentType; // 相册展示内容的类型,可以控制只展示照片、视频或音频(仅 iOS 8.0 及以上版本支持)的其中一种,也可以同时展示所有类型的资源,默认展示所有类型的资源。 - -@property(nonatomic, copy) NSString *tipTextWhenNoPhotosAuthorization; -@property(nonatomic, copy) NSString *tipTextWhenPhotosEmpty; -/** - * 加载相册列表时会出现 loading,若需要自定义 loading 的形式,可将该属性置为 NO,默认为 YES。 - * @see albumViewControllerWillStartLoad: & albumViewControllerWillFinishLoad: - */ -@property(nonatomic, assign) BOOL shouldShowDefaultLoadingView; - -@end - - -@interface QMUIAlbumViewController (UIAppearance) - -+ (instancetype)appearance; - -@end diff --git a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIAlbumViewController.m b/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIAlbumViewController.m deleted file mode 100644 index 2e60e528..00000000 --- a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIAlbumViewController.m +++ /dev/null @@ -1,258 +0,0 @@ -// -// QMUIAlbumViewController.m -// qmui -// -// Created by Kayo Lee on 15/5/3. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIAlbumViewController.h" -#import "QMUICore.h" -#import "QMUIButton.h" -#import "UIView+QMUI.h" -#import "QMUIAssetsManager.h" -#import "QMUIImagePickerViewController.h" -#import -#import -#import -#import -#import -#import - -// 相册预览图的大小默认值 -const CGFloat QMUIAlbumViewControllerDefaultAlbumTableViewCellHeight = 57; -// 相册名称的字号默认值 -const CGFloat QMUIAlbumTableViewCellDefaultAlbumNameFontSize = 16; -// 相册资源数量的字号默认值 -const CGFloat QMUIAlbumTableViewCellDefaultAlbumAssetsNumberFontSize = 16; -// 相册名称的 insets 默认值 -const UIEdgeInsets QMUIAlbumTableViewCellDefaultAlbumNameInsets = {0, 8, 0, 4}; - - -#pragma mark - QMUIAlbumTableViewCell - -@implementation QMUIAlbumTableViewCell { - CALayer *_bottomLineLayer; -} - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [QMUIAlbumTableViewCell appearance].albumNameFontSize = QMUIAlbumTableViewCellDefaultAlbumNameFontSize; - [QMUIAlbumTableViewCell appearance].albumAssetsNumberFontSize = QMUIAlbumTableViewCellDefaultAlbumAssetsNumberFontSize; - [QMUIAlbumTableViewCell appearance].albumNameInsets = QMUIAlbumTableViewCellDefaultAlbumNameInsets; - }); -} - -- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { - if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { - self.albumNameFontSize = [QMUIAlbumTableViewCell appearance].albumNameFontSize; - self.albumAssetsNumberFontSize = [QMUIAlbumTableViewCell appearance].albumAssetsNumberFontSize; - self.albumNameInsets = [QMUIAlbumTableViewCell appearance].albumNameInsets; - - self.imageView.contentMode = UIViewContentModeScaleAspectFill; - self.imageView.clipsToBounds = YES; - self.detailTextLabel.textColor = UIColorGrayDarken; - - _bottomLineLayer = [[CALayer alloc] init]; - _bottomLineLayer.backgroundColor = UIColorSeparator.CGColor; - // 让分隔线垫在背后 - [self.layer insertSublayer:_bottomLineLayer atIndex:0]; - } - return self; -} - -- (void)updateCellAppearanceWithIndexPath:(NSIndexPath *)indexPath { - [super updateCellAppearanceWithIndexPath:indexPath]; - self.textLabel.font = UIFontBoldMake(self.albumNameFontSize); - self.detailTextLabel.font = UIFontMake(self.albumAssetsNumberFontSize); -} - -- (void)layoutSubviews { - [super layoutSubviews]; - // 避免iOS7下seletedBackgroundView会往上下露出1px(以盖住系统分隔线,但我们的分隔线是自定义的) - self.selectedBackgroundView.frame = self.bounds; - - CGFloat contentViewPaddingRight = 10; - self.imageView.frame = CGRectMake(0, 0, CGRectGetHeight(self.contentView.bounds), CGRectGetHeight(self.contentView.bounds)); - self.textLabel.frame = CGRectSetXY(self.textLabel.frame, CGRectGetMaxX(self.imageView.frame) + self.albumNameInsets.left, flat([self.textLabel qmui_minYWhenCenterInSuperview])); - CGFloat textLabelMaxWidth = CGRectGetWidth(self.contentView.bounds) - contentViewPaddingRight - CGRectGetWidth(self.detailTextLabel.frame) - self.albumNameInsets.right - CGRectGetMinX(self.textLabel.frame); - if (CGRectGetWidth(self.textLabel.frame) > textLabelMaxWidth) { - self.textLabel.frame = CGRectSetWidth(self.textLabel.frame, textLabelMaxWidth); - } - - self.detailTextLabel.frame = CGRectSetXY(self.detailTextLabel.frame, CGRectGetMaxX(self.textLabel.frame) + self.albumNameInsets.right, flat([self.detailTextLabel qmui_minYWhenCenterInSuperview])); - _bottomLineLayer.frame = CGRectMake(0, CGRectGetHeight(self.contentView.bounds) - PixelOne, CGRectGetWidth(self.bounds), PixelOne); -} - -- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated { - [super setHighlighted:highlighted animated:animated]; - _bottomLineLayer.hidden = highlighted; -} - -@end - - -#pragma mark - QMUIAlbumViewController (UIAppearance) - -@implementation QMUIAlbumViewController (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken1; - dispatch_once(&onceToken1, ^{ - [self appearance]; // +initialize 时就先设置好默认样式 - }); -} - -static QMUIAlbumViewController *albumViewControllerAppearance; -+ (instancetype)appearance { - static dispatch_once_t onceToken2; - dispatch_once(&onceToken2, ^{ - if (!albumViewControllerAppearance) { - albumViewControllerAppearance = [[QMUIAlbumViewController alloc] init]; - albumViewControllerAppearance.albumTableViewCellHeight = QMUIAlbumViewControllerDefaultAlbumTableViewCellHeight; - } - }); - return albumViewControllerAppearance; -} - -@end - - -#pragma mark - QMUIAlbumViewController - -@implementation QMUIAlbumViewController { - QMUIImagePickerViewController *_imagePickerViewController; - NSMutableArray *_albumsArray; - - BOOL _usePhotoKit; -} - -- (void)didInitialized { - [super didInitialized]; - _usePhotoKit = IOS_VERSION >= 8.0; - _shouldShowDefaultLoadingView = YES; - if (albumViewControllerAppearance) { - // 避免 albumViewControllerAppearance init 时走到这里来,导致死循环 - self.albumTableViewCellHeight = [QMUIAlbumViewController appearance].albumTableViewCellHeight; - } -} - -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; - if (!self.title) { - self.title = @"照片"; - } - self.navigationItem.rightBarButtonItem = [QMUINavigationButton barButtonItemWithType:QMUINavigationButtonTypeNormal title:@"取消" position:QMUINavigationButtonPositionRight target:self action:@selector(handleCancelSelectAlbum:)]; -} - -- (void)initTableView { - [super initTableView]; - self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - if ([QMUIAssetsManager authorizationStatus] == QMUIAssetAuthorizationStatusNotAuthorized) { - // 如果没有获取访问授权,或者访问授权状态已经被明确禁止,则显示提示语,引导用户开启授权 - NSString *tipString = self.tipTextWhenNoPhotosAuthorization; - if (!tipString) { - NSDictionary *mainInfoDictionary = [[NSBundle mainBundle] infoDictionary]; - NSString *appName = [mainInfoDictionary objectForKey:@"CFBundleDisplayName"]; - if (!appName) { - appName = [mainInfoDictionary objectForKey:(NSString *)kCFBundleNameKey]; - } - tipString = [NSString stringWithFormat:@"请在设备的\"设置-隐私-照片\"选项中,允许%@访问你的手机相册", appName]; - } - [self showEmptyViewWithText:tipString detailText:nil buttonTitle:nil buttonAction:nil]; - } else { - - _albumsArray = [[NSMutableArray alloc] init]; - - // 获取相册列表较为耗时,交给子线程去处理,因此这里需要显示 Loading - if ([self.albumViewControllerDelegate respondsToSelector:@selector(albumViewControllerWillStartLoad:)]) { - [self.albumViewControllerDelegate albumViewControllerWillStartLoad:self]; - } - if (self.shouldShowDefaultLoadingView) { - [self showEmptyViewWithLoading]; - } - dispatch_async(dispatch_get_global_queue(0, 0), ^{ - [[QMUIAssetsManager sharedInstance] enumerateAllAlbumsWithAlbumContentType:self.contentType usingBlock:^(QMUIAssetsGroup *resultAssetsGroup) { - dispatch_async(dispatch_get_main_queue(), ^{ - // 这里需要对 UI 进行操作,因此放回主线程处理 - if (resultAssetsGroup) { - [_albumsArray addObject:resultAssetsGroup]; - } else { - [self refreshAlbumAndShowEmptyTipIfNeed]; - } - }); - }]; - }); - } -} - -- (void)refreshAlbumAndShowEmptyTipIfNeed { - if ([_albumsArray count] > 0) { - if ([self.albumViewControllerDelegate respondsToSelector:@selector(albumViewControllerWillFinishLoad:)]) { - [self.albumViewControllerDelegate albumViewControllerWillFinishLoad:self]; - } - if (self.shouldShowDefaultLoadingView) { - [self hideEmptyView]; - } - [self.tableView reloadData]; - } else { - NSString *tipString = self.tipTextWhenPhotosEmpty ? : @"空照片"; - [self showEmptyViewWithText:tipString detailText:nil buttonTitle:nil buttonAction:nil]; - } -} - -#pragma mark - - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return [_albumsArray count]; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - return self.albumTableViewCellHeight; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - static NSString *kCellIdentifer = @"cell"; - QMUIAlbumTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifer]; - if (!cell) { - cell = [[QMUIAlbumTableViewCell alloc] initForTableView:self.tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:kCellIdentifer]; - cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; - } - QMUIAssetsGroup *assetsGroup = [_albumsArray objectAtIndex: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; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - if (!_imagePickerViewController) { - _imagePickerViewController = [self.albumViewControllerDelegate imagePickerViewControllerForAlbumViewController:self]; - } - NSAssert(_imagePickerViewController, @"self.%@ 必须实现 %@ 并返回一个 %@ 对象", NSStringFromSelector(@selector(albumViewControllerDelegate)), NSStringFromSelector(@selector(imagePickerViewControllerForAlbumViewController:)), NSStringFromClass([QMUIImagePickerViewController class])); - QMUIAssetsGroup *assetsGroup = [_albumsArray objectAtIndex:indexPath.row]; - [_imagePickerViewController refreshWithAssetsGroup:assetsGroup]; - _imagePickerViewController.title = [assetsGroup name]; - [self.navigationController pushViewController:_imagePickerViewController animated:YES]; -} - -- (void)handleCancelSelectAlbum:(id)sender { - [self.navigationController dismissViewControllerAnimated:YES completion:^(void) { - if (self.albumViewControllerDelegate && [self.albumViewControllerDelegate respondsToSelector:@selector(albumViewControllerDidCancel:)]) { - [self.albumViewControllerDelegate albumViewControllerDidCancel:self]; - } - }]; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h b/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h deleted file mode 100644 index e1baf096..00000000 --- a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h +++ /dev/null @@ -1,65 +0,0 @@ -// -// QMUIImagePickerCollectionViewCell.h -// qmui -// -// Created by 李浩成 on 16/8/29. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import -#import -#import "QMUIAsset.h" - -@class QMUIPieProgressView; - -// checkbox 的 margin 默认值 -extern const UIEdgeInsets QMUIImagePickerCollectionViewCellDefaultCheckboxButtonMargins; - -/** - * 图片选择空间里的九宫格 cell,支持显示 checkbox、饼状进度条及重试按钮(iCloud 图片需要) - */ -@interface QMUIImagePickerCollectionViewCell : UICollectionViewCell - -/// checkbox 未被选中时显示的图片 -@property(nonatomic, strong) UIImage *checkboxImage UI_APPEARANCE_SELECTOR; - -/// checkbox 被选中时显示的图片 -@property(nonatomic, strong) UIImage *checkboxCheckedImage UI_APPEARANCE_SELECTOR; - -/// checkbox 的 margin,定位从每个 cell(即每张图片)的最右边开始计算 -@property(nonatomic, assign) UIEdgeInsets checkboxButtonMargins UI_APPEARANCE_SELECTOR; - -/// progressView tintColor -@property(nonatomic, strong) UIColor *progressViewTintColor UI_APPEARANCE_SELECTOR; - -/// downloadRetryButton 的 icon -@property(nonatomic, strong) UIImage *downloadRetryImage UI_APPEARANCE_SELECTOR; - -/// videoMarkImageView 的 icon -@property(nonatomic, strong) UIImage *videoMarkImage UI_APPEARANCE_SELECTOR; - -/// videoMarkImageView 的 margin,定位从每个 cell(即每张图片)的左下角开始计算 -@property(nonatomic, assign) UIEdgeInsets videoMarkImageViewMargins UI_APPEARANCE_SELECTOR; - -/// videoDurationLabel 的字号 -@property(nonatomic, strong) UIFont *videoDurationLabelFont UI_APPEARANCE_SELECTOR; - -/// videoDurationLabel 的字体颜色 -@property(nonatomic, strong) UIColor *videoDurationLabelTextColor UI_APPEARANCE_SELECTOR; - -/// videoDurationLabel 布局是对齐右下角再做 margins 偏移 -@property(nonatomic, assign) UIEdgeInsets videoDurationLabelMargins UI_APPEARANCE_SELECTOR; - -@property(nonatomic, strong, readonly) UIImageView *contentImageView; -@property(nonatomic, strong, readonly) UIButton *checkboxButton; -@property(nonatomic, strong, readonly) QMUIPieProgressView *progressView; -@property(nonatomic, strong, readonly) UIButton *downloadRetryButton; -@property(nonatomic, strong, readonly) UIImageView *videoMarkImageView; -@property(nonatomic, strong, readonly) UILabel *videoDurationLabel; -@property(nonatomic, strong, readonly) CAGradientLayer *videoBottomShadowLayer; - -@property(nonatomic, assign, getter=isEditing) BOOL editing; -@property(nonatomic, assign, getter=isChecked) BOOL checked; -@property(nonatomic, assign) QMUIAssetDownloadStatus downloadStatus; // Cell 中对应资源的下载状态,这个值的变动会相应地调整 UI 表现 - -@end diff --git a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m b/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m deleted file mode 100644 index b08f7db6..00000000 --- a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m +++ /dev/null @@ -1,279 +0,0 @@ -// -// QMUIImagePickerCollectionViewCell.m -// qmui -// -// Created by 李浩成 on 16/8/29. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIImagePickerCollectionViewCell.h" -#import "QMUICore.h" -#import "QMUIImagePickerHelper.h" -#import "QMUIPieProgressView.h" -#import "UIControl+QMUI.h" -#import "UILabel+QMUI.h" -#import "CALayer+QMUI.h" - -// checkbox 的 margin 默认值 -const UIEdgeInsets QMUIImagePickerCollectionViewCellDefaultCheckboxButtonMargins = {2, 0, 0, 2}; -const UIEdgeInsets QMUIImagePickerCollectionViewCellDefaultVideoMarkImageViewMargins = {0, 8, 8, 0}; - - -@interface QMUIImagePickerCollectionViewCell () - -@property(nonatomic, strong, readwrite) UIButton *checkboxButton; - -@end - - -@implementation QMUIImagePickerCollectionViewCell - -@synthesize videoMarkImageView = _videoMarkImageView; -@synthesize videoDurationLabel = _videoDurationLabel; -@synthesize videoBottomShadowLayer = _videoBottomShadowLayer; - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [QMUIImagePickerCollectionViewCell appearance].checkboxImage = [QMUIHelper imageWithName:@"QMUI_pickerImage_checkbox"]; - [QMUIImagePickerCollectionViewCell appearance].checkboxCheckedImage = [QMUIHelper imageWithName:@"QMUI_pickerImage_checkbox_checked"]; - [QMUIImagePickerCollectionViewCell appearance].checkboxButtonMargins = QMUIImagePickerCollectionViewCellDefaultCheckboxButtonMargins; - [QMUIImagePickerCollectionViewCell appearance].progressViewTintColor = UIColorWhite; - [QMUIImagePickerCollectionViewCell appearance].downloadRetryImage = [QMUIHelper imageWithName:@"QMUI_icloud_download_fault_small"]; - [QMUIImagePickerCollectionViewCell appearance].videoMarkImage = [QMUIHelper imageWithName:@"QMUI_pickerImage_video_mark"]; - [QMUIImagePickerCollectionViewCell appearance].videoMarkImageViewMargins = QMUIImagePickerCollectionViewCellDefaultVideoMarkImageViewMargins; - [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelFont = UIFontMake(12); - [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelTextColor = UIColorWhite; - [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelMargins = UIEdgeInsetsMake(0, 0, 6, 6); - }); -} - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - [self initImagePickerCollectionViewCellUI]; - self.checkboxImage = [QMUIImagePickerCollectionViewCell appearance].checkboxImage; - self.checkboxCheckedImage = [QMUIImagePickerCollectionViewCell appearance].checkboxCheckedImage; - self.checkboxButtonMargins = [QMUIImagePickerCollectionViewCell appearance].checkboxButtonMargins; - self.progressViewTintColor = [QMUIImagePickerCollectionViewCell appearance].progressViewTintColor; - self.downloadRetryImage = [QMUIImagePickerCollectionViewCell appearance].downloadRetryImage; - self.videoMarkImage = [QMUIImagePickerCollectionViewCell appearance].videoMarkImage; - self.videoMarkImageViewMargins = [QMUIImagePickerCollectionViewCell appearance].videoMarkImageViewMargins; - self.videoDurationLabelFont = [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelFont; - self.videoDurationLabelTextColor = [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelTextColor; - self.videoDurationLabelMargins = [QMUIImagePickerCollectionViewCell appearance].videoDurationLabelMargins; - } - return self; -} - -- (void)initImagePickerCollectionViewCellUI { - _contentImageView = [[UIImageView alloc] init]; - self.contentImageView.contentMode = UIViewContentModeScaleAspectFill; - self.contentImageView.clipsToBounds = YES; - [self.contentView addSubview:self.contentImageView]; - - self.checkboxButton = [[UIButton alloc] init]; - self.checkboxButton.qmui_outsideEdge = UIEdgeInsetsMake(-6, -6, -6, -6); - self.checkboxButton.hidden = YES; - [self.contentView addSubview:self.checkboxButton]; - - _progressView = [[QMUIPieProgressView alloc] init]; - self.progressView.hidden = YES; - [self.contentView addSubview:self.progressView]; - - _downloadRetryButton = [[UIButton alloc] init]; - self.downloadRetryButton.qmui_outsideEdge = UIEdgeInsetsMake(-6, -6, -6, -6); - self.downloadRetryButton.hidden = YES; - [self.contentView addSubview:self.downloadRetryButton]; -} - -- (void)initVideoBottomShadowLayerIfNeeded { - if (!_videoBottomShadowLayer) { - _videoBottomShadowLayer = [CAGradientLayer layer]; - [_videoBottomShadowLayer qmui_removeDefaultAnimations]; - _videoBottomShadowLayer.colors = @[(id)UIColorMakeWithRGBA(0, 0, 0, 0).CGColor, (id)UIColorMakeWithRGBA(0, 0, 0, .6).CGColor]; - [self.contentView.layer addSublayer:_videoBottomShadowLayer]; - - [self setNeedsLayout]; - } -} - -- (void)initVideoMarkImageViewIfNeed { - if (_videoMarkImageView) { - return; - } - _videoMarkImageView = [[UIImageView alloc] init]; - [_videoMarkImageView setImage:self.videoMarkImage]; - [_videoMarkImageView sizeToFit]; - [self.contentView addSubview:_videoMarkImageView]; - - [self setNeedsLayout]; -} - -- (void)initVideoDurationLabelIfNeed { - if (_videoDurationLabel) { - return; - } - _videoDurationLabel = [[UILabel alloc] init]; - _videoDurationLabel.font = self.videoDurationLabelFont; - _videoDurationLabel.textColor = self.videoDurationLabelTextColor; - [self.contentView addSubview:_videoDurationLabel]; - - [self setNeedsLayout]; -} - -- (void)initVideoRelatedViewsIfNeeded { - [self initVideoBottomShadowLayerIfNeeded]; - [self initVideoMarkImageViewIfNeed]; - [self initVideoDurationLabelIfNeed]; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - self.contentImageView.frame = self.contentView.bounds; - if (_editing) { - self.checkboxButton.frame = CGRectFlatted(CGRectSetXY(self.checkboxButton.frame, CGRectGetWidth(self.contentView.bounds) - self.checkboxButtonMargins.right - CGRectGetWidth(self.checkboxButton.frame), self.checkboxButtonMargins.top)); - } - - /* 理论上 downloadRetryButton 应该在 setImage 后 sizeToFit 计算大小, - * 但因为当图片小于某个高度时, UIButton sizeToFit 时会自动改写 height 值, - * 因此,这里 downloadRetryButton 直接拿 downloadRetryButton 的 image 图片尺寸作为 frame size - */ - self.downloadRetryButton.frame = CGRectFlatted(CGRectMake(CGRectGetWidth(self.contentView.bounds) - self.checkboxButtonMargins.right - _downloadRetryImage.size.width, self.checkboxButtonMargins.top, _downloadRetryImage.size.width, _downloadRetryImage.size.height)); - self.progressView.frame = CGRectMake(CGRectGetMinX(self.downloadRetryButton.frame), CGRectGetMinY(self.downloadRetryButton.frame) + self.downloadRetryButton.contentEdgeInsets.top, CGRectGetWidth(self.downloadRetryButton.frame), CGRectGetHeight(self.downloadRetryButton.frame)); - - if (_videoBottomShadowLayer && _videoMarkImageView && _videoDurationLabel) { - _videoMarkImageView.frame = CGRectFlatted(CGRectSetXY(_videoMarkImageView.frame, self.videoMarkImageViewMargins.left, CGRectGetHeight(self.contentView.bounds) - CGRectGetHeight(_videoMarkImageView.frame) - self.videoMarkImageViewMargins.bottom)); - - [_videoDurationLabel sizeToFit]; - _videoDurationLabel.frame = ({ - CGFloat minX = CGRectGetWidth(self.contentView.bounds) - self.videoDurationLabelMargins.right - CGRectGetWidth(_videoDurationLabel.frame); - CGFloat minY = CGRectGetHeight(self.contentView.bounds) - self.videoDurationLabelMargins.bottom - CGRectGetHeight(_videoDurationLabel.frame); - CGRectFlatted(CGRectSetXY(_videoDurationLabel.frame, minX, minY)); - }); - - CGFloat videoBottomShadowLayerHeight = CGRectGetHeight(self.contentView.bounds) - CGRectGetMinY(_videoMarkImageView.frame) + self.videoMarkImageViewMargins.bottom;// 背景阴影遮罩的高度取决于(视频 icon 的高度 + 上下 margin) - _videoBottomShadowLayer.frame = CGRectMake(0, CGRectGetHeight(self.contentView.bounds) - videoBottomShadowLayerHeight, CGRectGetWidth(self.contentView.bounds), videoBottomShadowLayerHeight); - } -} - -- (void)setCheckboxImage:(UIImage *)checkboxImage { - if (![self.checkboxImage isEqual:checkboxImage]) { - [self.checkboxButton setImage:checkboxImage forState:UIControlStateNormal]; - [self.checkboxButton sizeToFit]; - } - _checkboxImage = checkboxImage; -} - -- (void)setCheckboxCheckedImage:(UIImage *)checkboxCheckedImage { - if (![self.checkboxCheckedImage isEqual:checkboxCheckedImage]) { - [self.checkboxButton setImage:checkboxCheckedImage forState:UIControlStateSelected]; - [self.checkboxButton setImage:checkboxCheckedImage forState:UIControlStateSelected|UIControlStateHighlighted]; - [self.checkboxButton sizeToFit]; - } - _checkboxCheckedImage = checkboxCheckedImage; -} - -- (void)setDownloadRetryImage:(UIImage *)downloadRetryImage { - if (![self.downloadRetryImage isEqual:downloadRetryImage]) { - [self.downloadRetryButton setImage:downloadRetryImage forState:UIControlStateNormal]; - } - _downloadRetryImage = downloadRetryImage; -} - -- (void)setProgressViewTintColor:(UIColor *)progressViewTintColor { - _progressView.tintColor = progressViewTintColor; - _progressViewTintColor = progressViewTintColor; -} - -- (void)setVideoMarkImage:(UIImage *)videoMarkImage { - if (![self.videoMarkImage isEqual:videoMarkImage]) { - [_videoMarkImageView setImage:videoMarkImage]; - [_videoMarkImageView sizeToFit]; - } - _videoMarkImage = videoMarkImage; -} - -- (void)setVideoDurationLabelFont:(UIFont *)videoDurationLabelFont { - if (![self.videoDurationLabelFont isEqual:videoDurationLabelFont]) { - _videoDurationLabel.font = videoDurationLabelFont; - [_videoDurationLabel qmui_calculateHeightAfterSetAppearance]; - } - _videoDurationLabelFont = videoDurationLabelFont; -} - -- (void)setVideoDurationLabelTextColor:(UIColor *)videoDurationLabelTextColor { - if (![self.videoDurationLabelTextColor isEqual:videoDurationLabelTextColor]) { - _videoDurationLabel.textColor = videoDurationLabelTextColor; - } - _videoDurationLabelTextColor = videoDurationLabelTextColor; -} - -- (void)setChecked:(BOOL)checked { - _checked = checked; - if (_editing) { - self.checkboxButton.selected = checked; - [QMUIImagePickerHelper removeSpringAnimationOfImageCheckedWithCheckboxButton:self.checkboxButton]; - if (checked) { - [QMUIImagePickerHelper springAnimationOfImageCheckedWithCheckboxButton:self.checkboxButton]; - } - } -} - -- (void)setEditing:(BOOL)editing { - _editing = editing; - if (self.downloadStatus == QMUIAssetDownloadStatusSucceed) { - self.checkboxButton.hidden = !_editing; - } -} - -- (void)setDownloadStatus:(QMUIAssetDownloadStatus)downloadStatus { - _downloadStatus = downloadStatus; - switch (downloadStatus) { - case QMUIAssetDownloadStatusSucceed: - if (_editing) { - self.checkboxButton.hidden = !_editing; - } - self.progressView.hidden = YES; - self.downloadRetryButton.hidden = YES; - break; - - case QMUIAssetDownloadStatusDownloading: - self.checkboxButton.hidden = YES; - self.progressView.hidden = NO; - self.downloadRetryButton.hidden = YES; - break; - - case QMUIAssetDownloadStatusCanceled: - self.checkboxButton.hidden = NO; - self.progressView.hidden = YES; - self.downloadRetryButton.hidden = YES; - break; - - case QMUIAssetDownloadStatusFailed: - self.progressView.hidden = YES; - self.checkboxButton.hidden = YES; - self.downloadRetryButton.hidden = NO; - break; - - default: - break; - } -} - -- (UILabel *)videoDurationLabel { - [self initVideoRelatedViewsIfNeeded]; - return _videoDurationLabel; -} - -- (UIImageView *)videoMarkImageView { - [self initVideoRelatedViewsIfNeeded]; - return _videoMarkImageView; -} - -- (CAGradientLayer *)videoBottomShadowLayer { - [self initVideoRelatedViewsIfNeeded]; - return _videoBottomShadowLayer; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h b/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h deleted file mode 100644 index ec385128..00000000 --- a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h +++ /dev/null @@ -1,78 +0,0 @@ -// -// QMUIImagePickerHelper.h -// qmui -// -// Created by Kayo Lee on 15/5/9. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import -#import "QMUIAsset.h" -#import "QMUIAssetsGroup.h" - -/** - * 配合 QMUIImagePickerViewController 使用的工具类 - */ -@interface QMUIImagePickerHelper : NSObject - -/** - * 判断一个由 QMUIAsset 对象组成的数组中是否包含特定的 QMUIAsset 对象 - * - * @param imageAssetArray 一个由 QMUIAsset 对象组成的数组 - * @param targetImageAsset 需要被判断的 QMUIAsset 对象 - * - */ -+ (BOOL)imageAssetArray:(NSMutableArray *)imageAssetArray containsImageAsset:(QMUIAsset *)targetImageAsset; - -/** - * 从一个由 QMUIAsset 对象组成的数组中移除特定的 QMUIAsset 对象(如果这个 QMUIAsset 对象不在该数组中,则不作处理) - * - * @param imageAssetArray 一个由 QMUIAsset 对象组成的数组 - * @param targetImageAsset 需要被移除的 QMUIAsset 对象 - */ -+ (void)imageAssetArray:(NSMutableArray *)imageAssetArray removeImageAsset:(QMUIAsset *)targetImageAsset; - -/** - * 选中图片数量改变时,展示图片数量的 Label 的动画,动画过程如下: - * Label 背景色改为透明,同时产生一个与背景颜色和形状、大小都相同的图形置于 Label 底下,做先缩小再放大的 spring 动画 - * 动画结束后移除该图形,并恢复 Label 的背景色 - * - * @warning iOS6 下降级处理不调用动画效果 - * - * @param label 需要做动画的 UILabel - */ -+ (void)springAnimationOfImageSelectedCountChangeWithCountLabel:(UILabel *)label; - -/** - * 图片 checkBox 被选中时的动画 - * @warning iOS6 下降级处理不调用动画效果 - * - * @param button 需要做动画的 checkbox 按钮 - */ -+ (void)springAnimationOfImageCheckedWithCheckboxButton:(UIButton *)button; - -/** - * 搭配springAnimationOfImageCheckedWithCheckboxButton:一起使用,添加animation之前建议先remove - */ -+ (void)removeSpringAnimationOfImageCheckedWithCheckboxButton:(UIButton *)button; - - -/** - * 获取最近一次调用 updateLastAlbumWithAssetsGroup 方法调用时储存的 QMUIAssetsGroup 对象 - * - * @param userIdentify 用户标识,由于每个用户可能需要分开储存一个最近调用过的 QMUIAssetsGroup,因此增加一个标识区分用户。 - * 一个常见的应用场景是选择图片时保存图片所在相册的对应的 QMUIAssetsGroup,并使用用户的 user id 作为区分不同用户的标识, - * 当用户再次选择图片时可以根据已经保存的 QMUIAssetsGroup 直接进入上次使用过的相册。 - */ -+ (QMUIAssetsGroup *)assetsGroupOfLastestPickerAlbumWithUserIdentify:(NSString *)userIdentify; - -/** - * 储存一个 QMUIAssetsGroup,从而储存一个对应的相册,与 assetsGroupOfLatestPickerAlbumWithUserIdentify 方法对应使用 - * - * @param assetsGroup 要被储存的 QMUIAssetsGroup - * @param albumContentType 相册的内容类型 - * @param userIdentify 用户标识,由于每个用户可能需要分开储存一个最近调用过的 QMUIAssetsGroup,因此增加一个标识区分用户 - */ -+ (void)updateLastestAlbumWithAssetsGroup:(QMUIAssetsGroup *)assetsGroup ablumContentType:(QMUIAlbumContentType)albumContentType userIdentify:(NSString *)userIdentify; - -@end diff --git a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m b/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m deleted file mode 100644 index ea329af8..00000000 --- a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m +++ /dev/null @@ -1,140 +0,0 @@ -// -// QMUIImagePickerHelper.m -// qmui -// -// Created by Kayo Lee on 15/5/9. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIImagePickerHelper.h" -#import "QMUICore.h" -#import "QMUIAssetsManager.h" -#import -#import -#import -#import "UIImage+QMUI.h" - - -static NSString * const kLastAlbumKeyPrefix = @"QMUILastAlbumKeyWith"; -static NSString * const kContentTypeOfLastAlbumKeyPrefix = @"QMUIContentTypeOfLastAlbumKeyWith"; - -@implementation QMUIImagePickerHelper - -+ (BOOL)imageAssetArray:(NSMutableArray *)imageAssetArray containsImageAsset:(QMUIAsset *)targetImageAsset { - NSString *targetAssetIdentify = [targetImageAsset assetIdentity]; - for (NSUInteger i = 0; i < [imageAssetArray count]; i++) { - QMUIAsset *imageAsset = [imageAssetArray objectAtIndex:i]; - if ([[imageAsset assetIdentity] isEqual:targetAssetIdentify]) { - return YES; - } - } - return NO; -} - -+ (void)imageAssetArray:(NSMutableArray *)imageAssetArray removeImageAsset:(QMUIAsset *)targetImageAsset { - NSString *targetAssetIdentify = [targetImageAsset assetIdentity]; - for (NSUInteger i = 0; i < [imageAssetArray count]; i++) { - QMUIAsset *imageAsset = [imageAssetArray objectAtIndex:i]; - if ([[imageAsset assetIdentity] isEqual:targetAssetIdentify]) { - [imageAssetArray removeObject:imageAsset]; - break; - } - } -} - -+ (void)springAnimationOfImageSelectedCountChangeWithCountLabel:(UILabel *)label { - [QMUIHelper actionSpringAnimationForView:label]; -} - -+ (void)springAnimationOfImageCheckedWithCheckboxButton:(UIButton *)button { - [QMUIHelper actionSpringAnimationForView:button]; -} - -+ (void)removeSpringAnimationOfImageCheckedWithCheckboxButton:(UIButton *)button { - [button.layer removeAnimationForKey:QMUISpringAnimationKey]; -} - -+ (QMUIAssetsGroup *)assetsGroupOfLastestPickerAlbumWithUserIdentify:(NSString *)userIdentify { - // 获取 NSUserDefaults,里面储存了所有 updateLastestAlbumWithAssetsGroup 的结果 - NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; - // 使用特定的前缀和可以标记不同用户的字符串拼接成 key,用于获取当前用户最近调用 updateLastestAlbumWithAssetsGroup 储存的相册以及对于的 QMUIAlbumContentType 值 - NSString *lastAlbumKey = [NSString stringWithFormat:@"%@%@", kLastAlbumKeyPrefix, userIdentify]; - NSString *contentTypeOflastAlbumKey = [NSString stringWithFormat:@"%@%@", kContentTypeOfLastAlbumKeyPrefix, userIdentify]; - - __block QMUIAssetsGroup *assetsGroup; - BOOL usePhotoKit = IOS_VERSION >= 8.0 ? YES : NO; - - QMUIAlbumContentType albumContentType = (QMUIAlbumContentType)[userDefaults integerForKey:contentTypeOflastAlbumKey]; - - if (usePhotoKit) { - NSString *groupIdentifier = [userDefaults valueForKey:lastAlbumKey]; - /** - * 如果获取到的 PHAssetCollection localIdentifier 不为空,则获取该 URL 对应的相册。 - * 用户升级设备的系统后,这里会从原来的 AssetsLibrary 改为用 PhotoKit, - * 因此原来储存的 groupIdentifier 实际上会是一个 NSURL 而不是我们需要的 NSString。 - * 所以这里还需要判断一下实际拿到的数据的类型是否为 NSString,如果是才继续进行。 - */ - if (groupIdentifier && [groupIdentifier isKindOfClass:[NSString class]]) { - PHFetchResult *phFetchResult = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[groupIdentifier] options:nil]; - if (phFetchResult.count > 0) { - // 创建一个 PHFetchOptions,用于对内容类型进行控制 - PHFetchOptions *phFetchOptions; - // 旧版本中没有存储 albumContentType,因此为了防止 crash,这里做一下判断 - if (albumContentType) { - phFetchOptions = [PHPhotoLibrary createFetchOptionsWithAlbumContentType:albumContentType]; - } - PHAssetCollection *phAssetCollection = [phFetchResult firstObject]; - assetsGroup = [[QMUIAssetsGroup alloc] initWithPHCollection:phAssetCollection fetchAssetsOptions:phFetchOptions]; - } - } else { - QMUILog(@"Group For localIdentifier is not found!"); - } - } else { - NSURL *groupUrl = [userDefaults URLForKey:lastAlbumKey]; - // 如果获取到的 ALAssetsGroup URL 不为空,则获取该 URL 对应的相册 - if (groupUrl) { - [[[QMUIAssetsManager sharedInstance] alAssetsLibrary] groupForURL:groupUrl resultBlock:^(ALAssetsGroup *group) { - if (group) { - assetsGroup = [[QMUIAssetsGroup alloc] initWithALAssetsGroup:group]; - // 对内容类型进行控制 - switch (albumContentType) { - case QMUIAlbumContentTypeOnlyPhoto: - [group setAssetsFilter:[ALAssetsFilter allPhotos]]; - break; - - case QMUIAlbumContentTypeOnlyVideo: - [group setAssetsFilter:[ALAssetsFilter allVideos]]; - break; - - default: - break; - } - } - } failureBlock:^(NSError *error) { - QMUILog(@"Group For URL is Error!"); - }]; - } - } - return assetsGroup; -} - -+ (void)updateLastestAlbumWithAssetsGroup:(QMUIAssetsGroup *)assetsGroup ablumContentType:(QMUIAlbumContentType)albumContentType userIdentify:(NSString *)userIdentify { - NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; - // 使用特定的前缀和可以标记不同用户的字符串拼接成 key,用于为当前用户储存相册对应的 QMUIAssetsGroup 与 QMUIAlbumContentType - NSString *lastAlbumKey = [NSString stringWithFormat:@"%@%@", kLastAlbumKeyPrefix, userIdentify]; - NSString *contentTypeOflastAlbumKey = [NSString stringWithFormat:@"%@%@", kContentTypeOfLastAlbumKeyPrefix, userIdentify]; - if (assetsGroup.alAssetsGroup) { - [userDefaults setURL:[assetsGroup.alAssetsGroup valueForProperty:ALAssetsGroupPropertyURL] forKey:lastAlbumKey]; - } else { - // 使用 PhotoKit - [userDefaults setValue:assetsGroup.phAssetCollection.localIdentifier forKey:lastAlbumKey]; - } - - [userDefaults setInteger:albumContentType forKey:contentTypeOflastAlbumKey]; - - [userDefaults synchronize]; -} - -@end - - diff --git a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h b/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h deleted file mode 100644 index 4cda471d..00000000 --- a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h +++ /dev/null @@ -1,79 +0,0 @@ -// -// QMUIImagePickerPreviewViewController.h -// qmui -// -// Created by Kayo Lee on 15/5/3. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIImagePreviewViewController.h" -#import "QMUIAsset.h" - -@class QMUIButton; -@class QMUIPieProgressView; -@class QMUIImagePickerViewController; -@class QMUIImagePickerPreviewViewController; - -@protocol QMUIImagePickerPreviewViewControllerDelegate - -@optional -/** - * 取消选择图片后被调用 - */ -- (void)imagePickerPreviewViewControllerDidCancel:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController; - -- (void)imagePickerPreviewViewController:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController willCheckImageAtIndex:(NSInteger)index; -- (void)imagePickerPreviewViewController:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController didCheckImageAtIndex:(NSInteger)index; - -- (void)imagePickerPreviewViewController:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController willUncheckImageAtIndex:(NSInteger)index; -- (void)imagePickerPreviewViewController:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController didUncheckImageAtIndex:(NSInteger)index; - -@end - - -@interface QMUIImagePickerPreviewViewController : QMUIImagePreviewViewController - -@property(nonatomic, strong) UIColor *toolBarBackgroundColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *toolBarTintColor UI_APPEARANCE_SELECTOR; - -@property(nonatomic, weak) id delegate; - -@property(nonatomic, strong, readonly) UIView *topToolBarView; -@property(nonatomic, strong, readonly) QMUIButton *backButton; -@property(nonatomic, strong, readonly) QMUIButton *checkboxButton; -@property(nonatomic, strong, readonly) QMUIPieProgressView *progressView; -@property(nonatomic, strong, readonly) UIButton *downloadRetryButton; - -/** - * 由于组件需要通过本地图片的 QMUIAsset 对象读取图片的详细信息,因此这里的需要传入的是包含一个或多个 QMUIAsset 对象的数组 - */ -@property(nonatomic, strong) NSMutableArray *imagesAssetArray; -@property(nonatomic, strong) NSMutableArray *selectedImageAssetArray; - -@property(nonatomic, assign) QMUIAssetDownloadStatus downloadStatus; -@property(nonatomic, assign) NSUInteger maximumSelectImageCount; // 最多可以选择的图片数,默认为无穷大 -@property(nonatomic, assign) NSUInteger minimumSelectImageCount; // 最少需要选择的图片数,默认为 0 -@property(nonatomic, copy) NSString *alertTitleWhenExceedMaxSelectImageCount; // 选择图片超出最大图片限制时 alertView 的标题 -@property(nonatomic, copy) NSString *alertButtonTitleWhenExceedMaxSelectImageCount; // 选择图片超出最大图片限制时 alertView 的标题 - -/** - * 更新数据并刷新 UI,手工调用 - * - * @param imageAssetArray 包含所有需要展示的图片的数组 - * @param selectedImageAssetArray 包含所有需要展示的图片中已经被选中的图片的数组 - * @param currentImageIndex 当前展示的图片在 imageAssetArray 的索引 - * @param singleCheckMode 是否为单选模式,如果是单选模式,则不显示 checkbox - */ -- (void)updateImagePickerPreviewViewWithImagesAssetArray:(NSArray *)imageAssetArray - selectedImageAssetArray:(NSArray *)selectedImageAssetArray - currentImageIndex:(NSInteger)currentImageIndex - singleCheckMode:(BOOL)singleCheckMode; - -@end - - -@interface QMUIImagePickerPreviewViewController (UIAppearance) - -+ (instancetype)appearance; - -@end diff --git a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m b/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m deleted file mode 100644 index 30ae0b38..00000000 --- a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m +++ /dev/null @@ -1,431 +0,0 @@ -// -// QMUIImagePickerPreviewViewController.m -// qmui -// -// Created by Kayo Lee on 15/5/3. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIImagePickerPreviewViewController.h" -#import "QMUICore.h" -#import "QMUIImagePickerViewController.h" -#import "QMUIImagePickerHelper.h" -#import "QMUIAssetsManager.h" -#import "QMUIZoomImageView.h" -#import "QMUIAsset.h" -#import "QMUIButton.h" -#import "QMUIImagePickerHelper.h" -#import "QMUIPieProgressView.h" -#import "QMUIAlertController.h" -#import "UIImage+QMUI.h" -#import "UIView+QMUI.h" -#import "UIControl+QMUI.h" -#import - -#define TopToolBarViewHeight 64 - -#pragma mark - QMUIImagePickerPreviewViewController (UIAppearance) - -@implementation QMUIImagePickerPreviewViewController (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; // +initialize 时就先设置好默认样式 - }); -} - -static QMUIImagePickerPreviewViewController *imagePickerPreviewViewControllerAppearance; -+ (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; -} - -@end - -@implementation QMUIImagePickerPreviewViewController { - BOOL _singleCheckMode; - BOOL _usePhotoKit; -} - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - self.maximumSelectImageCount = INT_MAX; - self.minimumSelectImageCount = 0; - _usePhotoKit = EnforceUseAssetsLibraryForTest ? NO : ((IOS_VERSION >= 8.0) ? YES : NO); - - if (imagePickerPreviewViewControllerAppearance) { - // 避免 imagePickerPreviewViewControllerAppearance init 时走到这里来,导致死循环 - self.toolBarBackgroundColor = [QMUIImagePickerPreviewViewController appearance].toolBarBackgroundColor; - self.toolBarTintColor = [QMUIImagePickerPreviewViewController appearance].toolBarTintColor; - } - } - return self; -} - -- (void)initSubviews { - [super initSubviews]; - - self.imagePreviewView.delegate = self; - - _topToolBarView = [[UIView alloc] init]; - self.topToolBarView.backgroundColor = self.toolBarBackgroundColor; - self.topToolBarView.tintColor = self.toolBarTintColor; - [self.view addSubview:self.topToolBarView]; - - _backButton = [[QMUIButton alloc] init]; - self.backButton.adjustsImageTintColorAutomatically = YES; - [self.backButton setImage:NavBarBackIndicatorImage forState:UIControlStateNormal]; - self.backButton.tintColor = self.topToolBarView.tintColor; - [self.backButton sizeToFit]; - [self.backButton addTarget:self action:@selector(handleCancelPreviewImage:) forControlEvents:UIControlEventTouchUpInside]; - self.backButton.qmui_outsideEdge = UIEdgeInsetsMake(-30, -20, -50, -80); - [self.topToolBarView addSubview:self.backButton]; - - _checkboxButton = [[QMUIButton alloc] init]; - self.checkboxButton.adjustsImageTintColorAutomatically = YES; - [self.checkboxButton setImage:[QMUIHelper imageWithName:@"QMUI_previewImage_checkbox"] forState:UIControlStateNormal]; - [self.checkboxButton setImage:[QMUIHelper imageWithName:@"QMUI_previewImage_checkbox_checked"] forState:UIControlStateSelected]; - [self.checkboxButton setImage:[QMUIHelper imageWithName:@"QMUI_previewImage_checkbox_checked"] forState:UIControlStateSelected|UIControlStateHighlighted]; - self.checkboxButton.tintColor = self.topToolBarView.tintColor; - [self.checkboxButton sizeToFit]; - [self.checkboxButton addTarget:self action:@selector(handleCheckButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - self.checkboxButton.qmui_outsideEdge = UIEdgeInsetsMake(-6, -6, -6, -6); - [self.topToolBarView addSubview:self.checkboxButton]; - - _progressView = [[QMUIPieProgressView alloc] init]; - self.progressView.tintColor = self.toolBarTintColor; - self.progressView.hidden = YES; - [self.topToolBarView addSubview:self.progressView]; - - _downloadRetryButton = [[UIButton alloc] init]; - [self.downloadRetryButton setImage:[QMUIHelper imageWithName:@"QMUI_icloud_download_fault"] forState:UIControlStateNormal]; - [self.downloadRetryButton sizeToFit]; - [self.downloadRetryButton addTarget:self action:@selector(handleDownloadRetryButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - self.downloadRetryButton.qmui_outsideEdge = UIEdgeInsetsMake(-6, -6, -6, -6); - self.downloadRetryButton.hidden = YES; - [self.topToolBarView addSubview:self.downloadRetryButton]; -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - [[UIApplication sharedApplication] setStatusBarHidden:YES]; - [self.navigationController setNavigationBarHidden:YES animated:NO]; - if (!_singleCheckMode) { - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:self.imagePreviewView.currentImageIndex]; - self.checkboxButton.selected = [self.selectedImageAssetArray containsObject:imageAsset]; - } -} - -- (void)viewWillDisappear:(BOOL)animated { - [super viewWillDisappear:animated]; - [[UIApplication sharedApplication] setStatusBarHidden:NO]; - [self.navigationController setNavigationBarHidden:NO animated:NO]; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - self.topToolBarView.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), TopToolBarViewHeight); - - CGFloat topToolbarPaddingTop = [[UIApplication sharedApplication] isStatusBarHidden] ? 0 : StatusBarHeight; - CGFloat topToolbarContentHeight = CGRectGetHeight(self.topToolBarView.bounds) - topToolbarPaddingTop; - self.backButton.frame = CGRectSetXY(self.backButton.frame, 8, topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.backButton.frame))); - if (!self.checkboxButton.hidden) { - self.checkboxButton.frame = CGRectSetXY(self.checkboxButton.frame, CGRectGetWidth(self.topToolBarView.frame) - 10 - CGRectGetWidth(self.checkboxButton.frame), topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.checkboxButton.frame))); - } - UIImage *downloadRetryImage = [self.downloadRetryButton imageForState:UIControlStateNormal]; - self.downloadRetryButton.frame = CGRectSetXY(self.downloadRetryButton.frame, CGRectGetWidth(self.topToolBarView.frame) - 10 - downloadRetryImage.size.width, topToolbarPaddingTop + CGFloatGetCenter(topToolbarContentHeight, CGRectGetHeight(self.downloadRetryButton.frame))); - /* 理论上 progressView 作为进度按钮,应该需要跟错误重试按钮 downloadRetryButton 的 frame 保持一致,但这里并没有直接使用 - * self.progressView.frame = self.downloadRetryButton.frame,这是因为 self.downloadRetryButton 具有 1pt 的 top - * contentEdgeInsets,因此最终的 frame 是椭圆型,如果按上面的操作,progressView 内部绘制出的饼状图形就会变成椭圆型, - * 因此,这里 progressView 直接拿 downloadRetryButton 的 image 图片尺寸作为 frame size - */ - self.progressView.frame = CGRectFlatMake(CGRectGetMinX(self.downloadRetryButton.frame), CGRectGetMinY(self.downloadRetryButton.frame) + self.downloadRetryButton.contentEdgeInsets.top, downloadRetryImage.size.width, downloadRetryImage.size.height); -} - -- (void)setToolBarBackgroundColor:(UIColor *)toolBarBackgroundColor { - _toolBarBackgroundColor = toolBarBackgroundColor; - self.topToolBarView.backgroundColor = self.toolBarBackgroundColor; -} - -- (void)setToolBarTintColor:(UIColor *)toolBarTintColor { - _toolBarTintColor = toolBarTintColor; - self.topToolBarView.tintColor = toolBarTintColor; - self.backButton.tintColor = toolBarTintColor; - self.checkboxButton.tintColor = toolBarTintColor; -} - -- (void)setDownloadStatus:(QMUIAssetDownloadStatus)downloadStatus { - _downloadStatus = downloadStatus; - switch (downloadStatus) { - case QMUIAssetDownloadStatusSucceed: - if (!_singleCheckMode) { - self.checkboxButton.hidden = NO; - } - self.progressView.hidden = YES; - self.downloadRetryButton.hidden = YES; - break; - - case QMUIAssetDownloadStatusDownloading: - self.checkboxButton.hidden = YES; - self.progressView.hidden = NO; - self.downloadRetryButton.hidden = YES; - break; - - case QMUIAssetDownloadStatusCanceled: - self.checkboxButton.hidden = NO; - self.progressView.hidden = YES; - self.downloadRetryButton.hidden = YES; - break; - - case QMUIAssetDownloadStatusFailed: - self.progressView.hidden = YES; - self.checkboxButton.hidden = YES; - self.downloadRetryButton.hidden = NO; - break; - - default: - break; - } -} - -- (void)updateImagePickerPreviewViewWithImagesAssetArray:(NSMutableArray *)imageAssetArray - selectedImageAssetArray:(NSMutableArray *)selectedImageAssetArray - currentImageIndex:(NSInteger)currentImageIndex - singleCheckMode:(BOOL)singleCheckMode { - self.imagesAssetArray = imageAssetArray; - self.selectedImageAssetArray = selectedImageAssetArray; - self.imagePreviewView.currentImageIndex = currentImageIndex; - _singleCheckMode = singleCheckMode; - if (singleCheckMode) { - self.checkboxButton.hidden = YES; - } -} - -#pragma mark - - -- (NSUInteger)numberOfImagesInImagePreviewView:(QMUIImagePreviewView *)imagePreviewView { - return [self.imagesAssetArray count]; -} - -- (QMUIImagePreviewMediaType)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView assetTypeAtIndex:(NSUInteger)index { - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:index]; - if (imageAsset.assetType == QMUIAssetTypeImage) { - return QMUIImagePreviewMediaTypeImage; - } else if (imageAsset.assetType == QMUIAssetTypeLivePhoto) { - return QMUIImagePreviewMediaTypeLivePhoto; - } else if (imageAsset.assetType == QMUIAssetTypeVideo) { - return QMUIImagePreviewMediaTypeVideo; - } else { - return QMUIImagePreviewMediaTypeOthers; - } -} - -- (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView renderZoomImageView:(QMUIZoomImageView *)zoomImageView atIndex:(NSUInteger)index { - [self requestImageForZoomImageView:zoomImageView withIndex:index]; -} - -- (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView willScrollHalfToIndex:(NSUInteger)index { - if (!_singleCheckMode) { - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:index]; - self.checkboxButton.selected = [self.selectedImageAssetArray containsObject:imageAsset]; - } -} - -#pragma mark - - -- (void)singleTouchInZoomingImageView:(QMUIZoomImageView *)zoomImageView location:(CGPoint)location { - self.topToolBarView.hidden = !self.topToolBarView.hidden; -} - - -- (void)zoomImageView:(QMUIZoomImageView *)imageView didHideVideoToolbar:(BOOL)didHide { - self.topToolBarView.hidden = didHide; -} - -#pragma mark - 按钮点击回调 - -- (void)handleCancelPreviewImage:(id)sender { - [self.navigationController popViewControllerAnimated:YES]; - if (self.delegate && [self.delegate respondsToSelector:@selector(imagePickerPreviewViewControllerDidCancel:)]) { - [self.delegate imagePickerPreviewViewControllerDidCancel:self]; - } -} - -- (void)handleCheckButtonClick:(id)sender { - QMUINavigationButton *button = sender; - [QMUIImagePickerHelper removeSpringAnimationOfImageCheckedWithCheckboxButton:button]; - - if (button.selected) { - if ([self.delegate respondsToSelector:@selector(imagePickerPreviewViewController:willUncheckImageAtIndex:)]) { - [self.delegate imagePickerPreviewViewController:self willUncheckImageAtIndex:self.imagePreviewView.currentImageIndex]; - } - - button.selected = NO; - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:self.imagePreviewView.currentImageIndex]; - [QMUIImagePickerHelper imageAssetArray:self.selectedImageAssetArray removeImageAsset:imageAsset]; - - if ([self.delegate respondsToSelector:@selector(imagePickerPreviewViewController:didUncheckImageAtIndex:)]) { - [self.delegate imagePickerPreviewViewController:self didUncheckImageAtIndex:self.imagePreviewView.currentImageIndex]; - } - } else { - if ([self.selectedImageAssetArray count] >= self.maximumSelectImageCount) { - if (!self.alertTitleWhenExceedMaxSelectImageCount) { - self.alertTitleWhenExceedMaxSelectImageCount = [NSString stringWithFormat:@"你最多只能选择%@张图片", @(self.maximumSelectImageCount)]; - } - if (!self.alertButtonTitleWhenExceedMaxSelectImageCount) { - self.alertButtonTitleWhenExceedMaxSelectImageCount = [NSString stringWithFormat:@"我知道了"]; - } - - QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:self.alertTitleWhenExceedMaxSelectImageCount message:nil preferredStyle:QMUIAlertControllerStyleAlert]; - [alertController addAction:[QMUIAlertAction actionWithTitle:self.alertButtonTitleWhenExceedMaxSelectImageCount style:QMUIAlertActionStyleCancel handler:nil]]; - [alertController showWithAnimated:YES]; - return; - } - - if (self.delegate && [self.delegate respondsToSelector:@selector(imagePickerPreviewViewController:willCheckImageAtIndex:)]) { - [self.delegate imagePickerPreviewViewController:self willCheckImageAtIndex:self.imagePreviewView.currentImageIndex]; - } - - button.selected = YES; - [QMUIImagePickerHelper springAnimationOfImageCheckedWithCheckboxButton:button]; - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:self.imagePreviewView.currentImageIndex]; - [self.selectedImageAssetArray addObject:imageAsset]; - - if (self.delegate && [self.delegate respondsToSelector:@selector(imagePickerPreviewViewController:didCheckImageAtIndex:)]) { - [self.delegate imagePickerPreviewViewController:self didCheckImageAtIndex:self.imagePreviewView.currentImageIndex]; - } - } -} - -- (void)handleDownloadRetryButtonClick:(id)sender { - [self requestImageForZoomImageView:nil withIndex:self.imagePreviewView.currentImageIndex]; -} - -#pragma mark - Request Image - -- (void)requestImageForZoomImageView:(QMUIZoomImageView *)zoomImageView withIndex:(NSInteger)index { - QMUIZoomImageView *imageView = zoomImageView ? : [self.imagePreviewView zoomImageViewAtIndex:index]; - // 如果是走 PhotoKit 的逻辑,那么这个 block 会被多次调用,并且第一次调用时返回的图片是一张小图, - // 拉取图片的过程中可能会多次返回结果,且图片尺寸越来越大,因此这里调整 contentMode 以防止图片大小跳动 - imageView.contentMode = UIViewContentModeScaleAspectFit; - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:index]; - - // 获取资源图片的预览图,这是一张适合当前设备屏幕大小的图片,最终展示时把图片交给组件控制最终展示出来的大小。 - // 系统相册本质上也是这么处理的,因此无论是系统相册,还是这个系列组件,由始至终都没有显示照片原图, - // 这也是系统相册能加载这么快的原因。 - // 另外这里采用异步请求获取图片,避免获取图片时 UI 卡顿 - PHAssetImageProgressHandler phProgressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { - imageAsset.downloadProgress = progress; - - dispatch_async(dispatch_get_main_queue(), ^{ - if (index == self.imagePreviewView.currentImageIndex) { - // 只有当前显示的预览图才会展示下载进度 - QMUILog(@"Download iCloud image in preview, current progress is: %f", progress); - - if (self.downloadStatus != QMUIAssetDownloadStatusDownloading) { - self.downloadStatus = QMUIAssetDownloadStatusDownloading; - // 重置 progressView 的显示的进度为 0 - [self.progressView setProgress:0 animated:NO]; - } - // 拉取资源的初期,会有一段时间没有进度,猜测是发出网络请求以及与 iCloud 建立连接的耗时,这时预先给个 0.02 的进度值,看上去好看些 - float targetProgress = fmaxf(0.02, progress); - if ( targetProgress < self.progressView.progress ) { - [self.progressView setProgress:targetProgress animated:NO]; - } else { - self.progressView.progress = fmaxf(0.02, progress); - } - if (error) { - QMUILog(@"Download iCloud image Failed, current progress is: %f", progress); - self.downloadStatus = QMUIAssetDownloadStatusFailed; - } - } - }); - }; - - if (imageAsset.assetType == QMUIAssetTypeLivePhoto) { - imageView.tag = -1; - imageAsset.requestID = [imageAsset requestLivePhotoWithCompletion:^void(PHLivePhoto *livePhoto, NSDictionary *info) { - // 这里可能因为 imageView 复用,导致前面的请求得到的结果显示到别的 imageView 上, - // 因此判断如果是新请求(无复用问题)或者是当前的请求才把获得的图片结果展示出来 - BOOL isNewRequest = (imageView.tag == -1 && imageAsset.requestID == 0); - BOOL isCurrentRequest = imageView.tag == imageAsset.requestID; - BOOL loadICloudImageFault = !livePhoto || info[PHImageErrorKey]; - if (!loadICloudImageFault && (isNewRequest || isCurrentRequest)) { - // 如果是走 PhotoKit 的逻辑,那么这个 block 会被多次调用,并且第一次调用时返回的图片是一张小图, - // 这时需要把图片放大到跟屏幕一样大,避免后面加载大图后图片的显示会有跳动 - dispatch_async(dispatch_get_main_queue(), ^{ - imageView.livePhoto = livePhoto; - }); - } - - BOOL downloadSucceed = (livePhoto && !info) || (![[info objectForKey:PHLivePhotoInfoCancelledKey] boolValue] && ![info objectForKey:PHLivePhotoInfoErrorKey] && ![[info objectForKey:PHLivePhotoInfoIsDegradedKey] boolValue]); - - if (downloadSucceed) { - // 资源资源已经在本地或下载成功 - [imageAsset updateDownloadStatusWithDownloadResult:YES]; - self.downloadStatus = QMUIAssetDownloadStatusSucceed; - - } else if ([info objectForKey:PHLivePhotoInfoErrorKey] ) { - // 下载错误 - [imageAsset updateDownloadStatusWithDownloadResult:NO]; - self.downloadStatus = QMUIAssetDownloadStatusFailed; - } - - } withProgressHandler:phProgressHandler]; - imageView.tag = imageAsset.requestID; - } else if (imageAsset.assetType == QMUIAssetTypeVideo) { - imageView.tag = -1; - imageAsset.requestID = [imageAsset requestPlayerItemWithCompletion:^(AVPlayerItem *playerItem, NSDictionary *info) { - // 这里可能因为 imageView 复用,导致前面的请求得到的结果显示到别的 imageView 上, - // 因此判断如果是新请求(无复用问题)或者是当前的请求才把获得的图片结果展示出来 - BOOL isNewRequest = (imageView.tag == -1 && imageAsset.requestID == 0); - BOOL isCurrentRequest = imageView.tag == imageAsset.requestID; - BOOL loadICloudImageFault = !playerItem || info[PHImageErrorKey]; - if (!loadICloudImageFault && (isNewRequest || isCurrentRequest)) { - dispatch_async(dispatch_get_main_queue(), ^{ - imageView.videoPlayerItem = playerItem; - }); - } - } withProgressHandler:phProgressHandler]; - imageView.tag = imageAsset.requestID; - } else { - imageView.tag = -1; - imageAsset.requestID = [imageAsset requestPreviewImageWithCompletion:^void(UIImage *result, NSDictionary *info) { - // 这里可能因为 imageView 复用,导致前面的请求得到的结果显示到别的 imageView 上, - // 因此判断如果是新请求(无复用问题)或者是当前的请求才把获得的图片结果展示出来 - BOOL isNewRequest = (imageView.tag == -1 && imageAsset.requestID == 0); - BOOL isCurrentRequest = imageView.tag == imageAsset.requestID; - BOOL loadICloudImageFault = !result || info[PHImageErrorKey]; - if (!loadICloudImageFault && (isNewRequest || isCurrentRequest)) { - dispatch_async(dispatch_get_main_queue(), ^{ - imageView.image = result; - }); - } - - BOOL downloadSucceed = (result && !info) || (![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue]); - - if (downloadSucceed) { - // 资源资源已经在本地或下载成功 - [imageAsset updateDownloadStatusWithDownloadResult:YES]; - self.downloadStatus = QMUIAssetDownloadStatusSucceed; - - } else if ([info objectForKey:PHImageErrorKey] ) { - // 下载错误 - [imageAsset updateDownloadStatusWithDownloadResult:NO]; - self.downloadStatus = QMUIAssetDownloadStatusFailed; - } - - } withProgressHandler:phProgressHandler]; - imageView.tag = imageAsset.requestID; - } -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h b/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h deleted file mode 100644 index ae92f14f..00000000 --- a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h +++ /dev/null @@ -1,132 +0,0 @@ -// -// QMUIImagePickerViewController.h -// qmui -// -// Created by Kayo Lee on 15/5/2. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUICommonViewController.h" -#import -#import "QMUIImagePickerPreviewViewController.h" -#import "QMUIAsset.h" -#import "QMUIAssetsGroup.h" - -@class QMUIImagePickerViewController; -@class QMUIButton; - -@protocol QMUIImagePickerViewControllerDelegate - -@optional - -/** - * 创建一个 ImagePickerPreviewViewController 用于预览图片 - */ -- (QMUIImagePickerPreviewViewController *)imagePickerPreviewViewControllerForImagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController; - -/** - * 控制照片的排序,若不实现,默认为 QMUIAlbumSortTypePositive - * @note 注意返回值会决定第一次进来相片列表时列表默认的滚动位置,如果为 QMUIAlbumSortTypePositive,则列表默认滚动到底部,如果为 QMUIAlbumSortTypeReverse,则列表默认滚动到顶部。 - */ -- (QMUIAlbumSortType)albumSortTypeForImagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController; - -/** - * 多选模式下选择图片完毕后被调用(点击 sendButton 后被调用),单选模式下没有底部发送按钮,所以也不会走到这个delegate - * - * @param imagePickerViewController 对应的 QMUIImagePickerViewController - * @param imagesAssetArray 包含被选择的图片的 QMUIAsset 对象的数组。 - */ -- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController didFinishPickingImageWithImagesAssetArray:(NSMutableArray *)imagesAssetArray; - -/** - * cell 被点击时调用(先调用这个接口,然后才去走预览大图的逻辑),注意这并非指选中 checkbox 事件 - * - * @param imagePickerViewController 对应的 QMUIImagePickerViewController - * @param imageAsset 被选中的图片的 QMUIAsset 对象 - * @param imagePickerPreviewViewController 选中图片后进行图片预览的 viewController - */ -- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController didSelectImageWithImagesAsset:(QMUIAsset *)imageAsset afterImagePickerPreviewViewControllerUpdate:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController; - -/// 即将选中 checkbox 时调用 -- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController willCheckImageAtIndex:(NSInteger)index; - -/// 选中了 checkbox 之后调用 -- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController didCheckImageAtIndex:(NSInteger)index; - -/// 即将取消选中 checkbox 时调用 -- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController willUncheckImageAtIndex:(NSInteger)index; - -/// 取消了 checkbox 选中之后调用 -- (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController didUncheckImageAtIndex:(NSInteger)index; - -/** - * 取消选择图片后被调用 - */ -- (void)imagePickerViewControllerDidCancel:(QMUIImagePickerViewController *)imagePickerViewController; - -/** - * 即将需要显示 Loading 时调用 - * - * @see shouldShowDefaultLoadingView - */ -- (void)imagePickerViewControllerWillStartLoad:(QMUIImagePickerViewController *)imagePickerViewController; - -/** - * 即将需要隐藏 Loading 时调用 - * - * @see shouldShowDefaultLoadingView - */ -- (void)imagePickerViewControllerWillFinishLoad:(QMUIImagePickerViewController *)imagePickerViewController; - -@end - - -@interface QMUIImagePickerViewController : QMUICommonViewController - -/** - * 图片的最小尺寸,布局时如果有剩余空间,会将空间分配给图片大小,所以最终显示出来的大小不一定等于minimumImageWidth。默认是75。 - */ -@property(nonatomic, assign) CGFloat minimumImageWidth UI_APPEARANCE_SELECTOR; - -@property(nonatomic, weak) idimagePickerViewControllerDelegate; - -@property(nonatomic, strong, readonly) UICollectionViewFlowLayout *collectionViewLayout; -@property(nonatomic, strong, readonly) UICollectionView *collectionView; -@property(nonatomic, strong, readonly) UIView *operationToolBarView; -@property(nonatomic, strong, readonly) QMUIButton *previewButton; -@property(nonatomic, strong, readonly) QMUIButton *sendButton; -@property(nonatomic, strong, readonly) UILabel *imageCountLabel; - -/** - * 由于组件需要通过本地图片的 QMUIAsset 对象读取图片的详细信息,因此这里的需要传入的是包含一个或多个 QMUIAsset 对象的数组,传入后会赋值到 imagesAssetArray ,并自动刷新 UI 展示 - */ -- (void)refreshWithImagesArray:(NSMutableArray *)imagesArray; -/** - * 也可以直接传入 QMUIAssetsGroup,然后读取其中的 QMUIAsset 并储存到 imagesAssetArray 中,传入后会赋值到 QMUIAssetsGroup,并自动刷新 UI 展示 - */ -- (void)refreshWithAssetsGroup:(QMUIAssetsGroup *)assetsGroup; - -@property(nonatomic, strong, readonly) NSMutableArray *imagesAssetArray; -@property(nonatomic, strong, readonly) QMUIAssetsGroup *assetsGroup; -@property(nonatomic, strong) NSMutableArray *selectedImageAssetArray; // 当前被选择的图片对应的 QMUIAsset 对象数组 - -@property(nonatomic, assign) BOOL allowsMultipleSelection; // 是否允许图片多选,默认为 YES。如果为 NO,则不显示 checkbox 和底部工具栏。 -@property(nonatomic, assign) NSUInteger maximumSelectImageCount; // 最多可以选择的图片数,默认为无符号整形数的最大值,相当于没有限制 -@property(nonatomic, assign) NSUInteger minimumSelectImageCount; // 最少需要选择的图片数,默认为 0 -@property(nonatomic, copy) NSString *alertTitleWhenExceedMaxSelectImageCount; // 选择图片超出最大图片限制时 alertView 的标题 -@property(nonatomic, copy) NSString *alertButtonTitleWhenExceedMaxSelectImageCount; // 选择图片超出最大图片限制时 alertView 底部按钮的标题 - -/** - * 加载相册列表时会出现 loading,若需要自定义 loading 的形式,可将该属性置为 NO,默认为 YES。 - * @see imagePickerViewControllerWillStartLoad: & imagePickerViewControllerWillFinishLoad: - */ -@property(nonatomic, assign) BOOL shouldShowDefaultLoadingView; - -@end - - -@interface QMUIImagePickerViewController (UIAppearance) - -+ (instancetype)appearance; - -@end diff --git a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m b/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m deleted file mode 100644 index 73db8780..00000000 --- a/QMUI/QMUIKit/UIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m +++ /dev/null @@ -1,575 +0,0 @@ -// -// QMUIImagePickerViewController.m -// qmui -// -// Created by Kayo Lee on 15/5/2. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIImagePickerViewController.h" -#import "QMUICore.h" -#import "QMUIImagePickerCollectionViewCell.h" -#import "QMUIButton.h" -#import "QMUIPieProgressView.h" -#import "QMUIAssetsManager.h" -#import "QMUIAlertController.h" -#import "QMUIImagePickerHelper.h" -#import "QMUIImagePickerHelper.h" -#import "UICollectionView+QMUI.h" -#import "UIScrollView+QMUI.h" -#import "CALayer+QMUI.h" -#import "UIView+QMUI.h" -#import -#import -#import "NSString+QMUI.h" -#import "QMUIEmptyView.h" - -// 底部工具栏 -#define OperationToolBarViewHeight 44 -#define OperationToolBarViewPaddingHorizontal 12 -#define ImageCountLabelSize CGSizeMake(18, 18) - -// CollectionView -#define CollectionViewInsetHorizontal PreferredVarForDevices((PixelOne * 2), 1, 2, 2) -#define CollectionViewInset UIEdgeInsetsMake(CollectionViewInsetHorizontal, CollectionViewInsetHorizontal, CollectionViewInsetHorizontal, CollectionViewInsetHorizontal) -#define CollectionViewCellMargin CollectionViewInsetHorizontal - -static NSString * const kVideoCellIdentifier = @"video"; -static NSString * const kImageOrUnknownCellIdentifier = @"imageorunknown"; - - -#pragma mark - QMUIImagePickerViewController (UIAppearance) - -@implementation QMUIImagePickerViewController (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; // +initialize 时就先设置好默认样式 - }); -} - -static QMUIImagePickerViewController *imagePickerViewControllerAppearance; -+ (instancetype)appearance { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - if (!imagePickerViewControllerAppearance) { - imagePickerViewControllerAppearance = [[QMUIImagePickerViewController alloc] init]; - imagePickerViewControllerAppearance.minimumImageWidth = 75; - } - }); - return imagePickerViewControllerAppearance; -} - -@end - -#pragma mark - QMUIImagePickerViewController - -@interface QMUIImagePickerViewController () - -@property(nonatomic, strong, readwrite) UICollectionViewFlowLayout *collectionViewLayout; -@property(nonatomic, strong, readwrite) UICollectionView *collectionView; -@property(nonatomic, strong, readwrite) UIView *operationToolBarView; -@property(nonatomic, strong, readwrite) QMUIButton *previewButton; -@property(nonatomic, strong, readwrite) QMUIButton *sendButton; -@property(nonatomic, strong, readwrite) UILabel *imageCountLabel; - -@property(nonatomic, strong, readwrite) NSMutableArray *imagesAssetArray; -@property(nonatomic, strong, readwrite) QMUIAssetsGroup *assetsGroup; - -@property(nonatomic, strong) QMUIImagePickerPreviewViewController *imagePickerPreviewViewController; -@property(nonatomic, assign) BOOL hasScrollToInitialPosition; -@property(nonatomic, assign) BOOL canScrollToInitialPosition;// 要等数据加载完才允许滚动 -@end - -@implementation QMUIImagePickerViewController - -- (void)didInitialized { - [super didInitialized]; - - if (imagePickerViewControllerAppearance) { - // 避免 imagePickerViewControllerAppearance init 时走到这里来,导致死循环 - self.minimumImageWidth = [QMUIImagePickerViewController appearance].minimumImageWidth; - } - - _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; -} - -- (void)initSubviews { - [super initSubviews]; - - self.collectionViewLayout = [[UICollectionViewFlowLayout alloc] init]; - self.collectionViewLayout.sectionInset = CollectionViewInset; - self.collectionViewLayout.minimumLineSpacing = CollectionViewCellMargin; - self.collectionViewLayout.minimumInteritemSpacing = CollectionViewCellMargin; - - self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.collectionViewLayout]; - self.collectionView.delegate = self; - self.collectionView.dataSource = self; - self.collectionView.delaysContentTouches = NO; - 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]; - [self.view addSubview:self.collectionView]; - - // 只有允许多选时,才显示底部工具 - if (self.allowsMultipleSelection) { - self.operationToolBarView = [[UIView alloc] init]; - self.operationToolBarView.backgroundColor = UIColorWhite; - self.operationToolBarView.qmui_borderPosition = QMUIBorderViewPositionTop; - [self.view addSubview:self.operationToolBarView]; - - self.sendButton = [[QMUIButton alloc] init]; - 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 sizeToFit]; - self.sendButton.enabled = NO; - [self.sendButton addTarget:self action:@selector(handleSendButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - [self.operationToolBarView addSubview:self.sendButton]; - - self.previewButton = [[QMUIButton alloc] init]; - 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 sizeToFit]; - self.previewButton.enabled = NO; - [self.previewButton addTarget:self action:@selector(handlePreviewButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - [self.operationToolBarView addSubview:self.previewButton]; - - self.imageCountLabel = [[UILabel alloc] init]; - 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.layer.cornerRadius = ImageCountLabelSize.width / 2; - self.imageCountLabel.hidden = YES; - [self.operationToolBarView addSubview:self.imageCountLabel]; - } - - _selectedImageAssetArray = [[NSMutableArray alloc] init]; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - self.view.backgroundColor = UIColorWhite; -} - -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; - self.navigationItem.rightBarButtonItem = [QMUINavigationButton barButtonItemWithType:QMUINavigationButtonTypeNormal title:@"取消" position:QMUINavigationButtonPositionRight target:self action:@selector(handleCancelPickerImage:)]; -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - // 由于被选中的图片 selectedImageAssetArray 是 property,所以可以由外部改变, - // 因此 viewWillAppear 时检查一下图片被选中的情况,并刷新 collectionView - if (self.allowsMultipleSelection) { - // 只有允许多选,即底部工具栏显示时,需要重新设置底部工具栏的元素 - NSInteger selectedImageCount = [_selectedImageAssetArray count]; - if (selectedImageCount > 0) { - // 如果有图片被选择,则预览按钮和发送按钮可点击,并刷新当前被选中的图片数量 - self.previewButton.enabled = YES; - self.sendButton.enabled = YES; - self.imageCountLabel.text = [NSString stringWithFormat:@"%@", @(selectedImageCount)]; - self.imageCountLabel.hidden = NO; - } else { - // 如果没有任何图片被选择,则预览和发送按钮不可点击,并且隐藏显示图片数量的 Label - self.previewButton.enabled = NO; - self.sendButton.enabled = NO; - self.imageCountLabel.hidden = YES; - } - } - [self.collectionView reloadData]; -} - -- (void)showEmptyView { - [super showEmptyView]; - self.emptyView.backgroundColor = self.view.backgroundColor;// 为了盖住背后的 collectionView,这里加个背景色(不盖住的话会看到 collectionView 先滚到列表顶部然后跳到列表底部) -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - - if (!CGSizeEqualToSize(self.collectionView.frame.size, self.view.bounds.size)) { - self.collectionView.frame = self.view.bounds; - } - - CGFloat operationToolBarViewHeight = 0; - if (self.allowsMultipleSelection) { - self.operationToolBarView.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - OperationToolBarViewHeight, CGRectGetWidth(self.view.bounds), OperationToolBarViewHeight); - self.previewButton.frame = CGRectSetXY(self.previewButton.frame, OperationToolBarViewPaddingHorizontal, CGFloatGetCenter(CGRectGetHeight(self.operationToolBarView.frame), CGRectGetHeight(self.previewButton.frame))); - self.sendButton.frame = CGRectMake(CGRectGetWidth(self.operationToolBarView.frame) - OperationToolBarViewPaddingHorizontal - CGRectGetWidth(self.sendButton.frame), CGFloatGetCenter(CGRectGetHeight(self.operationToolBarView.frame), CGRectGetHeight(self.sendButton.frame)), CGRectGetWidth(self.sendButton.frame), CGRectGetHeight(self.sendButton.frame)); - self.imageCountLabel.frame = CGRectMake(CGRectGetMinX(self.sendButton.frame) - ImageCountLabelSize.width - 5, CGRectGetMinY(self.sendButton.frame) + CGFloatGetCenter(CGRectGetHeight(self.sendButton.frame), ImageCountLabelSize.height), ImageCountLabelSize.width, ImageCountLabelSize.height); - operationToolBarViewHeight = CGRectGetHeight(self.operationToolBarView.frame); - } - - if (self.collectionView.contentInset.bottom != operationToolBarViewHeight) { - self.collectionView.contentInset = UIEdgeInsetsSetBottom(self.collectionView.contentInset, operationToolBarViewHeight); - self.collectionView.scrollIndicatorInsets = self.collectionView.contentInset; - } -} - -- (void)refreshWithImagesArray:(NSMutableArray *)imagesArray { - self.imagesAssetArray = imagesArray; - [self.collectionView reloadData]; -} - -- (void)refreshWithAssetsGroup:(QMUIAssetsGroup *)assetsGroup { - self.assetsGroup = assetsGroup; - if (!self.imagesAssetArray) { - self.imagesAssetArray = [[NSMutableArray alloc] init]; - } else { - [self.imagesAssetArray removeAllObjects]; - } - // 通过 QMUIAssetsGroup 获取该相册所有的图片 QMUIAsset,并且储存到数组中 - QMUIAlbumSortType albumSortType = QMUIAlbumSortTypePositive; - // 从 delegate 中获取相册内容的排序方式,如果没有实现这个 delegate,则使用 QMUIAlbumSortType 的默认值,即最新的内容排在最后面 - if (self.imagePickerViewControllerDelegate && [self.imagePickerViewControllerDelegate respondsToSelector:@selector(albumSortTypeForImagePickerViewController:)]) { - albumSortType = [self.imagePickerViewControllerDelegate albumSortTypeForImagePickerViewController:self]; - } - - // 遍历相册内的资源较为耗时,交给子线程去处理,因此这里需要显示 Loading - if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewControllerWillStartLoad:)]) { - [self.imagePickerViewControllerDelegate imagePickerViewControllerWillStartLoad:self]; - } - if (self.shouldShowDefaultLoadingView) { - [self showEmptyViewWithLoading]; - } - dispatch_async(dispatch_get_global_queue(0, 0), ^{ - [assetsGroup enumerateAssetsWithOptions:albumSortType usingBlock:^(QMUIAsset *resultAsset) { - dispatch_async(dispatch_get_main_queue(), ^{ - // 这里需要对 UI 进行操作,因此放回主线程处理 - if (resultAsset) { - [self.imagesAssetArray addObject:resultAsset]; - } else { // result 为 nil,即遍历相片或视频完毕 - [self.collectionView reloadData]; - [self.collectionView performBatchUpdates:NULL - completion:^(BOOL finished) { - [self scrollToInitialPositionIfNeeded]; - - if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewControllerWillFinishLoad:)]) { - [self.imagePickerViewControllerDelegate imagePickerViewControllerWillFinishLoad:self]; - } - if (self.shouldShowDefaultLoadingView) { - [self hideEmptyView]; - } - }]; - } - }); - }]; - }); -} - -- (void)initPreviewViewControllerIfNeeded { - if (!self.imagePickerPreviewViewController) { - self.imagePickerPreviewViewController = [self.imagePickerViewControllerDelegate imagePickerPreviewViewControllerForImagePickerViewController:self]; - self.imagePickerPreviewViewController.maximumSelectImageCount = self.maximumSelectImageCount; - self.imagePickerPreviewViewController.minimumSelectImageCount = self.minimumSelectImageCount; - } -} - -- (CGSize)referenceImageSize { - CGFloat collectionViewWidth = CGRectGetWidth(self.collectionView.bounds); - CGFloat collectionViewContentSpacing = collectionViewWidth - UIEdgeInsetsGetHorizontalValue(self.collectionView.contentInset); - NSInteger columnCount = floor(collectionViewContentSpacing / self.minimumImageWidth); - CGFloat referenceImageWidth = self.minimumImageWidth; - BOOL isSpacingEnoughWhenDisplayInMinImageSize = UIEdgeInsetsGetHorizontalValue(self.collectionViewLayout.sectionInset) + (self.minimumImageWidth + self.collectionViewLayout.minimumInteritemSpacing) * columnCount - self.collectionViewLayout.minimumInteritemSpacing <= collectionViewContentSpacing; - if (!isSpacingEnoughWhenDisplayInMinImageSize) { - // 算上图片之间的间隙后发现其实还是放不下啦,所以得把列数减少,然后放大图片以撑满剩余空间 - columnCount -= 1; - } - referenceImageWidth = (collectionViewContentSpacing - UIEdgeInsetsGetHorizontalValue(self.collectionViewLayout.sectionInset) - self.collectionViewLayout.minimumInteritemSpacing * (columnCount - 1)) / columnCount; - return CGSizeMake(referenceImageWidth, referenceImageWidth); -} - -- (void)setMinimumImageWidth:(CGFloat)minimumImageWidth { - _minimumImageWidth = minimumImageWidth; - [self referenceImageSize]; - [self.collectionView.collectionViewLayout invalidateLayout]; -} - -- (void)scrollToInitialPositionIfNeeded { - BOOL hasDataLoaded = [self.collectionView numberOfItemsInSection:0] > 0; - if (self.collectionView.window && hasDataLoaded && !self.hasScrollToInitialPosition) { - if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(albumSortTypeForImagePickerViewController:)] && [self.imagePickerViewControllerDelegate albumSortTypeForImagePickerViewController:self] == QMUIAlbumSortTypeReverse) { - [self.collectionView qmui_scrollToTop]; - } else { - [self.collectionView qmui_scrollToBottom]; - } - - self.hasScrollToInitialPosition = YES; - } -} - -- (void)willPopInNavigationControllerWithAnimated:(BOOL)animated { - self.hasScrollToInitialPosition = NO; -} - -#pragma mark - - -- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { - return 1; -} - -- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - return [self.imagesAssetArray count]; -} - -- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { - return [self referenceImageSize]; -} - -- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - NSString *identifier = kImageOrUnknownCellIdentifier; - // 获取需要显示的资源 - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item]; - if (imageAsset.assetType == QMUIAssetTypeVideo) { - identifier = kVideoCellIdentifier; - } - QMUIImagePickerCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath]; - - // 异步请求资源对应的缩略图(因系统接口限制,iOS 8.0 以下为实际上同步请求) - [imageAsset requestThumbnailImageWithSize:[self referenceImageSize] completion:^(UIImage *result, NSDictionary *info) { - if (!info || [[info objectForKey:PHImageResultIsDegradedKey] boolValue]) { - // 模糊,此时为同步调用 - cell.contentImageView.image = result; - } else if ([collectionView qmui_itemVisibleAtIndexPath:indexPath]) { - // 清晰,此时为异步调用 - QMUIImagePickerCollectionViewCell *anotherCell = (QMUIImagePickerCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath]; - anotherCell.contentImageView.image = result; - } - }]; - - if (imageAsset.assetType == QMUIAssetTypeVideo) { - cell.videoDurationLabel.text = [NSString qmui_timeStringWithMinsAndSecsFromSecs:imageAsset.duration]; - } - - [cell.checkboxButton addTarget:self action:@selector(handleCheckBoxButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - [cell.progressView addTarget:self action:@selector(handleProgressViewClick:) forControlEvents:UIControlEventTouchUpInside]; - [cell.downloadRetryButton addTarget:self action:@selector(handleDownloadRetryButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - - cell.editing = self.allowsMultipleSelection; - if (cell.editing) { - // 如果该图片的 QMUIAsset 被包含在已选择图片的数组中,则控制该图片被选中 - cell.checked = [QMUIImagePickerHelper imageAssetArray:_selectedImageAssetArray containsImageAsset:imageAsset]; - } - return cell; -} - -- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item]; - if (self.imagePickerViewControllerDelegate && [self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:didSelectImageWithImagesAsset:afterImagePickerPreviewViewControllerUpdate:)]) { - [self.imagePickerViewControllerDelegate imagePickerViewController:self didSelectImageWithImagesAsset:imageAsset afterImagePickerPreviewViewControllerUpdate:self.imagePickerPreviewViewController]; - } - - if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerPreviewViewControllerForImagePickerViewController:)]) { - [self initPreviewViewControllerIfNeeded]; - if (!self.allowsMultipleSelection) { - // 单选的情况下 - [self.imagePickerPreviewViewController updateImagePickerPreviewViewWithImagesAssetArray:@[imageAsset] - selectedImageAssetArray:nil - currentImageIndex:0 - singleCheckMode:YES]; - } else { - // cell 处于编辑状态,即图片允许多选 - [self.imagePickerPreviewViewController updateImagePickerPreviewViewWithImagesAssetArray:self.imagesAssetArray - selectedImageAssetArray:_selectedImageAssetArray - currentImageIndex:indexPath.item - singleCheckMode:NO]; - } - [self.navigationController pushViewController:self.imagePickerPreviewViewController animated:YES]; - } -} - -#pragma mark - 按钮点击回调 - -- (void)handleSendButtonClick:(id)sender { - if (self.imagePickerViewControllerDelegate && [self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:didFinishPickingImageWithImagesAssetArray:)]) { - [self.imagePickerViewControllerDelegate imagePickerViewController:self didFinishPickingImageWithImagesAssetArray:_selectedImageAssetArray]; - } - [self.navigationController dismissViewControllerAnimated:YES completion:NULL]; -} - -- (void)handlePreviewButtonClick:(id)sender { - [self initPreviewViewControllerIfNeeded]; - // 手工更新图片预览界面 - [self.imagePickerPreviewViewController updateImagePickerPreviewViewWithImagesAssetArray:[_selectedImageAssetArray copy] - selectedImageAssetArray:_selectedImageAssetArray - currentImageIndex:0 - singleCheckMode:NO]; - [self.navigationController pushViewController:self.imagePickerPreviewViewController animated:YES]; -} - -- (void)handleCancelPickerImage:(id)sender { - [self.navigationController dismissViewControllerAnimated:YES completion:^() { - if (self.imagePickerViewControllerDelegate && [self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewControllerDidCancel:)]) { - [self.imagePickerViewControllerDelegate imagePickerViewControllerDidCancel:self]; - } - }]; -} - -- (void)handleCheckBoxButtonClick:(id)sender { - UIButton *checkBoxButton = sender; - NSIndexPath *indexPath = [self.collectionView qmui_indexPathForItemAtView:checkBoxButton]; - - QMUIImagePickerCollectionViewCell *cell = (QMUIImagePickerCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item]; - if (cell.checked) { - // 移除选中状态 - if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:willUncheckImageAtIndex:)]) { - [self.imagePickerViewControllerDelegate imagePickerViewController:self willUncheckImageAtIndex:indexPath.item]; - } - - cell.checked = NO; - [QMUIImagePickerHelper imageAssetArray:_selectedImageAssetArray removeImageAsset:imageAsset]; - - if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:didUncheckImageAtIndex:)]) { - [self.imagePickerViewControllerDelegate imagePickerViewController:self didUncheckImageAtIndex:indexPath.item]; - } - - // 根据选择图片数控制预览和发送按钮的 enable,以及修改已选中的图片数 - [self updateImageCountAndCheckLimited]; - } else { - // 选中该资源 - // 发出请求获取大图,如果图片在 iCloud,则会发出网络请求下载图片。这里同时保存请求 id,供取消请求使用 - [self requestImageWithIndexPath:indexPath]; - } -} - -- (void)handleProgressViewClick:(id)sender { - UIControl *progressView = sender; - NSIndexPath *indexPath = [self.collectionView qmui_indexPathForItemAtView:progressView]; - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item]; - if (imageAsset.downloadStatus == QMUIAssetDownloadStatusDownloading) { - // 下载过程中点击,取消下载,理论上能点击 progressView 就肯定是下载中,这里只是做个保护 - QMUIImagePickerCollectionViewCell *cell = (QMUIImagePickerCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; - [[QMUIAssetsManager sharedInstance].phCachingImageManager cancelImageRequest:(int32_t)imageAsset.requestID]; - QMUILog(@"Cancel download asset image with request ID %@", [NSNumber numberWithInteger:imageAsset.requestID]); - cell.downloadStatus = QMUIAssetDownloadStatusCanceled; - [imageAsset updateDownloadStatusWithDownloadResult:NO]; - } -} - -- (void)handleDownloadRetryButtonClick:(id)sender { - UIButton *downloadRetryButton = sender; - NSIndexPath *indexPath = [self.collectionView qmui_indexPathForItemAtView:downloadRetryButton]; - [self requestImageWithIndexPath:indexPath]; -} - -- (void)updateImageCountAndCheckLimited { - NSInteger selectedImageCount = [_selectedImageAssetArray count]; - if (selectedImageCount > 0 && selectedImageCount >= _minimumSelectImageCount) { - self.previewButton.enabled = YES; - self.sendButton.enabled = YES; - self.imageCountLabel.text = [NSString stringWithFormat:@"%@", @(selectedImageCount)]; - self.imageCountLabel.hidden = NO; - [QMUIImagePickerHelper springAnimationOfImageSelectedCountChangeWithCountLabel:self.imageCountLabel]; - } else { - self.previewButton.enabled = NO; - self.sendButton.enabled = NO; - self.imageCountLabel.hidden = YES; - } -} - -#pragma mark - Request Image - -- (void)requestImageWithIndexPath:(NSIndexPath *)indexPath { - // 发出请求获取大图,如果图片在 iCloud,则会发出网络请求下载图片。这里同时保存请求 id,供取消请求使用 - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:indexPath.item]; - QMUIImagePickerCollectionViewCell *cell = (QMUIImagePickerCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; - imageAsset.requestID = [imageAsset requestPreviewImageWithCompletion:^(UIImage *result, NSDictionary *info) { - - BOOL downloadSucceed = (result && !info) || (![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue]); - - if (downloadSucceed) { - // 资源资源已经在本地或下载成功 - [imageAsset updateDownloadStatusWithDownloadResult:YES]; - cell.downloadStatus = QMUIAssetDownloadStatusSucceed; - - if ([_selectedImageAssetArray count] >= _maximumSelectImageCount) { - if (!_alertTitleWhenExceedMaxSelectImageCount) { - _alertTitleWhenExceedMaxSelectImageCount = [NSString stringWithFormat:@"你最多只能选择%@张图片", @(_maximumSelectImageCount)]; - } - if (!_alertButtonTitleWhenExceedMaxSelectImageCount) { - _alertButtonTitleWhenExceedMaxSelectImageCount = [NSString stringWithFormat:@"我知道了"]; - } - - QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:_alertTitleWhenExceedMaxSelectImageCount message:nil preferredStyle:QMUIAlertControllerStyleAlert]; - [alertController addAction:[QMUIAlertAction actionWithTitle:_alertButtonTitleWhenExceedMaxSelectImageCount style:QMUIAlertActionStyleCancel handler:nil]]; - [alertController showWithAnimated:YES]; - return; - } - - if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:willCheckImageAtIndex:)]) { - [self.imagePickerViewControllerDelegate imagePickerViewController:self willCheckImageAtIndex:indexPath.item]; - } - - cell.checked = YES; - [_selectedImageAssetArray addObject:imageAsset]; - - if ([self.imagePickerViewControllerDelegate respondsToSelector:@selector(imagePickerViewController:didCheckImageAtIndex:)]) { - [self.imagePickerViewControllerDelegate imagePickerViewController:self didCheckImageAtIndex:indexPath.item]; - } - - // 根据选择图片数控制预览和发送按钮的 enable,以及修改已选中的图片数 - [self updateImageCountAndCheckLimited]; - } else if ([info objectForKey:PHImageErrorKey] ) { - // 下载错误 - [imageAsset updateDownloadStatusWithDownloadResult:NO]; - cell.downloadStatus = QMUIAssetDownloadStatusFailed; - } - - } withProgressHandler:^(double progress, NSError *error, BOOL *stop, NSDictionary *info) { - imageAsset.downloadProgress = progress; - - if ([self.collectionView qmui_itemVisibleAtIndexPath:indexPath]) { - /** - * withProgressHandler 不在主线程执行,若用户在该 block 中操作 UI 时会产生一些问题, - * 为了避免这种情况,这里该 block 主动放到主线程执行。 - */ - dispatch_async(dispatch_get_main_queue(), ^{ - QMUILog(@"Download iCloud image, current progress is : %f", progress); - - if (cell.downloadStatus != QMUIAssetDownloadStatusDownloading) { - cell.downloadStatus = QMUIAssetDownloadStatusDownloading; - // 重置 progressView 的显示的进度为 0 - [cell.progressView setProgress:0 animated:NO]; - // 预先设置预览界面的下载状态 - self.imagePickerPreviewViewController.downloadStatus = QMUIAssetDownloadStatusDownloading; - } - // 拉取资源的初期,会有一段时间没有进度,猜测是发出网络请求以及与 iCloud 建立连接的耗时,这时预先给个 0.02 的进度值,看上去好看些 - float targetProgress = MAX(0.02, progress); - if ( targetProgress < cell.progressView.progress ) { - [cell.progressView setProgress:targetProgress animated:NO]; - } else { - cell.progressView.progress = MAX(0.02, progress); - } - if (error) { - QMUILog(@"Download iCloud image Failed, current progress is: %f", progress); - cell.downloadStatus = QMUIAssetDownloadStatusFailed; - } - }); - } - }]; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIDialogViewController.h b/QMUI/QMUIKit/UIComponents/QMUIDialogViewController.h deleted file mode 100644 index 12ed881c..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIDialogViewController.h +++ /dev/null @@ -1,115 +0,0 @@ -// -// QMUIDialogViewController.h -// WeRead -// -// Created by MoLice on 16/7/8. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import -#import "QMUICommonViewController.h" -#import "QMUIModalPresentationViewController.h" -#import "QMUITableView.h" - -@class QMUIButton; -@class QMUITextField; -@class QMUITableViewCell; - -/** - * 弹窗组件基类,自带`headerView`、`contentView`、`footerView`,并通过`addCancelButtonWithText:block:`、`addSubmitButtonWithText:block:`方法来添加取消、确定按钮。 - * 建议将一个自定义的UIView设置给`contentView`属性,此时弹窗将会自动帮你计算大小并布局。大小取决于你的contentView的sizeThatFits:返回值。 - * 弹窗继承自`QMUICommonViewController`,因此可直接使用self.titleView的功能来实现双行标题,具体请查看`QMUINavigationTitleView`。 - * `QMUIDialogViewController`支持以类似`UIAppearance`的方式来统一设置全局的dialog样式,例如`[QMUIDialogViewController appearance].headerViewHeight = 48;`。 - * - * @see QMUIDialogSelectionViewController - * @see QMUIDialogTextFieldViewController - */ -@interface QMUIDialogViewController : QMUICommonViewController - -@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) UIEdgeInsets contentViewMargins UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *titleTintColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIFont *titleLabelFont UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *titleLabelTextColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIFont *subTitleLabelFont UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *subTitleLabelTextColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *headerFooterSeparatorColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) CGFloat headerViewHeight UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *headerViewBackgroundColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) CGFloat footerViewHeight UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *footerViewBackgroundColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) NSDictionary *buttonTitleAttributes UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *buttonHighlightedBackgroundColor UI_APPEARANCE_SELECTOR; - -@property(nonatomic, strong, readonly) UIView *headerView; -@property(nonatomic, strong, readonly) CALayer *headerViewSeparatorLayer; - -/// dialog的主体内容部分,默认是一个空的白色UIView,建议设置为自己的UIView -/// dialog会通过询问contentView的sizeThatFits得到当前内容的大小 -@property(nonatomic, strong) UIView *contentView; - -@property(nonatomic, strong, readonly) UIView *footerView; -@property(nonatomic, strong, readonly) CALayer *footerViewSeparatorLayer; - -@property(nonatomic, strong, readonly) QMUIButton *cancelButton; -@property(nonatomic, strong, readonly) QMUIButton *submitButton; -@property(nonatomic, strong, readonly) CALayer *buttonSeparatorLayer; - -- (void)addCancelButtonWithText:(NSString *)buttonText block:(void (^)(QMUIDialogViewController *dialogViewController))block; -- (void)addSubmitButtonWithText:(NSString *)buttonText block:(void (^)(QMUIDialogViewController *dialogViewController))block; -- (void)show; -- (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; -- (void)hide; -- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; - -@end - -@interface QMUIDialogViewController (UIAppearance) - -+ (instancetype)appearance; -@end - -/// 表示没有选中的item -extern const NSInteger QMUIDialogSelectionViewControllerSelectedItemIndexNone; - -/** - * 支持列表选择的弹窗,通过 `items` 指定要展示的所有选项(暂时只支持`NSString`)。默认使用单选,可通过 `allowsMultipleSelection` 支持多选。 - * 单选模式下,通过 `selectedItemIndex` 可获取当前被选中的选项,也可在初始化完dialog后设置这个属性来达到默认值的效果。 - * 多选模式下,通过 `selectedItemIndexes` 可获取当前被选中的多个选项,可也在初始化完dialog后设置这个属性来达到默认值的效果。 - */ -@interface QMUIDialogSelectionViewController : QMUIDialogViewController - -@property(nonatomic, strong, readonly) QMUITableView *tableView; - -@property(nonatomic, copy) NSArray *items; - -/// 表示单选模式下已选中的item序号,默认为QMUIDialogSelectionViewControllerSelectedItemIndexNone。此属性与 `selectedItemIndexes` 互斥。 -@property(nonatomic, assign) NSInteger selectedItemIndex; - -/// 表示多选模式下已选中的item序号,默认为nil。此属性与 `selectedItemIndex` 互斥。 -@property(nonatomic, strong) NSMutableSet *selectedItemIndexes; - -/// 控制是否允许多选,默认为NO。 -@property(nonatomic, assign) BOOL allowsMultipleSelection; - -@property(nonatomic, copy) void (^cellForItemBlock)(QMUIDialogSelectionViewController *dialogViewController, QMUITableViewCell *cell, NSUInteger itemIndex); -@property(nonatomic, copy) CGFloat (^heightForItemBlock)(QMUIDialogSelectionViewController *dialogViewController, NSUInteger itemIndex); -@property(nonatomic, copy) BOOL (^canSelectItemBlock)(QMUIDialogSelectionViewController *dialogViewController, NSUInteger itemIndex); -@property(nonatomic, copy) void (^didSelectItemBlock)(QMUIDialogSelectionViewController *dialogViewController, NSUInteger itemIndex); -@property(nonatomic, copy) void (^didDeselectItemBlock)(QMUIDialogSelectionViewController *dialogViewController, NSUInteger itemIndex); -@end - -/** - * 支持单行文本输入的弹窗,可通过`maximumLength`属性来控制最长可输入的字符,超过则无法继续输入。 - * 可通过`enablesSubmitButtonAutomatically`来自动设置`submitButton.enabled`的状态 - */ -@interface QMUIDialogTextFieldViewController : QMUIDialogViewController - -@property(nonatomic, strong, readonly) QMUITextField *textField; - -/// 是否自动控制提交按钮的enabled状态,默认为YES,则当输入框内容为空时禁用提交按钮 -@property(nonatomic, assign) BOOL enablesSubmitButtonAutomatically; - -@property(nonatomic, copy) BOOL (^shouldEnableSubmitButtonBlock)(QMUIDialogTextFieldViewController *dialogViewController); - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIDialogViewController.m b/QMUI/QMUIKit/UIComponents/QMUIDialogViewController.m deleted file mode 100644 index f280fdd4..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIDialogViewController.m +++ /dev/null @@ -1,644 +0,0 @@ -;// -// QMUIDialogViewController.m -// WeRead -// -// Created by MoLice on 16/7/8. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIDialogViewController.h" -#import "QMUICore.h" -#import "QMUIButton.h" -#import "QMUITextField.h" -#import "QMUITableViewCell.h" -#import "QMUINavigationTitleView.h" -#import "QMUIModalPresentationViewController.h" -#import "CALayer+QMUI.h" -#import "UITableView+QMUI.h" -#import "NSString+QMUI.h" - -@implementation QMUIDialogViewController (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; - }); -} - -static QMUIDialogViewController *dialogViewControllerAppearance; -+ (instancetype)appearance { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - if (!dialogViewControllerAppearance) { - dialogViewControllerAppearance = [[QMUIDialogViewController alloc] init]; - dialogViewControllerAppearance.cornerRadius = 6; - dialogViewControllerAppearance.contentViewMargins = UIEdgeInsetsMake(20, 20, 20, 20); - dialogViewControllerAppearance.titleTintColor = UIColorBlack; - dialogViewControllerAppearance.titleLabelFont = UIFontMake(16); - dialogViewControllerAppearance.titleLabelTextColor = UIColorMake(53, 60, 70); - dialogViewControllerAppearance.subTitleLabelFont = UIFontMake(12); - dialogViewControllerAppearance.subTitleLabelTextColor = UIColorMake(133, 140, 150); - - dialogViewControllerAppearance.headerFooterSeparatorColor = UIColorMake(222, 224, 226); - dialogViewControllerAppearance.headerViewHeight = 48; - dialogViewControllerAppearance.headerViewBackgroundColor = UIColorMake(244, 245, 247); - dialogViewControllerAppearance.footerViewHeight = 48; - dialogViewControllerAppearance.footerViewBackgroundColor = UIColorWhite; - - dialogViewControllerAppearance.buttonTitleAttributes = @{NSForegroundColorAttributeName: UIColorBlue, NSKernAttributeName: @2}; - dialogViewControllerAppearance.buttonHighlightedBackgroundColor = [UIColorBlue colorWithAlphaComponent:.25]; - } - }); - return dialogViewControllerAppearance; -} - -@end - -@interface QMUIDialogViewController () - -@property(nonatomic, assign) BOOL hasCustomContentView; -@property(nonatomic,copy) void (^cancelButtonBlock)(QMUIDialogViewController *dialogViewController); -@property(nonatomic,copy) void (^submitButtonBlock)(QMUIDialogViewController *dialogViewController); -@end - -@implementation QMUIDialogViewController - -- (void)didInitialized { - [super didInitialized]; - if (dialogViewControllerAppearance) { - self.cornerRadius = [QMUIDialogViewController appearance].cornerRadius; - self.contentViewMargins = [QMUIDialogViewController appearance].contentViewMargins; - 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.headerFooterSeparatorColor = [QMUIDialogViewController appearance].headerFooterSeparatorColor; - self.headerViewHeight = [QMUIDialogViewController appearance].headerViewHeight; - self.headerViewBackgroundColor = [QMUIDialogViewController appearance].headerViewBackgroundColor; - self.footerViewHeight = [QMUIDialogViewController appearance].footerViewHeight; - self.footerViewBackgroundColor = [QMUIDialogViewController appearance].footerViewBackgroundColor; - self.buttonTitleAttributes = [QMUIDialogViewController appearance].buttonTitleAttributes; - self.buttonHighlightedBackgroundColor = [QMUIDialogViewController appearance].buttonHighlightedBackgroundColor; - } -} - -- (void)setCornerRadius:(CGFloat)cornerRadius { - _cornerRadius = cornerRadius; - if ([self isViewLoaded ]) { - self.view.layer.cornerRadius = cornerRadius; - } -} - -- (void)setTitleTintColor:(UIColor *)titleTintColor { - _titleTintColor = titleTintColor; - self.titleView.tintColor = titleTintColor; -} - -- (void)setTitleLabelFont:(UIFont *)titleLabelFont { - _titleLabelFont = titleLabelFont; - self.titleView.titleLabel.font = titleLabelFont; -} - -- (void)setTitleLabelTextColor:(UIColor *)titleLabelTextColor { - _titleLabelTextColor = titleLabelTextColor; - self.titleView.titleLabel.textColor = titleLabelTextColor; -} - -- (void)setSubTitleLabelFont:(UIFont *)subTitleLabelFont { - _subTitleLabelFont = subTitleLabelFont; - self.titleView.subtitleLabel.font = subTitleLabelFont; -} - -- (void)setSubTitleLabelTextColor:(UIColor *)subTitleLabelTextColor { - _subTitleLabelTextColor = subTitleLabelTextColor; - self.titleView.subtitleLabel.textColor = subTitleLabelTextColor; -} - -- (void)setHeaderFooterSeparatorColor:(UIColor *)headerFooterSeparatorColor { - _headerFooterSeparatorColor = headerFooterSeparatorColor; - if (self.headerViewSeparatorLayer) { - self.headerViewSeparatorLayer.backgroundColor = headerFooterSeparatorColor.CGColor; - } - if (self.footerViewSeparatorLayer) { - self.footerViewSeparatorLayer.backgroundColor = headerFooterSeparatorColor.CGColor; - } - if (self.buttonSeparatorLayer) { - self.buttonSeparatorLayer.backgroundColor = headerFooterSeparatorColor.CGColor; - } -} - -- (void)setHeaderViewHeight:(CGFloat)headerViewHeight { - _headerViewHeight = headerViewHeight; -} - -- (void)setHeaderViewBackgroundColor:(UIColor *)headerViewBackgroundColor { - _headerViewBackgroundColor = headerViewBackgroundColor; - if ([self isViewLoaded]) { - self.headerView.backgroundColor = headerViewBackgroundColor; - } -} - -- (void)setFooterViewHeight:(CGFloat)footerViewHeight { - _footerViewHeight = footerViewHeight; -} - -- (void)setFooterViewBackgroundColor:(UIColor *)footerViewBackgroundColor { - _footerViewBackgroundColor = footerViewBackgroundColor; - self.footerView.backgroundColor = footerViewBackgroundColor; -} - -- (void)setButtonTitleAttributes:(NSDictionary *)buttonTitleAttributes { - _buttonTitleAttributes = buttonTitleAttributes; - if (self.cancelButton) { - [self.cancelButton setAttributedTitle:[[NSAttributedString alloc] initWithString:[self.cancelButton attributedTitleForState:UIControlStateNormal].string attributes:buttonTitleAttributes] forState:UIControlStateNormal]; - } - if (self.submitButton) { - [self.submitButton setAttributedTitle:[[NSAttributedString alloc] initWithString:[self.submitButton attributedTitleForState:UIControlStateNormal].string attributes:buttonTitleAttributes] forState:UIControlStateNormal]; - } -} - -- (void)setButtonHighlightedBackgroundColor:(UIColor *)buttonHighlightedBackgroundColor { - _buttonHighlightedBackgroundColor = buttonHighlightedBackgroundColor; - if (self.cancelButton) { - self.cancelButton.highlightedBackgroundColor = buttonHighlightedBackgroundColor; - } - if (self.submitButton) { - self.submitButton.highlightedBackgroundColor = buttonHighlightedBackgroundColor; - } -} - -BeginIgnoreClangWarning(-Wobjc-missing-super-calls) -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - // 不继承父类的实现,从而避免把 self.titleView 放到 navigationItem 上 -// [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; -} -EndIgnoreClangWarning - -- (void)viewDidLoad { - [super viewDidLoad]; - - // subview都在[super viewDidLoad]里添加,所以在添加完subview后再强制把headerView和footerView拉到最前面,以保证分隔线不会被subview盖住 - [self.view bringSubviewToFront:self.headerView]; - [self.view bringSubviewToFront:self.footerView]; - - self.view.backgroundColor = UIColorClear;// 减少Color Blended Layers - self.view.layer.cornerRadius = self.cornerRadius; - self.view.layer.masksToBounds = YES; -} - -- (void)initSubviews { - [super initSubviews]; - - if (self.hasCustomContentView) { - if (!self.contentView.superview) { - [self.view insertSubview:self.contentView atIndex:0]; - } - } else { - _contentView = [[UIView alloc] init];// 特地不使用setter,从而不要影响self.hasCustomContentView的默认值 - self.contentView.backgroundColor = UIColorWhite; - [self.view addSubview:self.contentView]; - } - - _headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), self.headerViewHeight)]; - self.headerView.backgroundColor = self.headerViewBackgroundColor; - - // 使用自带的QMUINavigationTitleView,支持loading、subTitle - [self.headerView addSubview:self.titleView]; - - // 加上分隔线 - _headerViewSeparatorLayer = [CALayer layer]; - [self.headerViewSeparatorLayer qmui_removeDefaultAnimations]; - self.headerViewSeparatorLayer.backgroundColor = self.headerFooterSeparatorColor.CGColor; - [self.headerView.layer addSublayer:self.headerViewSeparatorLayer]; - - [self.view addSubview:self.headerView]; - - [self initFooterViewIfNeeded]; -} - -- (void)setContentView:(UIView *)contentView { - if (_contentView != contentView) { - [_contentView removeFromSuperview]; - _contentView = contentView; - if ([self isViewLoaded]) { - [self.view insertSubview:_contentView atIndex:0]; - } - self.hasCustomContentView = YES; - } else { - self.hasCustomContentView = NO; - } -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - BOOL isFooterViewShowing = self.footerView && !self.footerView.hidden; - - self.headerView.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), self.headerViewHeight); - self.headerViewSeparatorLayer.frame = CGRectMake(0, CGRectGetHeight(self.headerView.bounds), CGRectGetWidth(self.headerView.bounds), PixelOne); - CGFloat headerViewPaddingHorizontal = 16; - CGFloat headerViewContentWidth = CGRectGetWidth(self.headerView.bounds) - headerViewPaddingHorizontal * 2; - CGSize titleViewSize = [self.titleView sizeThatFits:CGSizeMake(headerViewContentWidth, CGFLOAT_MAX)]; - CGFloat titleViewWidth = fminf(titleViewSize.width, headerViewContentWidth); - self.titleView.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.headerView.bounds), titleViewWidth), CGFloatGetCenter(CGRectGetHeight(self.headerView.bounds), titleViewSize.height), titleViewWidth, titleViewSize.height); - - if (isFooterViewShowing) { - self.footerView.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - self.footerViewHeight, CGRectGetWidth(self.view.bounds), self.footerViewHeight); - self.footerViewSeparatorLayer.frame = CGRectMake(0, -PixelOne, CGRectGetWidth(self.footerView.bounds), PixelOne); - - NSUInteger buttonCount = self.footerView.subviews.count; - if (buttonCount == 1) { - QMUIButton *button = self.cancelButton ? : self.submitButton; - button.frame = self.footerView.bounds; - self.buttonSeparatorLayer.hidden = YES; - } else { - CGFloat buttonWidth = flat(CGRectGetWidth(self.footerView.bounds) / buttonCount); - self.cancelButton.frame = CGRectMake(0, 0, buttonWidth, CGRectGetHeight(self.footerView.bounds)); - self.submitButton.frame = CGRectMake(CGRectGetMaxX(self.cancelButton.frame), 0, CGRectGetWidth(self.footerView.bounds) - CGRectGetMaxX(self.cancelButton.frame), CGRectGetHeight(self.footerView.bounds)); - self.buttonSeparatorLayer.hidden = NO; - self.buttonSeparatorLayer.frame = CGRectMake(CGRectGetMaxX(self.cancelButton.frame), 0, PixelOne, CGRectGetHeight(self.footerView.bounds)); - } - } - - CGFloat contentViewMinY = CGRectGetMaxY(self.headerView.frame); - CGFloat contentViewHeight = (isFooterViewShowing ? CGRectGetMinY(self.footerView.frame) : CGRectGetHeight(self.view.bounds)) - contentViewMinY; - self.contentView.frame = CGRectMake(0, contentViewMinY, CGRectGetWidth(self.view.bounds), contentViewHeight); -} - -- (void)initFooterViewIfNeeded { - if (!self.footerView) { - _footerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), self.footerViewHeight)]; - self.footerView.backgroundColor = self.footerViewBackgroundColor; - self.footerView.hidden = YES; - - _footerViewSeparatorLayer = [CALayer layer]; - [self.footerViewSeparatorLayer qmui_removeDefaultAnimations]; - self.footerViewSeparatorLayer.backgroundColor = self.headerFooterSeparatorColor.CGColor; - [self.footerView.layer addSublayer:self.footerViewSeparatorLayer]; - - _buttonSeparatorLayer = [CALayer layer]; - [self.buttonSeparatorLayer qmui_removeDefaultAnimations]; - self.buttonSeparatorLayer.backgroundColor = self.footerViewSeparatorLayer.backgroundColor; - self.buttonSeparatorLayer.hidden = YES; - [self.footerView.layer addSublayer:self.buttonSeparatorLayer]; - - [self.view addSubview:self.footerView]; - } -} - -- (void)addCancelButtonWithText:(NSString *)buttonText block:(void (^)(QMUIDialogViewController *))block { - if (_cancelButton) { - [_cancelButton removeFromSuperview]; - } - - _cancelButton = [self generateButtonWithText:buttonText]; - [self.cancelButton addTarget:self action:@selector(handleCancelButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - - [self initFooterViewIfNeeded]; - self.footerView.hidden = NO; - [self.footerView addSubview:self.cancelButton]; - - self.cancelButtonBlock = block; -} - -- (void)addSubmitButtonWithText:(NSString *)buttonText block:(void (^)(QMUIDialogViewController *dialogViewController))block { - if (_submitButton) { - [_submitButton removeFromSuperview]; - } - - _submitButton = [self generateButtonWithText:buttonText]; - [self.submitButton addTarget:self action:@selector(handleSubmitButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - - [self initFooterViewIfNeeded]; - self.footerView.hidden = NO; - [self.footerView addSubview:self.submitButton]; - - self.submitButtonBlock = block; -} - -- (QMUIButton *)generateButtonWithText:(NSString *)buttonText { - QMUIButton *button = [[QMUIButton alloc] init]; - button.titleLabel.font = UIFontBoldMake(15); - button.adjustsTitleTintColorAutomatically = YES; - button.highlightedBackgroundColor = self.buttonHighlightedBackgroundColor; - [button setAttributedTitle:[[NSAttributedString alloc] initWithString:buttonText attributes:self.buttonTitleAttributes] forState:UIControlStateNormal]; - return button; -} - -- (void)handleCancelButtonEvent:(QMUIButton *)cancelButton { - [self hideWithAnimated:YES completion:^(BOOL finished) { - if (self.cancelButtonBlock) { - self.cancelButtonBlock(self); - } - }]; -} - -- (void)handleSubmitButtonEvent:(QMUIButton *)submitButton { - if (self.submitButtonBlock) { - // 把自己传过去,方便在block里调用self时不会导致内存泄露 - __weak QMUIDialogViewController *weakSelf = self; - self.submitButtonBlock(weakSelf); - } -} - -- (void)show { - [self showWithAnimated:YES completion:nil]; -} - -- (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { - QMUIModalPresentationViewController *modalPresentationViewController = [[QMUIModalPresentationViewController alloc] init]; - modalPresentationViewController.contentViewMargins = self.contentViewMargins; - modalPresentationViewController.contentViewController = self; - modalPresentationViewController.modal = YES; - [modalPresentationViewController showWithAnimated:YES completion:completion]; -} - -- (void)hide { - [self hideWithAnimated:YES completion:nil]; -} - -- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { - [self.modalPresentedViewController hideWithAnimated:animated completion:^(BOOL finished) { - if (completion) { - completion(finished); - } - }]; -} - -#pragma mark - - -- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller limitSize:(CGSize)limitSize { - if (!self.hasCustomContentView) { - return limitSize; - } - - BOOL isFooterViewShowing = self.footerView && !self.footerView.hidden; - CGFloat footerViewHeight = isFooterViewShowing ? self.footerViewHeight : 0; - - CGSize contentViewLimitSize = CGSizeMake(limitSize.width, limitSize.height - self.headerViewHeight - footerViewHeight); - CGSize contentViewSize = [self.contentView sizeThatFits:contentViewLimitSize]; - - CGSize finalSize = CGSizeMake(fminf(limitSize.width, contentViewSize.width), fminf(limitSize.height, self.headerViewHeight + contentViewSize.height + footerViewHeight)); - return finalSize; -} - -@end - -const NSInteger QMUIDialogSelectionViewControllerSelectedItemIndexNone = -1; - -@interface QMUIDialogSelectionViewController () - -@property(nonatomic,strong,readwrite) QMUITableView *tableView; -@end - -@implementation QMUIDialogSelectionViewController - -- (void)didInitialized { - [super didInitialized]; - self.selectedItemIndex = QMUIDialogSelectionViewControllerSelectedItemIndexNone; - self.selectedItemIndexes = [[NSMutableSet alloc] init]; - BeginIgnoreAvailabilityWarning - [self loadViewIfNeeded]; - EndIgnoreAvailabilityWarning -} - -- (void)initSubviews { - [super initSubviews]; - self.tableView = [[QMUITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; - self.tableView.delegate = self; - self.tableView.dataSource = self; - self.tableView.alwaysBounceVertical = NO; - [self.view addSubview:self.tableView]; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - CGFloat tableViewMinY = CGRectGetMaxY(self.headerView.frame); - CGFloat tableViewHeight = CGRectGetHeight(self.view.bounds) - tableViewMinY - (!self.footerView.hidden ? CGRectGetHeight(self.footerView.frame) : 0); - self.tableView.frame = CGRectMake(0, tableViewMinY, CGRectGetWidth(self.view.bounds), tableViewHeight); -} - -- (void)viewDidAppear:(BOOL)animated { - [super viewDidAppear:animated]; - // 当前的分组不在可视区域内,则滚动到可视区域(只对单选有效) - if (self.selectedItemIndex != QMUIDialogSelectionViewControllerSelectedItemIndexNone && self.selectedItemIndex < self.items.count && ![self.tableView qmui_cellVisibleAtIndexPath:[NSIndexPath indexPathForRow:self.selectedItemIndex inSection:0]]) { - [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.selectedItemIndex inSection:0] atScrollPosition:UITableViewScrollPositionMiddle animated:animated]; - } -} - -- (void)setSelectedItemIndex:(NSInteger)selectedItemIndex { - _selectedItemIndex = selectedItemIndex; - [self.selectedItemIndexes removeAllObjects]; -} - -- (void)setselectedItemIndexes:(NSMutableSet *)selectedItemIndexes { - _selectedItemIndexes = selectedItemIndexes; - self.selectedItemIndex = QMUIDialogSelectionViewControllerSelectedItemIndexNone; -} - -- (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection { - _allowsMultipleSelection = allowsMultipleSelection; - self.selectedItemIndex = QMUIDialogSelectionViewControllerSelectedItemIndexNone; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return self.items.count; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - static NSString *identifier = @"cell"; - QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; - if (!cell) { - cell = [[QMUITableViewCell alloc] initForTableView:self.tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]; - } - cell.textLabel.text = self.items[indexPath.row]; - - if (self.allowsMultipleSelection) { - // 多选 - if ([self.selectedItemIndexes containsObject:@(indexPath.row)]) { - cell.accessoryType = UITableViewCellAccessoryCheckmark; - } else { - cell.accessoryType = UITableViewCellAccessoryNone; - } - } else { - // 单选 - if (self.selectedItemIndex == indexPath.row) { - cell.accessoryType = UITableViewCellAccessoryCheckmark; - } else { - cell.accessoryType = UITableViewCellAccessoryNone; - } - } - - [cell updateCellAppearanceWithIndexPath:indexPath]; - - if (self.cellForItemBlock) { - self.cellForItemBlock(self, cell, indexPath.row); - } - return cell; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - if (self.heightForItemBlock) { - return self.heightForItemBlock(self, indexPath.row); - } - return TableViewCellNormalHeight; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - - // 单选情况下如果重复选中已被选中的cell,则什么都不做 - if (!self.allowsMultipleSelection && self.selectedItemIndex == indexPath.row) { - [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; - return; - } - - // 不允许选中当前cell,直接return - if (self.canSelectItemBlock && !self.canSelectItemBlock(self, indexPath.row)) { - [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; - return; - } - - if (self.allowsMultipleSelection) { - if ([self.selectedItemIndexes containsObject:@(indexPath.row)]) { - // 当前的cell已经被选中,则取消选中 - [self.selectedItemIndexes removeObject:@(indexPath.row)]; - if (self.didDeselectItemBlock) { - self.didDeselectItemBlock(self, indexPath.row); - } - } else { - [self.selectedItemIndexes addObject:@(indexPath.row)]; - if (self.didSelectItemBlock) { - self.didSelectItemBlock(self, indexPath.row); - } - } - if ([tableView qmui_cellVisibleAtIndexPath:indexPath]) { - [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; - } - } else { - BOOL isSelectedIndexPathBeforeVisible = NO; - - // 选中新的cell时,先反选之前被选中的那个cell - NSIndexPath *selectedIndexPathBefore = nil; - if (self.selectedItemIndex != QMUIDialogSelectionViewControllerSelectedItemIndexNone) { - selectedIndexPathBefore = [NSIndexPath indexPathForRow:self.selectedItemIndex inSection:0]; - if (self.didDeselectItemBlock) { - self.didDeselectItemBlock(self, selectedIndexPathBefore.row); - } - isSelectedIndexPathBeforeVisible = [tableView qmui_cellVisibleAtIndexPath:selectedIndexPathBefore]; - } - - self.selectedItemIndex = indexPath.row; - - // 如果之前被选中的那个cell也在可视区域里,则也要用动画去刷新它,否则只需要用动画刷新当前已选中的cell即可,之前被选中的那个交给cellForRow去刷新 - if (isSelectedIndexPathBeforeVisible) { - [tableView reloadRowsAtIndexPaths:@[selectedIndexPathBefore, indexPath] withRowAnimation:UITableViewRowAnimationFade]; - } else { - [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; - } - - if (self.didSelectItemBlock) { - self.didSelectItemBlock(self, indexPath.row); - } - } -} - -#pragma mark - - -- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller limitSize:(CGSize)limitSize { - CGFloat footerViewHeight = !self.footerView.hidden ? CGRectGetHeight(self.footerView.frame) : 0; - CGFloat tableViewLimitHeight = limitSize.height - CGRectGetHeight(self.headerView.frame) - footerViewHeight; - CGSize tableViewSize = [self.tableView sizeThatFits:CGSizeMake(limitSize.width, tableViewLimitHeight)]; - CGFloat finalTableViewHeight = fminf(tableViewSize.height, tableViewLimitHeight); - return CGSizeMake(limitSize.width, CGRectGetHeight(self.headerView.frame) + finalTableViewHeight + footerViewHeight); -} - -@end - -@interface QMUIDialogTextFieldViewController () - -@property(nonatomic,strong,readwrite) QMUITextField *textField; -@end - -@implementation QMUIDialogTextFieldViewController - -- (void)didInitialized { - [super didInitialized]; - self.enablesSubmitButtonAutomatically = YES; - BeginIgnoreAvailabilityWarning - [self loadViewIfNeeded]; - EndIgnoreAvailabilityWarning -} - -- (void)initSubviews { - [super initSubviews]; - self.textField = [[QMUITextField alloc] init]; - self.textField.backgroundColor = UIColorWhite; - self.textField.textInsets = UIEdgeInsetsMake(self.textField.textInsets.top, 16, self.textField.textInsets.bottom, 16); - self.textField.returnKeyType = UIReturnKeyDone; - self.textField.enablesReturnKeyAutomatically = self.enablesSubmitButtonAutomatically; - [self.textField addTarget:self action:@selector(handleTextFieldTextDidChangeEvent:) forControlEvents:UIControlEventEditingChanged]; - [self.view addSubview:self.textField]; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - self.textField.frame = CGRectMake(0, CGRectGetMaxY(self.headerView.frame), CGRectGetWidth(self.view.bounds), (!self.footerView.hidden ? CGRectGetMinY(self.footerView.frame) : CGRectGetHeight(self.view.bounds)) - CGRectGetMaxY(self.headerView.frame)); -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - [self.textField becomeFirstResponder]; -} - -- (void)viewWillDisappear:(BOOL)animated { - [super viewWillDisappear:animated]; - [self.textField resignFirstResponder]; -} - -#pragma mark - Submit Button Enables - -- (void)setEnablesSubmitButtonAutomatically:(BOOL)enablesSubmitButtonAutomatically { - _enablesSubmitButtonAutomatically = enablesSubmitButtonAutomatically; - self.textField.enablesReturnKeyAutomatically = _enablesSubmitButtonAutomatically; - if (_enablesSubmitButtonAutomatically) { - [self updateSubmitButtonEnables]; - } -} - -- (void)updateSubmitButtonEnables { - self.submitButton.enabled = [self shouldEnabledSubmitButton]; -} - -- (BOOL)shouldEnabledSubmitButton { - if (self.shouldEnableSubmitButtonBlock) { - return self.shouldEnableSubmitButtonBlock(self); - } - - if (self.enablesSubmitButtonAutomatically) { - NSInteger textLength = self.textField.text.qmui_trim.length; - return 0 < textLength && textLength <= self.textField.maximumTextLength; - } - - return YES; -} - -- (void)handleTextFieldTextDidChangeEvent:(QMUITextField *)textField { - if (self.textField == textField) { - [self updateSubmitButtonEnables]; - } -} - -- (void)addSubmitButtonWithText:(NSString *)buttonText block:(void (^)(QMUIDialogViewController *dialogViewController))block { - [super addSubmitButtonWithText:buttonText block:block]; - [self updateSubmitButtonEnables]; -} - -#pragma mark - - -- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller limitSize:(CGSize)limitSize { - CGFloat textFieldHeight = 56; - return CGSizeMake(limitSize.width, CGRectGetHeight(self.headerView.frame) + textFieldHeight + (!self.footerView.hidden ? CGRectGetHeight(self.footerView.frame) : 0)); -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIEmotionView.h b/QMUI/QMUIKit/UIComponents/QMUIEmotionView.h deleted file mode 100644 index 57beb971..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIEmotionView.h +++ /dev/null @@ -1,113 +0,0 @@ -// -// QMUIEmotionView.h -// qmui -// -// Created by MoLice on 16/9/6. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import - -@class QMUIButton; - -/** - * 代表一个表情的数据对象 - */ -@interface QMUIEmotion : NSObject - -/// 当前表情的标识符,可用于区分不同表情 -@property(nonatomic, copy) NSString *identifier; - -/// 当前表情展示出来的名字,可用于输入框里的占位文字,例如“[委屈]” -@property(nonatomic, copy) NSString *displayName; - -/// 表情对应的图片。若表情图片存放于项目内,则建议用当前表情的`identifier`作为图片名 -@property(nonatomic, strong) UIImage *image; - -/** - * 快速生成一个`QMUIEmotion`对象,并且以`identifier`为图片名在当前项目里查找,作为表情的图片 - * @param identifier 表情的标识符,也会被当成图片的名字 - * @param displayName 表情展示出来的名字 - */ -+ (instancetype)emotionWithIdentifier:(NSString *)identifier displayName:(NSString *)displayName; - -@end - - - - -/** - * 表情控件,支持任意表情的展示,每个表情以相同的大小显示。 - * - * 使用方式: - * - * - 通过`initWithFrame:`初始化,如果面板高度不变,建议在init时就设置好,若最终布局以父类的`layoutSubviews`为准,则也可通过`init`方法初始化,再在`layoutSubviews`里计算布局 - * - 通过调整`paddingInPage`、`emotionSize`等变量来自定义UI - * - 通过`emotions`设置要展示的表情 - * - 通过`didSelectEmotionBlock`设置选中表情时的回调,通过`didSelectDeleteButtonBlock`来响应面板内的删除按钮 - * - 为`sendButton`添加`addTarget:action:forState:`事件,从而触发发送逻辑 - * - * 本控件支持通过`UIAppearance`设置全局的默认样式。若要修改控件内的`UIPageControl`的样式,可通过`[UIPageControl appearanceWhenContainedIn:[QMUIEmotionView class], nil]`的方式来修改。 - */ -@interface QMUIEmotionView : UIView - -/// 要展示的所有表情 -@property(nonatomic, copy) NSArray *emotions; - -/** - * 选中表情时的回调 - * @argv index 被选中的表情在`emotions`里的索引 - * @argv emotion 被选中的表情对应的`QMUIEmotion`对象 - * @see QMUIEmotion - */ -@property(nonatomic, copy) void (^didSelectEmotionBlock)(NSInteger index, QMUIEmotion *emotion); - -/// 删除按钮的点击事件回调 -@property(nonatomic, copy) void (^didSelectDeleteButtonBlock)(); - -/// 用于展示表情面板的横向滚动collectionView,布局撑满整个控件 -@property(nonatomic, strong, readonly) UICollectionView *collectionView; - -/// 用于横向按页滚动的collectionViewLayout -@property(nonatomic, strong, readonly) UICollectionViewFlowLayout *collectionViewLayout; - -/// 控件底部的分页控件,可点击切换表情页面 -@property(nonatomic, strong, readonly) UIPageControl *pageControl; - -/// 控件右下角的发送按钮 -@property(nonatomic, strong, readonly) QMUIButton *sendButton; - -/// 每一页表情的上下左右padding,默认为{18, 18, 65, 18} -@property(nonatomic, assign) UIEdgeInsets paddingInPage UI_APPEARANCE_SELECTOR; - -/// 每一页表情允许的最大行数,默认为4 -@property(nonatomic, assign) NSInteger numberOfRowsPerPage UI_APPEARANCE_SELECTOR; - -/// 表情的图片大小,不管`QMUIEmotion.image.size`多大,都会被缩放到`emotionSize`里显示,默认为{30, 30} -@property(nonatomic, assign) CGSize emotionSize UI_APPEARANCE_SELECTOR; - -/// 表情点击时的背景遮罩相对于`emotionSize`往外拓展的区域,负值表示遮罩比表情还大,正值表示遮罩比表情还小,默认为{-3, -3, -3, -3} -@property(nonatomic, assign) UIEdgeInsets emotionSelectedBackgroundExtension UI_APPEARANCE_SELECTOR; - -/// 表情与表情之间的最小水平间距,默认为10 -@property(nonatomic, assign) CGFloat minimumEmotionHorizontalSpacing UI_APPEARANCE_SELECTOR; - -/// 表情面板右下角的删除按钮的图片,默认为`[QMUIHelper imageWithName:@"QMUI_emotion_delete"]` -@property(nonatomic, strong) UIImage *deleteButtonImage UI_APPEARANCE_SELECTOR; - -/// 发送按钮的文字样式,默认为{NSFontAttributeName: UIFontMake(15), NSForegroundColorAttributeName: UIColorWhite} -@property(nonatomic, strong) NSDictionary *sendButtonTitleAttributes UI_APPEARANCE_SELECTOR; - -/// 发送按钮的背景色,默认为`UIColorBlue` -@property(nonatomic, strong) UIColor *sendButtonBackgroundColor UI_APPEARANCE_SELECTOR; - -/// 发送按钮的圆角大小,默认为4 -@property(nonatomic, assign) CGFloat sendButtonCornerRadius UI_APPEARANCE_SELECTOR; - -/// 发送按钮布局时的外边距,相对于控件右下角。仅right/bottom有效,默认为{0, 0, 16, 16} -@property(nonatomic, assign) UIEdgeInsets sendButtonMargins UI_APPEARANCE_SELECTOR; - -/// 分页控件距离底部的间距,默认为22 -@property(nonatomic, assign) CGFloat pageControlMarginBottom UI_APPEARANCE_SELECTOR; - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIEmotionView.m b/QMUI/QMUIKit/UIComponents/QMUIEmotionView.m deleted file mode 100644 index 6e4fd719..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIEmotionView.m +++ /dev/null @@ -1,398 +0,0 @@ -// -// QMUIEmotionView.m -// qmui -// -// Created by MoLice on 16/9/6. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIEmotionView.h" -#import "QMUICore.h" -#import "QMUIButton.h" -#import "UIView+QMUI.h" -#import "UIScrollView+QMUI.h" -#import "UIControl+QMUI.h" -#import "UIImage+QMUI.h" - -@implementation QMUIEmotion - -@synthesize image = _image; - -+ (instancetype)emotionWithIdentifier:(NSString *)identifier displayName:(NSString *)displayName { - QMUIEmotion *emotion = [[QMUIEmotion alloc] init]; - emotion.identifier = identifier; - emotion.displayName = displayName; - return emotion; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"%@, identifier: %@, displayName: %@", [super description], self.identifier, self.displayName]; -} - -- (UIImage *)image { - if (!_image) { - _image = [QMUIHelper imageInBundle:[QMUIHelper resourcesBundleWithName:QMUIResourcesQQEmotionBundleName] withName:self.identifier]; - } - return _image; -} - -@end - -@class QMUIEmotionPageView; - -@protocol QMUIEmotionPageViewDelegate - -@optional -- (void)emotionPageView:(QMUIEmotionPageView *)emotionPageView didSelectEmotion:(QMUIEmotion *)emotion atIndex:(NSInteger)index; -- (void)didSelectDeleteButtonInEmotionPageView:(QMUIEmotionPageView *)emotionPageView; - -@end - -/// 表情面板每一页的cell,在drawRect里将所有表情绘制上去,同时自带一个末尾的删除按钮 -@interface QMUIEmotionPageView : UICollectionViewCell - -@property(nonatomic, weak) QMUIEmotionView *delegate; - -/// 表情被点击时盖在表情上方用于表示选中的遮罩 -@property(nonatomic, strong) UIView *emotionSelectedBackgroundView; - -/// 表情面板右下角的删除按钮 -@property(nonatomic, strong) QMUIButton *deleteButton; - -/// 分配给当前pageView的所有表情 -@property(nonatomic, copy) NSArray *emotions; - -/// 记录当前pageView里所有表情的可点击区域的rect,在drawRect:里更新,在tap事件里使用 -@property(nonatomic, strong) NSMutableArray *emotionHittingRects; - -/// 负责实现表情的点击 -@property(nonatomic, strong) UITapGestureRecognizer *tapGestureRecognizer; - -/// 整个pageView内部的padding -@property(nonatomic, assign) UIEdgeInsets padding; - -/// 每个pageView能展示表情的行数 -@property(nonatomic, assign) NSInteger numberOfRows; - -/// 每个表情的绘制区域大小,表情图片最终会以UIViewContentModeScaleAspectFit的方式撑满这个大小。表情计算布局时也是基于这个大小来算的。 -@property(nonatomic, assign) CGSize emotionSize; - -/// 点击表情时出现的遮罩要在表情所在的矩形位置拓展多少空间,负值表示遮罩比emotionSize更大,正值表示遮罩比emotionSize更小。最终判断表情点击区域时也是以拓展后的区域来判定的 -@property(nonatomic, assign) UIEdgeInsets emotionSelectedBackgroundExtension; - -/// 表情与表情之间的水平间距的最小值,实际值可能比这个要大一点(pageView会把剩余空间分配到表情的水平间距里) -@property(nonatomic, assign) CGFloat minimumEmotionHorizontalSpacing; - -/// debug模式会把表情的绘制矩形显示出来 -@property(nonatomic, assign) BOOL debug; -@end - -@implementation QMUIEmotionPageView - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - self.backgroundColor = UIColorClear; - - self.emotionSelectedBackgroundView = [[UIView alloc] init]; - self.emotionSelectedBackgroundView.userInteractionEnabled = NO; - self.emotionSelectedBackgroundView.backgroundColor = UIColorMakeWithRGBA(0, 0, 0, .16); - self.emotionSelectedBackgroundView.layer.cornerRadius = 3; - 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]; - } - return self; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - // 删除按钮必定布局到最后一个表情的位置,且与表情上下左右居中 - [self.deleteButton sizeToFit]; - self.deleteButton.frame = CGRectSetXY(self.deleteButton.frame, flat(CGRectGetWidth(self.bounds) - self.padding.right - CGRectGetWidth(self.deleteButton.frame) - (self.emotionSize.width - CGRectGetWidth(self.deleteButton.frame)) / 2.0), flat(CGRectGetHeight(self.bounds) - self.padding.bottom - CGRectGetHeight(self.deleteButton.frame) - (self.emotionSize.height - CGRectGetHeight(self.deleteButton.frame)) / 2.0)); -} - -- (void)drawRect:(CGRect)rect { - [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 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]; - } -} - -- (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 = fminf(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)); - } - [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)) { - QMUIEmotion *emotion = self.emotions[i]; - self.emotionSelectedBackgroundView.frame = rect; - [UIView animateWithDuration:.08 animations:^{ - self.emotionSelectedBackgroundView.alpha = 1; - } completion:^(BOOL finished) { - [UIView animateWithDuration:.08 animations:^{ - self.emotionSelectedBackgroundView.alpha = 0; - } completion:nil]; - }]; - if ([self.delegate respondsToSelector:@selector(emotionPageView:didSelectEmotion:atIndex:)]) { - [self.delegate emotionPageView:self didSelectEmotion:emotion atIndex:i]; - } - if (self.debug) { - NSLog(@"最终确定了点击的是当前页里的第 %@ 个表情,%@", @(i), emotion); - } - return; - } - } -} - -- (void)handleDeleteButtonEvent:(QMUIButton *)deleteButton { - if ([self.delegate respondsToSelector:@selector(didSelectDeleteButtonInEmotionPageView:)]) { - [self.delegate didSelectDeleteButtonInEmotionPageView:self]; - } -} - -@end - -@interface QMUIEmotionView () - -@property(nonatomic, strong) NSMutableArray *> *pagedEmotions; -@property(nonatomic, assign) BOOL debug; -@end - -@implementation QMUIEmotionView - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - [self didInitializedWithFrame:frame]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitializedWithFrame:CGRectZero]; - } - return self; -} - -- (void)didInitializedWithFrame:(CGRect)frame { - self.debug = NO; - - self.pagedEmotions = [[NSMutableArray alloc] init]; - - _collectionViewLayout = [[UICollectionViewFlowLayout alloc] init]; - self.collectionViewLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; - self.collectionViewLayout.minimumLineSpacing = 0; - self.collectionViewLayout.minimumInteritemSpacing = 0; - self.collectionViewLayout.sectionInset = UIEdgeInsetsZero; - - _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMakeWithSize(frame.size) collectionViewLayout:self.collectionViewLayout]; - self.collectionView.backgroundColor = UIColorClear; - self.collectionView.scrollsToTop = NO; - self.collectionView.pagingEnabled = YES; - self.collectionView.showsHorizontalScrollIndicator = NO; - self.collectionView.dataSource = self; - self.collectionView.delegate = self; - [self.collectionView registerClass:[QMUIEmotionPageView class] forCellWithReuseIdentifier:@"page"]; - [self addSubview:self.collectionView]; - - _pageControl = [[UIPageControl alloc] init]; - [self.pageControl addTarget:self action:@selector(handlePageControlEvent:) forControlEvents:UIControlEventValueChanged]; - [self addSubview:self.pageControl]; - - _sendButton = [[QMUIButton alloc] init]; - [self.sendButton setTitle:@"发送" forState:UIControlStateNormal]; - self.sendButton.contentEdgeInsets = UIEdgeInsetsMake(5, 17, 5, 17); - [self.sendButton sizeToFit]; - [self addSubview:self.sendButton]; -} - -- (void)setEmotions:(NSArray *)emotions { - _emotions = emotions; - [self pageEmotions]; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - BOOL collectionViewSizeChanged = !CGSizeEqualToSize(self.bounds.size, self.collectionView.bounds.size); - self.collectionView.frame = self.bounds; - self.collectionViewLayout.itemSize = self.collectionView.bounds.size; - - if (collectionViewSizeChanged) { - [self pageEmotions]; - } - - CGFloat pageControlHeight = 16; - self.pageControl.frame = CGRectMake(0, CGRectGetHeight(self.bounds) - self.pageControlMarginBottom - pageControlHeight, CGRectGetWidth(self.bounds), pageControlHeight); - - self.sendButton.frame = CGRectSetXY(self.sendButton.frame, CGRectGetWidth(self.bounds) - self.sendButtonMargins.right - CGRectGetWidth(self.sendButton.frame), CGRectGetHeight(self.bounds) - self.sendButtonMargins.bottom - CGRectGetHeight(self.sendButton.frame)); -} - -- (void)pageEmotions { - [self.pagedEmotions removeAllObjects]; - self.pageControl.numberOfPages = 0; - - if (!CGRectIsEmpty(self.collectionView.bounds) && self.emotions.count && !CGSizeIsEmpty(self.emotionSize)) { - CGFloat contentWidthInPage = CGRectGetWidth(self.collectionView.bounds) - UIEdgeInsetsGetHorizontalValue(self.paddingInPage); - NSInteger maximumEmotionCountPerRowInPage = (contentWidthInPage + self.minimumEmotionHorizontalSpacing) / (self.emotionSize.width + self.minimumEmotionHorizontalSpacing); - NSInteger maximumEmotionCountPerPage = maximumEmotionCountPerRowInPage * self.numberOfRowsPerPage - 1;// 删除按钮占一个表情位置 - NSInteger pageCount = ceil((CGFloat)self.emotions.count / (CGFloat)maximumEmotionCountPerPage); - for (NSInteger i = 0; i < pageCount; i ++) { - NSRange emotionRangeForPage = NSMakeRange(maximumEmotionCountPerPage * i, maximumEmotionCountPerPage); - if (NSMaxRange(emotionRangeForPage) > self.emotions.count) { - // 最后一页可能不满一整页,所以取剩余的所有表情即可 - emotionRangeForPage.length = self.emotions.count - emotionRangeForPage.location; - } - NSArray *emotionForPage = [self.emotions objectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:emotionRangeForPage]]; - [self.pagedEmotions addObject:emotionForPage]; - } - self.pageControl.numberOfPages = pageCount; - } - - [self.collectionView reloadData]; - [self.collectionView qmui_scrollToTop]; -} - -- (void)handlePageControlEvent:(UIPageControl *)pageControl { - [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:pageControl.currentPage inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES]; -} - -#pragma mark - UIAppearance Setter - -- (void)setSendButtonTitleAttributes:(NSDictionary *)sendButtonTitleAttributes { - _sendButtonTitleAttributes = sendButtonTitleAttributes; - [self.sendButton setAttributedTitle:[[NSAttributedString alloc] initWithString:[self.sendButton currentTitle] attributes:_sendButtonTitleAttributes] forState:UIControlStateNormal]; -} - -- (void)setSendButtonBackgroundColor:(UIColor *)sendButtonBackgroundColor { - _sendButtonBackgroundColor = sendButtonBackgroundColor; - self.sendButton.backgroundColor = _sendButtonBackgroundColor; -} - -- (void)setSendButtonCornerRadius:(CGFloat)sendButtonCornerRadius { - _sendButtonCornerRadius = sendButtonCornerRadius; - self.sendButton.layer.cornerRadius = _sendButtonCornerRadius; -} - -#pragma mark - - -- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - return self.pagedEmotions.count; -} - -- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - QMUIEmotionPageView *pageView = [collectionView dequeueReusableCellWithReuseIdentifier:@"page" forIndexPath:indexPath]; - pageView.delegate = self; - pageView.emotions = self.pagedEmotions[indexPath.item]; - pageView.padding = self.paddingInPage; - pageView.numberOfRows = self.numberOfRowsPerPage; - 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.debug = self.debug; - [pageView setNeedsDisplay]; - return pageView; -} - -- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { - if (scrollView == self.collectionView) { - NSInteger currentPage = round(scrollView.contentOffset.x / CGRectGetWidth(scrollView.bounds)); - self.pageControl.currentPage = currentPage; - } -} - -#pragma mark - - -- (void)emotionPageView:(QMUIEmotionPageView *)emotionPageView didSelectEmotion:(QMUIEmotion *)emotion atIndex:(NSInteger)index { - if (self.didSelectEmotionBlock) { - NSInteger index = [self.emotions indexOfObject:emotion]; - self.didSelectEmotionBlock(index, emotion); - } -} - -- (void)didSelectDeleteButtonInEmotionPageView:(QMUIEmotionPageView *)emotionPageView { - if (self.didSelectDeleteButtonBlock) { - self.didSelectDeleteButtonBlock(); - } -} - -@end - -@interface QMUIEmotionView (UIAppearance) - -@end - -@implementation QMUIEmotionView (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearance]; - }); -} - -+ (void)setDefaultAppearance { - QMUIEmotionView *appearance = [QMUIEmotionView appearance]; - appearance.backgroundColor = UIColorWhite; - appearance.deleteButtonImage = [QMUIHelper imageWithName:@"QMUI_emotion_delete"]; - appearance.paddingInPage = UIEdgeInsetsMake(18, 18, 65, 18); - appearance.numberOfRowsPerPage = 4; - appearance.emotionSize = CGSizeMake(30, 30); - appearance.emotionSelectedBackgroundExtension = UIEdgeInsetsMake(-3, -3, -3, -3); - appearance.minimumEmotionHorizontalSpacing = 10; - appearance.sendButtonTitleAttributes = @{NSFontAttributeName: UIFontMake(15), NSForegroundColorAttributeName: UIColorWhite}; - appearance.sendButtonBackgroundColor = UIColorBlue; - appearance.sendButtonCornerRadius = 4; - appearance.sendButtonMargins = UIEdgeInsetsMake(0, 0, 16, 16); - appearance.pageControlMarginBottom = 22; - - UIPageControl *pageControlAppearance = [UIPageControl appearanceWhenContainedIn:[QMUIEmotionView class], nil]; - pageControlAppearance.pageIndicatorTintColor = UIColorMake(210, 210, 210); - pageControlAppearance.currentPageIndicatorTintColor = UIColorMake(162, 162, 162); -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIEmptyView.m b/QMUI/QMUIKit/UIComponents/QMUIEmptyView.m deleted file mode 100644 index e17d328f..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIEmptyView.m +++ /dev/null @@ -1,301 +0,0 @@ -// -// QMUIEmptyView.m -// qmui -// -// Created by 李凯 on 2016/10/9. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIEmptyView.h" -#import "QMUICore.h" -#import "UIControl+QMUI.h" -#import "NSParagraphStyle+QMUI.h" - -@interface QMUIEmptyView () - -@property(nonatomic, strong) UIScrollView *scrollView; // 保证内容超出屏幕时也不至于直接被clip(比如横屏时) - -@end - -@implementation QMUIEmptyView - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - // 系统默认会在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.scrollView = [[UIScrollView alloc] init]; - self.scrollView.showsVerticalScrollIndicator = NO; - self.scrollView.showsHorizontalScrollIndicator = NO; - self.scrollView.scrollsToTop = NO; - self.scrollView.contentInset = UIEdgeInsetsMake(0, 10, 0, 10); // 避免 label 直接撑满到屏幕两边,不好看 - [self addSubview:self.scrollView]; - - _contentView = [[UIView alloc] init]; - [self.scrollView addSubview:self.contentView]; - - _loadingView = (UIView *)[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; - ((UIActivityIndicatorView *)self.loadingView).hidesWhenStopped = NO; // 此控件是通过loadingView.hidden属性来控制显隐的,如果UIActivityIndicatorView的hidesWhenStopped属性设置为YES的话,则手动设置它的hidden属性就会失效,因此这里要置为NO - [self.contentView addSubview:self.loadingView]; - - _imageView = [[UIImageView alloc] init]; - self.imageView.contentMode = UIViewContentModeCenter; - [self.contentView addSubview:self.imageView]; - - _textLabel = [[UILabel alloc] init]; - self.textLabel.textAlignment = NSTextAlignmentCenter; - self.textLabel.numberOfLines = 0; - [self.contentView addSubview:self.textLabel]; - - _detailTextLabel = [[UILabel alloc] init]; - self.detailTextLabel.textAlignment = NSTextAlignmentCenter; - self.detailTextLabel.numberOfLines = 0; - [self.contentView addSubview:self.detailTextLabel]; - - _actionButton = [[UIButton alloc] init]; - self.actionButton.qmui_outsideEdge = UIEdgeInsetsMake(-20, -20, -20, -20); - [self.contentView addSubview:self.actionButton]; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - - self.scrollView.frame = self.bounds; - - CGSize contentViewSize = CGSizeFlatted([self sizeThatContentViewFits]); - self.contentView.frame = CGRectFlatMake(0, CGRectGetMidY(self.scrollView.bounds) - contentViewSize.height / 2 + self.verticalOffset, contentViewSize.width, contentViewSize.height); - - self.scrollView.contentSize = CGSizeMake(fmaxf(CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.contentInset), contentViewSize.width), fmaxf(CGRectGetHeight(self.scrollView.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.contentInset), CGRectGetMaxY(self.contentView.frame))); - - CGFloat originY = 0; - - if (!self.imageView.hidden) { - [self.imageView sizeToFit]; - self.imageView.frame = CGRectSetXY(self.imageView.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.contentView.bounds, self.imageView.frame) + self.imageViewInsets.left - self.imageViewInsets.right, originY + self.imageViewInsets.top); - originY = CGRectGetMaxY(self.imageView.frame) + self.imageViewInsets.bottom; - } - - if (!self.loadingView.hidden) { - self.loadingView.frame = CGRectSetXY(self.loadingView.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.contentView.bounds, self.loadingView.frame) + self.loadingViewInsets.left - self.loadingViewInsets.right, originY + self.loadingViewInsets.top); - originY = CGRectGetMaxY(self.loadingView.frame) + self.loadingViewInsets.bottom; - } - - if (!self.textLabel.hidden) { - CGFloat labelWidth = CGRectGetWidth(self.contentView.bounds) - UIEdgeInsetsGetHorizontalValue(self.textLabelInsets); - CGSize labelSize = [self.textLabel sizeThatFits:CGSizeMake(labelWidth, CGFLOAT_MAX)]; - self.textLabel.frame = CGRectFlatMake(self.textLabelInsets.left, originY + self.textLabelInsets.top, labelWidth, labelSize.height); - originY = CGRectGetMaxY(self.textLabel.frame) + self.textLabelInsets.bottom; - } - - if (!self.detailTextLabel.hidden) { - CGFloat labelWidth = CGRectGetWidth(self.contentView.bounds) - UIEdgeInsetsGetHorizontalValue(self.detailTextLabelInsets); - CGSize labelSize = [self.detailTextLabel sizeThatFits:CGSizeMake(labelWidth, CGFLOAT_MAX)]; - self.detailTextLabel.frame = CGRectFlatMake(self.detailTextLabelInsets.left, originY + self.detailTextLabelInsets.top, labelWidth, labelSize.height); - originY = CGRectGetMaxY(self.detailTextLabel.frame) + self.detailTextLabelInsets.bottom; - } - - if (!self.actionButton.hidden) { - [self.actionButton sizeToFit]; - self.actionButton.frame = CGRectSetXY(self.actionButton.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.contentView.bounds, self.actionButton.frame) + self.actionButtonInsets.left - self.actionButtonInsets.right, originY + self.actionButtonInsets.top); - originY = CGRectGetMaxY(self.actionButton.frame) + self.actionButtonInsets.bottom; - } -} - -- (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) { - resultHeight += imageViewHeight; - } - if (!self.loadingView.hidden) { - resultHeight += loadingViewHeight; - } - if (!self.textLabel.hidden) { - resultHeight += textLabelHeight; - } - if (!self.detailTextLabel.hidden) { - resultHeight += detailTextLabelHeight; - } - if (!self.actionButton.hidden) { - resultHeight += actionButtonHeight; - } - - return CGSizeMake(resultWidth, resultHeight); -} - -- (void)updateDetailTextLabelWithText:(NSString *)text { - if (self.detailTextLabelFont && self.detailTextLabelTextColor && text) { - NSAttributedString *string = [[NSAttributedString alloc] initWithString:text attributes:@{ - NSFontAttributeName: self.detailTextLabelFont, - NSForegroundColorAttributeName: self.detailTextLabelTextColor, - NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:self.detailTextLabelFont.pointSize + 10 lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentCenter] - }]; - self.detailTextLabel.attributedText = string; - } - self.detailTextLabel.hidden = !text; - [self setNeedsLayout]; -} - -- (void)setLoadingView:(UIView *)loadingView { - if (self.loadingView != loadingView) { - [self.loadingView removeFromSuperview]; - _loadingView = loadingView; - [self.contentView addSubview:loadingView]; - } - [self setNeedsLayout]; -} - -- (void)setLoadingViewHidden:(BOOL)hidden { - self.loadingView.hidden = hidden; - if (!hidden && [self.loadingView respondsToSelector:@selector(startAnimating)]) { - [self.loadingView startAnimating]; - } - [self setNeedsLayout]; -} - -- (void)setImage:(UIImage *)image { - self.imageView.image = image; - self.imageView.hidden = !image; - [self setNeedsLayout]; -} - -- (void)setTextLabelText:(NSString *)text { - self.textLabel.text = text; - self.textLabel.hidden = !text; - [self setNeedsLayout]; -} - -- (void)setDetailTextLabelText:(NSString *)text { - [self updateDetailTextLabelWithText:text]; -} - -- (void)setActionButtonTitle:(NSString *)title { - [self.actionButton setTitle:title forState:UIControlStateNormal]; - self.actionButton.hidden = !title; - [self setNeedsLayout]; -} - -- (void)setImageViewInsets:(UIEdgeInsets)imageViewInsets { - _imageViewInsets = imageViewInsets; - [self setNeedsLayout]; -} - -- (void)setTextLabelInsets:(UIEdgeInsets)textLabelInsets { - _textLabelInsets = textLabelInsets; - [self setNeedsLayout]; -} - -- (void)setDetailTextLabelInsets:(UIEdgeInsets)detailTextLabelInsets { - _detailTextLabelInsets = detailTextLabelInsets; - [self setNeedsLayout]; -} - -- (void)setActionButtonInsets:(UIEdgeInsets)actionButtonInsets { - _actionButtonInsets = actionButtonInsets; - [self setNeedsLayout]; -} - -- (void)setVerticalOffset:(CGFloat)verticalOffset { - _verticalOffset = verticalOffset; - [self setNeedsLayout]; -} - -- (void)setTextLabelFont:(UIFont *)textLabelFont { - _textLabelFont = textLabelFont; - self.textLabel.font = textLabelFont; - [self setNeedsLayout]; -} - -- (void)setDetailTextLabelFont:(UIFont *)detailTextLabelFont { - _detailTextLabelFont = detailTextLabelFont; - [self updateDetailTextLabelWithText:self.detailTextLabel.text]; -} - -- (void)setActionButtonFont:(UIFont *)actionButtonFont { - _actionButtonFont = actionButtonFont; - self.actionButton.titleLabel.font = actionButtonFont; - [self setNeedsLayout]; -} - -- (void)setTextLabelTextColor:(UIColor *)textLabelTextColor { - _textLabelTextColor = textLabelTextColor; - self.textLabel.textColor = textLabelTextColor; -} - -- (void)setDetailTextLabelTextColor:(UIColor *)detailTextLabelTextColor { - _detailTextLabelTextColor = detailTextLabelTextColor; - [self updateDetailTextLabelWithText:self.detailTextLabel.text]; -} - -- (void)setActionButtonTitleColor:(UIColor *)actionButtonTitleColor { - _actionButtonTitleColor = actionButtonTitleColor; - [self.actionButton setTitleColor:actionButtonTitleColor forState:UIControlStateNormal]; -} - -@end - -@interface QMUIEmptyView (UIAppearance) - -@end - -@implementation QMUIEmptyView (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearance]; - }); -} - -+ (void)setDefaultAppearance { - QMUIEmptyView *appearance = [QMUIEmptyView appearance]; - appearance.imageViewInsets = UIEdgeInsetsMake(0, 0, 36, 0); - appearance.loadingViewInsets = UIEdgeInsetsMake(0, 0, 36, 0); - appearance.textLabelInsets = UIEdgeInsetsMake(0, 0, 10, 0); - appearance.detailTextLabelInsets = UIEdgeInsetsMake(0, 0, 10, 0); - appearance.actionButtonInsets = UIEdgeInsetsZero; - appearance.verticalOffset = -30; - - appearance.textLabelFont = UIFontMake(15); - appearance.detailTextLabelFont = UIFontMake(14); - appearance.actionButtonFont = UIFontMake(15); - - appearance.textLabelTextColor = UIColorMake(93, 100, 110); - appearance.detailTextLabelTextColor = UIColorMake(133, 140, 150); - appearance.actionButtonTitleColor = ButtonTintColor; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIFloatLayoutView.h b/QMUI/QMUIKit/UIComponents/QMUIFloatLayoutView.h deleted file mode 100644 index 953ad2ab..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIFloatLayoutView.h +++ /dev/null @@ -1,39 +0,0 @@ -// -// QMUIFloatLayoutView.h -// qmui -// -// Created by MoLice on 2016/11/10. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import - -/** - * 做类似 CSS 里的 float:left 的布局,自行使用 addSubview: 将子 View 添加进来即可。 - * - * 支持通过 `contentMode` 属性修改子 View 的对齐方式,目前仅支持 `UIViewContentModeLeft` 和 `UIViewContentModeRight`,默认为 `UIViewContentModeLeft`。 - */ -@interface QMUIFloatLayoutView : UIView - -/** - * QMUIFloatLayoutView 内部的间距,默认为 UIEdgeInsetsZero - */ -@property(nonatomic, assign) UIEdgeInsets padding; - -/** - * item 的最小宽高,默认为 CGSizeZero,也即不限制。 - */ -@property(nonatomic, assign) IBInspectable CGSize minimumItemSize; - -/** - * item 的最大宽高,默认为 CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX),也即不限制 - */ -@property(nonatomic, assign) IBInspectable CGSize maximumItemSize; - -/** - * item 之间的间距,默认为 UIEdgeInsetsZero。 - * - * @warning 上、下、左、右四个边缘的 item 布局时不会考虑 itemMargins.left/bottom/left/right。 - */ -@property(nonatomic, assign) UIEdgeInsets itemMargins; -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIFloatLayoutView.m b/QMUI/QMUIKit/UIComponents/QMUIFloatLayoutView.m deleted file mode 100644 index cddd83c1..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIFloatLayoutView.m +++ /dev/null @@ -1,111 +0,0 @@ -// -// QMUIFloatLayoutView.m -// qmui -// -// Created by MoLice on 2016/11/10. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIFloatLayoutView.h" -#import "QMUICore.h" - -#define ValueSwitchAlignLeftOrRight(valueLeft, valueRight) ([self shouldAlignRight] ? valueRight : valueLeft) - -@implementation QMUIFloatLayoutView - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - self.contentMode = UIViewContentModeLeft; - self.minimumItemSize = CGSizeZero; - self.maximumItemSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX); -} - -- (CGSize)sizeThatFits:(CGSize)size { - return [self layoutSubviewsWithSize:size shouldLayout:NO]; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - [self layoutSubviewsWithSize:self.bounds.size shouldLayout:YES]; -} - -- (CGSize)layoutSubviewsWithSize:(CGSize)size shouldLayout:(BOOL)shouldLayout { - NSArray *visibleItemViews = [self visibleSubviews]; - - if (visibleItemViews.count == 0) { - return CGSizeMake(UIEdgeInsetsGetHorizontalValue(self.padding), UIEdgeInsetsGetVerticalValue(self.padding)); - } - - // 如果是左对齐,则代表 item 左上角的坐标,如果是右对齐,则代表 item 右上角的坐标 - CGPoint itemViewOrigin = CGPointMake(ValueSwitchAlignLeftOrRight(self.padding.left, size.width - self.padding.right), self.padding.top); - CGFloat currentRowMaxY = itemViewOrigin.y; - - for (NSInteger i = 0, l = visibleItemViews.count; i < l; i ++) { - UIView *itemView = visibleItemViews[i]; - - CGSize itemViewSize = [itemView sizeThatFits:CGSizeMake(self.maximumItemSize.width, self.maximumItemSize.height)]; - itemViewSize.width = fmaxf(self.minimumItemSize.width, itemViewSize.width); - itemViewSize.height = fmaxf(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) { - // 换行,每一行第一个 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; - } 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)); - } - - currentRowMaxY = fmaxf(currentRowMaxY, itemViewOrigin.y + UIEdgeInsetsGetVerticalValue(self.itemMargins) + itemViewSize.height); - } - - // 最后一行不需要考虑 itemMarins.bottom,所以这里减掉 - currentRowMaxY -= self.itemMargins.bottom; - - CGSize resultSize = CGSizeMake(size.width, currentRowMaxY + self.padding.bottom); - return resultSize; -} - -- (NSArray *)visibleSubviews { - NSMutableArray *visibleItemViews = [[NSMutableArray alloc] init]; - - for (NSInteger i = 0, l = self.subviews.count; i < l; i++) { - UIView *itemView = self.subviews[i]; - if (!itemView.hidden) { - [visibleItemViews addObject:itemView]; - } - } - - return visibleItemViews; -} - -- (BOOL)shouldAlignRight { - return self.contentMode == UIViewContentModeRight; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIGridView.h b/QMUI/QMUIKit/UIComponents/QMUIGridView.h deleted file mode 100644 index 93b59f2d..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIGridView.h +++ /dev/null @@ -1,37 +0,0 @@ -// -// QMUIGridView.h -// qmui -// -// Created by MoLice on 15/1/30. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import - -/** - * 用于做九宫格布局,会将内部所有的 subview 根据指定的列数和行高,把每个 item(也即 subview) 拉伸到相同的大小。 - * - * 支持在 item 和 item 之间显示分隔线,分隔线支持虚线。 - * - * @warning 注意分隔线是占位的,把 item 隔开,而不是盖在某个 item 上。 - */ -@interface QMUIGridView : UIView - -/// 指定要显示的列数,默认为 0 -@property(nonatomic, assign) IBInspectable NSInteger columnCount; - -/// 指定每一行的高度,默认为 0 -@property(nonatomic, assign) IBInspectable CGFloat rowHeight; - -/// 指定 item 之间的分隔线宽度,默认为 0 -@property(nonatomic, assign) IBInspectable CGFloat separatorWidth; - -/// 指定 item 之间的分隔线颜色,默认为 UIColorSeparator -@property(nonatomic, strong) IBInspectable UIColor *separatorColor; - -/// item 之间的分隔线是否要用虚线显示,默认为 NO -@property(nonatomic, assign) IBInspectable BOOL separatorDashed; - -/// 候选的初始化方法,亦可通过 initWithFrame:、init 来初始化。 -- (instancetype)initWithColumn:(NSInteger)column rowHeight:(CGFloat)rowHeight; -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIImagePreviewViewController.h b/QMUI/QMUIKit/UIComponents/QMUIImagePreviewViewController.h deleted file mode 100644 index fa49161d..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIImagePreviewViewController.h +++ /dev/null @@ -1,62 +0,0 @@ -// -// QMUIImagePreviewViewController.h -// qmui -// -// Created by MoLice on 2016/11/30. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUICommonViewController.h" -#import "QMUIImagePreviewView.h" - -/** - * 图片预览控件,主要功能由内部自带的 QMUIImagePreviewView 提供,由于以 viewController 的形式存在,所以适用于那种在单独界面里展示图片,或者需要从某张目标图片的位置以动画的形式放大进入预览界面的场景。 - * - * 使用方式: - * - * 1. 使用 init 方法初始化 - * 2. 添加 imagePreviewView 的 delegate - * 3. 分两种查看方式: - * 1. 如果是左右 push 进入新界面查看图片,则直接按普通 UIViewController 的方式 push 即可; - * 2. 如果需要从指定图片位置以动画的形式放大进入预览,则调用 startPreviewFromRectInScreen:,传入一个 rect 即可开始预览,这种模式下会创建一个独立的 UIWindow 用于显示 QMUIImagePreviewViewController,所以可以达到盖住当前界面所有元素(包括顶部状态栏)的效果。 - * - * @see QMUIImagePreviewView - */ -@interface QMUIImagePreviewViewController : QMUICommonViewController - -@property(nonatomic, strong, readonly) QMUIImagePreviewView *imagePreviewView; -@property(nonatomic, strong) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; -@end - -/** - * 以 UIWindow 的形式来预览图片,优点是能盖住界面上所有元素(包括状态栏),缺点是无法进行 viewController 的界面切换(因为被 UIWindow 盖住了) - */ -@interface QMUIImagePreviewViewController (UIWindow) - -/** - * 从指定 rect 的位置以动画的形式进入预览 - * @param rect 在当前屏幕坐标系里的 rect,注意传进来的 rect 要做坐标系转换,例如:[view.superview convertRect:view.frame toView:nil] - */ -- (void)startPreviewFromRectInScreen:(CGRect)rect; - -/** - * 将当前图片缩放到指定 rect 的位置,然后退出预览 - * @param rect 在当前屏幕坐标系里的 rect,注意传进来的 rect 要做坐标系转换,例如:[view.superview convertRect:view.frame toView:nil] - */ -- (void)endPreviewToRectInScreen:(CGRect)rect; - -/** - * 以渐现的方式开始图片预览 - */ -- (void)startPreviewFading; - -/** - * 使用渐隐的动画退出图片预览 - */ -- (void)endPreviewFading; -@end - -@interface QMUIImagePreviewViewController (UIAppearance) - -+ (instancetype)appearance; -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIImagePreviewViewController.m b/QMUI/QMUIKit/UIComponents/QMUIImagePreviewViewController.m deleted file mode 100644 index 74fa018c..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIImagePreviewViewController.m +++ /dev/null @@ -1,234 +0,0 @@ -// -// QMUIImagePreviewViewController.m -// qmui -// -// Created by MoLice on 2016/11/30. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIImagePreviewViewController.h" -#import "QMUICore.h" - -@implementation QMUIImagePreviewViewController (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; - }); -} - -static QMUIImagePreviewViewController *imagePreviewViewControllerAppearance; -+ (instancetype)appearance { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - if (!imagePreviewViewControllerAppearance) { - imagePreviewViewControllerAppearance = [[QMUIImagePreviewViewController alloc] init]; - imagePreviewViewControllerAppearance.backgroundColor = UIColorBlack; - } - }); - return imagePreviewViewControllerAppearance; -} - -@end - -@interface QMUIImagePreviewViewController () - -@property(nonatomic, strong) UIWindow *previewWindow; -@property(nonatomic, assign) BOOL shouldStartWithFading; -@property(nonatomic, assign) CGRect previewFromRect; -@property(nonatomic, strong) UIImageView *transitionImageView; -@property(nonatomic, strong) UIColor *backgroundColorTemporarily; -@end - -@implementation QMUIImagePreviewViewController - -@synthesize imagePreviewView = _imagePreviewView; - -- (void)didInitialized { - [super didInitialized]; - self.automaticallyAdjustsScrollViewInsets = NO; - - if (imagePreviewViewControllerAppearance) { - self.backgroundColor = [QMUIImagePreviewViewController appearance].backgroundColor; - } -} - -- (QMUIImagePreviewView *)imagePreviewView { - [self loadViewIfNeeded]; - return _imagePreviewView; -} - -- (void)setBackgroundColor:(UIColor *)backgroundColor { - _backgroundColor = backgroundColor; - if ([self isViewLoaded]) { - self.view.backgroundColor = backgroundColor; - } -} - -- (void)viewDidLoad { - [super viewDidLoad]; - self.view.backgroundColor = self.backgroundColor; -} - -- (void)initSubviews { - [super initSubviews]; - _imagePreviewView = [[QMUIImagePreviewView alloc] initWithFrame:self.view.bounds]; - [self.view addSubview:self.imagePreviewView]; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - self.imagePreviewView.frame = self.view.bounds; -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - - [self.imagePreviewView.collectionView reloadData]; - - if (self.previewWindow && !self.shouldStartWithFading) { - // 为在 viewDidAppear 做动画做准备 - self.imagePreviewView.collectionView.hidden = YES; - } else { - self.imagePreviewView.collectionView.hidden = NO; - } -} - -- (void)viewDidAppear:(BOOL)animated { - [super viewDidAppear:animated]; - - // 配合 QMUIImagePreviewViewController (UIWindow) 使用的 - if (self.previewWindow) { - - if (self.shouldStartWithFading) { - [UIView animateWithDuration:.2 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - self.view.alpha = 1; - } completion:^(BOOL finished) { - self.imagePreviewView.collectionView.hidden = NO; - self.shouldStartWithFading = NO; - }]; - return; - } - - QMUIZoomImageView *zoomImageView = [self.imagePreviewView zoomImageViewAtIndex:self.imagePreviewView.currentImageIndex]; - if (!zoomImageView) { - NSAssert(NO, @"第 %@ 个 zoomImageView 不存在,可能当前还处于非可视区域", @(self.imagePreviewView.currentImageIndex)); - } - CGRect transitionFromRect = self.previewFromRect; - CGRect transitionToRect = [self.view convertRect:[zoomImageView imageViewRectInZoomImageView] fromView:zoomImageView.superview]; - - self.transitionImageView.contentMode = zoomImageView.imageView.contentMode; - self.transitionImageView.image = zoomImageView.imageView.image; - self.transitionImageView.frame = transitionFromRect; - [self.view addSubview:self.transitionImageView]; - - [UIView animateWithDuration:.2 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - self.transitionImageView.frame = transitionToRect; - self.view.backgroundColor = self.backgroundColorTemporarily; - } completion:^(BOOL finished) { - [self.transitionImageView removeFromSuperview]; - self.imagePreviewView.collectionView.hidden = NO; - self.backgroundColorTemporarily = nil; - }]; - } -} - -@end - -@implementation QMUIImagePreviewViewController (UIWindow) - -- (void)startPreviewFromRectInScreen:(CGRect)rect { - [self startPreviewWithFadingAnimation:NO orFromRect:rect]; -} - -- (void)endPreviewToRectInScreen:(CGRect)rect { - [self endPreviewWithFadingAnimation:NO orToRect:rect]; -} - -- (void)startPreviewFading { - [self startPreviewWithFadingAnimation:YES orFromRect:CGRectZero]; -} - -- (void)endPreviewFading { - [self endPreviewWithFadingAnimation:YES orToRect:CGRectZero]; -} - -#pragma mark - 动画 - -- (void)initPreviewWindowIfNeeded { - if (!self.previewWindow) { - self.previewWindow = [[UIWindow alloc] init]; - self.previewWindow.windowLevel = UIWindowLevelQMUIImagePreviewView; - self.previewWindow.backgroundColor = UIColorClear; - } -} - -- (void)removePreviewWindow { - self.previewWindow.hidden = YES; - self.previewWindow.rootViewController = nil; - self.previewWindow = nil; -} - -- (void)startPreviewWithFadingAnimation:(BOOL)isFading orFromRect:(CGRect)rect { - self.shouldStartWithFading = isFading; - - if (isFading) { - - // 为动画做准备,先置为透明 - self.view.alpha = 0; - - } else { - self.previewFromRect = rect; - - if (!self.transitionImageView) { - self.transitionImageView = [[UIImageView alloc] init]; - } - - // 为动画做准备,先置为透明 - self.backgroundColorTemporarily = self.view.backgroundColor; - self.view.backgroundColor = UIColorClear; - } - - [self initPreviewWindowIfNeeded]; - - self.previewWindow.rootViewController = self; - self.previewWindow.hidden = NO; -} - -- (void)endPreviewWithFadingAnimation:(BOOL)isFading orToRect:(CGRect)rect { - - if (isFading) { - [UIView animateWithDuration:.2 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - self.view.alpha = 0; - } completion:^(BOOL finished) { - [self removePreviewWindow]; - self.view.alpha = 1; - }]; - return; - } - - QMUIZoomImageView *zoomImageView = [self.imagePreviewView zoomImageViewAtIndex:self.imagePreviewView.currentImageIndex]; - CGRect transitionFromRect = [zoomImageView imageViewRectInZoomImageView]; - CGRect transitionToRect = rect; - - self.transitionImageView.image = zoomImageView.image; - self.transitionImageView.frame = transitionFromRect; - [self.view addSubview:self.transitionImageView]; - self.imagePreviewView.collectionView.hidden = YES; - - self.backgroundColorTemporarily = self.view.backgroundColor; - - [UIView animateWithDuration:.2 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - self.transitionImageView.frame = transitionToRect; - self.view.backgroundColor = UIColorClear; - } completion:^(BOOL finished) { - [self removePreviewWindow]; - [self.transitionImageView removeFromSuperview]; - self.imagePreviewView.collectionView.hidden = NO; - self.view.backgroundColor = self.backgroundColorTemporarily; - self.backgroundColorTemporarily = nil; - }]; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIKeyboardManager.h b/QMUI/QMUIKit/UIComponents/QMUIKeyboardManager.h deleted file mode 100644 index b4d62bc6..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIKeyboardManager.h +++ /dev/null @@ -1,212 +0,0 @@ -// -// QMUIKeyboardManager.h -// qmui -// -// Created by zhoonchen on 2017/3/23. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import -#import - -@protocol QMUIKeyboardManagerDelegate; -@class QMUIKeyboardUserInfo; - - -/** - * `QMUIKeyboardManager` 提供了方便管理键盘事件的方案,使用的场景是需要跟随键盘的显示或者隐藏来更改界面的 UI,例如输入框跟随在键盘的顶部。 - * 由于键盘通知是整个 App 全局的,所以经常会遇到 A 的键盘监听回调里接收到 B 的键盘事件,这样的情况往往不是我们想要的,即使可以通过判断当前的 firstResponder 来区分,但还是不能完美的解决问题或者有时候解决起来非常麻烦。`QMUIKeyboardManager` 通过 `delegateEnabled` 和 `targetResponder` 等属性来方便地控制 firstResponder,从而可以实现某个键盘监听回调方法只响应某个 UIResponder 或者某几个 UIResponder 触发的键盘通知。 - * 使用方式: - * 1. 使用 initWithDelegate: 方法初始化 - * 2. 通过 addTargetResponder: 的方式将要监听的输入框添加进来 - * 3. 在 delegate 方法里(一般用 keyboardWillChangeFrameWithUserInfo:)处理键盘位置变化时的布局 - * - * 另外 QMUIKeyboardManager 同时集成在了 UITextField(QMUI) 和 UITextView(QMUI) 里,具体请查看对应文件。 - * @see UITextField(QMUI) - * @see UITextView(QMUI) - */ -@interface QMUIKeyboardManager : NSObject - -/** - * 指定初始化方法,以 delegate 的方式将键盘事件传递给监听者 - */ -- (instancetype)initWithDelegate:(id)delegate NS_DESIGNATED_INITIALIZER; - -/** - * 获取当前的 delegate - */ -@property(nonatomic, weak, readonly) id delegate; - -/** - * 是否允许触发delegate的回调,某些场景可能要主动停止对键盘事件的响应。 - * 默认为 YES。 - */ -@property(nonatomic, assign) BOOL delegateEnabled; - -/** - * 添加触发键盘事件的 UIResponder,一般是 UITextView 或者 UITextField ,不添加 targetResponder 的话,则默认接受任何 UIResponder 产生的键盘通知。 - * 添加成功将会返回YES,否则返回NO。 - */ -- (BOOL)addTargetResponder:(UIResponder *)targetResponder; - -/** - * 获取当前所有的 target UIResponder,若不存在则返回 nil - */ -- (NSArray *)allTargetResponders; - -/** - * 把键盘的rect转为相对于view的rect。一般用来把键盘的rect转化为相对于当前 self.view 的 rect,然后获取 y 值来布局对应的 view(这里一般不要获取键盘的高度,因为对于iPad的键盘,浮动状态下键盘的高度往往不是我们想要的)。 - * @param rect 键盘的rect,一般拿 keyboardUserInfo.endFrame - * @param view 一个特定的view或者window,如果传入nil则相对有当前的 mainWindow - */ -+ (CGRect)convertKeyboardRect:(CGRect)rect toView:(UIView *)view; - -/** - * 获取键盘到顶部到相对于view底部的距离,这个值在某些情况下会等于endFrame.size.height或者visiableKeyboardHeight,不过在iPad浮动键盘的时候就包括了底部的空隙。所以建议使用这个方法。 - */ -+ (CGFloat)distanceFromMinYToBottomInView:(UIView *)view keyboardRect:(CGRect)rect; - -/** - * 根据键盘的动画参数自己构建一个动画,调用者只需要设置view的位置即可 - */ -+ (void)animateWithAnimated:(BOOL)animated keyboardUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion; - -/** - * 这个方法特殊处理 iPad Pro 外接键盘的情况。使用外接键盘在完全不显示键盘的时候,不会调用willShow的通知,所以导致一些通过willShow回调来显示targetResponder的场景(例如微信朋友圈的评论输入框)无法把targetResponder正常的显示出来。通过这个方法,你只需要关心你的show和hide的状态就好了,不需要关心是否 iPad Pro 的情况。 - * @param showBlock 键盘显示回调的block,不能把showBlock理解为系统的show通知,而是你有输入框聚焦了并且期望键盘显示出来。 - * @param hideBlock 键盘隐藏回调的block,不能把hideBlock理解为系统的hide通知,而是键盘即将消失在界面上并且你期望跟随键盘变化的UI回到默认状态。 - */ -+ (void)handleKeyboardNotificationWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo showBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))showBlock hideBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))hideBlock; - -/** - * 键盘面板的私有view,可能为nil - */ -+ (UIView *)keyboardView; - -/** - * 键盘面板所在的私有window,可能为nil - */ -+ (UIWindow *)keyboardWindow; - -/** - * 是否有键盘在显示 - */ -+ (BOOL)isKeyboardVisible; - -/** - * 当期那键盘相对于屏幕的frame - */ -+ (CGRect)currentKeyboardFrame; - -/** - * 当前键盘高度键盘的可见高度 - */ -+ (CGFloat)visiableKeyboardHeight; - -@end - - -@interface QMUIKeyboardUserInfo : NSObject - -/** - * 所在的KeyboardManager - */ -@property(nonatomic, weak, readonly) QMUIKeyboardManager *keyboardManager; - -/** - * 当前键盘的notification - */ -@property(nonatomic, strong, readonly) NSNotification *notification; - -/** - * notification自带的userInfo - */ -@property(nonatomic, strong, readonly) NSDictionary *originUserInfo; - -/** - * 触发键盘事件的UIResponder,注意这里的 `targetResponder` 不一定是通过 `addTargetResponder:` 添加的 UIResponder,而是当前触发键盘事件的 UIResponder。 - */ -@property(nonatomic, weak, readonly) UIResponder *targetResponder; - -/** - * 获取键盘实际宽度 - */ -@property(nonatomic, assign, readonly) CGFloat width; - -/** - * 获取键盘的实际高度 - */ -@property(nonatomic, assign, readonly) CGFloat height; - -/** - * 获取当前键盘在view上的可见高度,也就是键盘和view重叠的高度。如果view=nil,则直接返回键盘的实际高度。 - */ -- (CGFloat)heightInView:(UIView *)view; - -/** - * 获取键盘beginFrame - */ -@property(nonatomic, assign, readonly) CGRect beginFrame; - -/** - * 获取键盘endFrame - */ -@property(nonatomic, assign, readonly) CGRect endFrame; - -/** - * 获取键盘出现动画的duration,对于第三方键盘,这个值有可能为0 - */ -@property(nonatomic, assign, readonly) NSTimeInterval animationDuration; - -/** - * 获取键盘动画的Curve参数 - */ -@property(nonatomic, assign, readonly) UIViewAnimationCurve animationCurve; - -/** - * 获取键盘动画的Options参数 - */ -@property(nonatomic, assign, readonly) UIViewAnimationOptions animationOptions; - -@end - - -/** - * `QMUIKeyboardManagerDelegate`里面的方法是对应系统键盘通知的回调方法,具体请看delegate名字,`QMUIKeyboardUserInfo`是对系统的userInfo做了一个封装,可以方便的获取userInfo的属性值。 - */ -@protocol QMUIKeyboardManagerDelegate - -@optional - -/** - * 键盘即将显示 - */ -- (void)keyboardWillShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; - -/** - * 键盘即将隐藏 - */ -- (void)keyboardWillHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; - -/** - * 键盘frame即将发生变化。 - * 这个delegate除了对应系统的willChangeFrame通知外,在iPad下还增加了监听键盘frame变化的KVO来处理浮动键盘,所以调用次数会比系统默认多。需要让界面或者某个view跟随键盘运动,建议在这个通知delegate里面实现,因为willShow和willHide在手机上是准确的,但是在iPad的浮动键盘下是不准确的。另外,如果不需要跟随浮动键盘运动,那么在逻辑代码里面可以通过判断键盘的位置来过滤这种浮动的情况。 - */ -- (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; - -/** - * 键盘已经显示 - */ -- (void)keyboardDidShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; - -/** - * 键盘已经隐藏 - */ -- (void)keyboardDidHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; - -/** - * 键盘frame已经发生变化。 - */ -- (void)keyboardDidChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo; - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIKeyboardManager.m b/QMUI/QMUIKit/UIComponents/QMUIKeyboardManager.m deleted file mode 100644 index 1039d771..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIKeyboardManager.m +++ /dev/null @@ -1,822 +0,0 @@ -// -// QMUIKeyboardManager.m -// qmui -// -// Created by zhoonchen on 2017/3/23. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import "QMUIKeyboardManager.h" -#import "QMUICore.h" - - -@interface UIView (KeyboardManager) - -- (id)qmui_findFirstResponder; - -@end - -@implementation UIView (KeyboardManager) - -- (id)qmui_findFirstResponder { - if (self.isFirstResponder) { - return self; - } - for (UIView *subView in self.subviews) { - id responder = [subView qmui_findFirstResponder]; - if (responder) return responder; - } - return nil; -} - -@end - - -@interface UIResponder (KeyboardManager) - -// 系统自己的isFirstResponder有延迟,这里手动记录UIResponder是否isFirstResponder -@property(nonatomic, assign) BOOL keyboardManager_isFirstResponder; - -@end - -@implementation UIResponder (KeyboardManager) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - ReplaceMethod([self class], @selector(becomeFirstResponder), @selector(keyboardManager_becomeFirstResponder)); - ReplaceMethod([self class], @selector(resignFirstResponder), @selector(keyboardManager_resignFirstResponder)); - }); -} - -- (BOOL)keyboardManager_becomeFirstResponder { - self.keyboardManager_isFirstResponder = YES; - return [self keyboardManager_becomeFirstResponder]; -} - -- (BOOL)keyboardManager_resignFirstResponder { - self.keyboardManager_isFirstResponder = NO; - return [self keyboardManager_resignFirstResponder]; -} - -- (void)setKeyboardManager_isFirstResponder:(BOOL)keyboardManager_isFirstResponder { - objc_setAssociatedObject(self, @selector(keyboardManager_isFirstResponder), @(keyboardManager_isFirstResponder), OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (BOOL)keyboardManager_isFirstResponder { - return [objc_getAssociatedObject(self, _cmd) boolValue]; -} - -@end - - -@interface QMUIKeyboardUserInfo () - -@property(nonatomic, weak, readwrite) QMUIKeyboardManager *keyboardManager; -@property(nonatomic, strong, readwrite) NSNotification *notification; -@property(nonatomic, weak, readwrite) UIResponder *targetResponder; -@property(nonatomic, assign) BOOL isTargetResponderFocused; - -@property(nonatomic, assign, readwrite) CGFloat width; -@property(nonatomic, assign, readwrite) CGFloat height; - -@property(nonatomic, assign, readwrite) CGRect beginFrame; -@property(nonatomic, assign, readwrite) CGRect endFrame; - -@property(nonatomic, assign, readwrite) NSTimeInterval animationDuration; -@property(nonatomic, assign, readwrite) UIViewAnimationCurve animationCurve; -@property(nonatomic, assign, readwrite) UIViewAnimationOptions animationOptions; - -@end - -@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]; - } -} - -- (void)setTargetResponder:(UIResponder *)targetResponder { - _targetResponder = targetResponder; - self.isTargetResponderFocused = targetResponder && targetResponder.keyboardManager_isFirstResponder; -} - -- (NSDictionary *)originUserInfo { - return self.notification ? self.notification.userInfo : nil; -} - -- (CGFloat)width { - CGRect keyboardRect = [QMUIKeyboardManager convertKeyboardRect:_endFrame toView:nil]; - return keyboardRect.size.width; -} - -- (CGFloat)height { - CGRect keyboardRect = [QMUIKeyboardManager convertKeyboardRect:_endFrame toView:nil]; - return keyboardRect.size.height; -} - -- (CGFloat)heightInView:(UIView *)view { - if (!view) { - return [self height]; - } - CGRect keyboardRect = [QMUIKeyboardManager convertKeyboardRect:_endFrame toView:view]; - CGRect visiableRect = CGRectIntersection(view.bounds, keyboardRect); - if (CGRectIsNull(visiableRect)) { - return 0; - } - return visiableRect.size.height; -} - -- (CGRect)beginFrame { - return _beginFrame; -} - -- (CGRect)endFrame { - return _endFrame; -} - -- (NSTimeInterval)animationDuration { - return _animationDuration; -} - -- (UIViewAnimationCurve)animationCurve { - return _animationCurve; -} - -- (UIViewAnimationOptions)animationOptions { - return _animationOptions; -} - -@end - - -@interface QMUIKeyboardViewFrameObserver : NSObject - -@property (nonatomic, copy) void (^keyboardViewChangeFrameBlock)(UIView *keyboardView); -- (void)addToKeyboardView:(UIView *)keyboardView; -+ (instancetype)observerForView:(UIView *)keyboardView; - -@end - -static char kAssociatedObjectKey_KeyboardViewFrameObserver; - -@implementation QMUIKeyboardViewFrameObserver { - __unsafe_unretained UIView *_keyboardView; -} - -- (void)addToKeyboardView:(UIView *)keyboardView { - if (_keyboardView == keyboardView) { - return; - } - if (_keyboardView) { - [self removeFrameObserver]; - objc_setAssociatedObject(_keyboardView, &kAssociatedObjectKey_KeyboardViewFrameObserver, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - _keyboardView = keyboardView; - if (keyboardView) { - [self addFrameObserver]; - } - objc_setAssociatedObject(keyboardView, &kAssociatedObjectKey_KeyboardViewFrameObserver, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (void)addFrameObserver { - if (!_keyboardView) { - return; - } - [_keyboardView addObserver:self forKeyPath:@"frame" options:kNilOptions context:NULL]; - [_keyboardView addObserver:self forKeyPath:@"center" options:kNilOptions context:NULL]; - [_keyboardView addObserver:self forKeyPath:@"bounds" options:kNilOptions context:NULL]; - [_keyboardView addObserver:self forKeyPath:@"transform" options:kNilOptions context:NULL]; -} - -- (void)removeFrameObserver { - [_keyboardView removeObserver:self forKeyPath:@"frame"]; - [_keyboardView removeObserver:self forKeyPath:@"center"]; - [_keyboardView removeObserver:self forKeyPath:@"bounds"]; - [_keyboardView removeObserver:self forKeyPath:@"transform"]; - _keyboardView = nil; -} - -- (void)dealloc { - [self removeFrameObserver]; -} - -+ (instancetype)observerForView:(UIView *)keyboardView { - if (!keyboardView) { - return nil; - } - return objc_getAssociatedObject(keyboardView, &kAssociatedObjectKey_KeyboardViewFrameObserver); -} - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if (![keyPath isEqualToString:@"frame"] && - ![keyPath isEqualToString:@"center"] && - ![keyPath isEqualToString:@"bounds"] && - ![keyPath isEqualToString:@"transform"]) { - return; - } - if ([[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue]) { - return; - } - if ([[change objectForKey:NSKeyValueChangeKindKey] integerValue] != NSKeyValueChangeSetting) { - return; - } - id newValue = [change objectForKey:NSKeyValueChangeNewKey]; - if (newValue == [NSNull null]) { newValue = nil; } - if (self.keyboardViewChangeFrameBlock) { - self.keyboardViewChangeFrameBlock(_keyboardView); - } -} - -@end - - -@interface QMUIKeyboardManager () - -@property(nonatomic, strong) NSMutableArray *targetResponderValues; - -@property(nonatomic, strong) QMUIKeyboardUserInfo *keyboardMoveUserInfo; -@property(nonatomic, assign) CGRect keyboardMoveBeginRect; - -@end - -static UIResponder *kCurrentResponder = nil; - -@implementation QMUIKeyboardManager - -// 1、系统键盘app启动第一次使用键盘的时候,会调用两轮键盘通知事件,之后就只会调用一次。而搜狗等第三方输入法的键盘,目前发现每次都会调用三次键盘通知事件。总之,键盘的通知事件是不确定的。 - -// 2、搜狗键盘可以修改键盘的高度,在修改键盘高度之后,会调用键盘的keyboardWillChangeFrameNotification和keyboardWillShowNotification通知。 - -// 3、如果从一个聚焦的输入框直接聚焦到另一个输入框,会调用前一个输入框的keyboardWillChangeFrameNotification,在调用后一个输入框的keyboardWillChangeFrameNotification,最后调用后一个输入框的keyboardWillShowNotification(如果此时是浮动键盘,那么后一个输入框的keyboardWillShowNotification不会被调用;)。 - -// 4、iPad可以变成浮动键盘,固定->浮动:会调用keyboardWillChangeFrameNotification和keyboardWillHideNotification;浮动->固定:会调用keyboardWillChangeFrameNotification和keyboardWillShowNotification;浮动键盘在移动的时候只会调用keyboardWillChangeFrameNotification通知,并且endFrame为zero,fromFrame不为zero,而是移动前键盘的frame。浮动键盘在聚焦和失焦的时候只会调用keyboardWillChangeFrameNotification,不会调用show和hide的notification。 - -// 5、iPad可以拆分为左右的小键盘,小键盘的通知具体基本跟浮动键盘一样。 - -// 6、iPad可以外接键盘,外接键盘之后屏幕上就没有虚拟键盘了,但是当我们输入文字的时候,发现底部还是有一条灰色的候选词,条东西也是键盘,它也会触发跟虚拟键盘一样的通知事件。如果点击这条候选词右边的向下箭头,则可以完全隐藏虚拟键盘,这个时候如果失焦再聚焦发现还是没有这条候选词,也就是键盘完全不出来了,如果输入文字,候选词才会重新出来。总结来说就是这条候选词是可以关闭的,关闭之后只有当下次输入才会重新出现。(聚焦和失焦都只调用keyboardWillChangeFrameNotification和keyboardWillHideNotification通知,而且frame始终不变,都是在屏幕下面) - -// 7、iOS8 hide 之后高度变成0了,keyboardWillHideNotification还是正常的,所以建议不要使用键盘高度来做动画,而是用键盘的y值;在show和hide的时候endFrame会出现一些奇怪的中间值,最终值是对的;两个输入框切换聚焦,iOS8不会触发任何键盘通知;iOS8的浮动切换正常; - -// 8、iOS8在 固定->浮动 的过程中,后面的keyboardWillChangeFrameNotification和keyboardWillHideNotification里面的endFrame是正确的,而iOS10和iOS9是错的,iOS9的y值是键盘的MaxY,而iOS10的y值是隐藏状态下的y,也就是屏幕高度。所以iOS9和iOS10需要在keyboardDidChangeFrameNotification里面重新刷新一下。 - -- (instancetype)init { - NSAssert(NO, @"请使用initWithDelegate:初始化"); - return [self initWithDelegate:nil]; -} - -- (instancetype)initWithCoder:(NSCoder *)coder { - NSAssert(NO, @"请使用initWithDelegate:初始化"); - return [self initWithDelegate:nil]; -} - -- (instancetype)initWithDelegate:(id )delegate { - if (self = [super init]) { - _delegate = delegate; - _delegateEnabled = YES; - _targetResponderValues = [[NSMutableArray alloc] init]; - [self addKeyboardNotification]; - } - return self; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (BOOL)addTargetResponder:(UIResponder *)targetResponder { - if (!targetResponder || ![targetResponder isKindOfClass:[UIResponder class]]) { - return NO; - } - [self.targetResponderValues addObject:[self packageTargetResponder:targetResponder]]; - return YES; -} - -- (NSArray *)allTargetResponders { - NSMutableArray *targetResponders = nil; - for (int i = 0; i < self.targetResponderValues.count; i++) { - if (!targetResponders) { - targetResponders = [[NSMutableArray alloc] init]; - } - id unPackageValue = [self unPackageTargetResponder:self.targetResponderValues[i]]; - if (unPackageValue && [unPackageValue isKindOfClass:[UIResponder class]]) { - [targetResponders addObject:(UIResponder *)unPackageValue]; - } - } - return [targetResponders copy]; -} - -- (NSValue *)packageTargetResponder:(UIResponder *)targetResponder { - if (![targetResponder isKindOfClass:[UIResponder class]]) { - return nil; - } - return [NSValue valueWithNonretainedObject:targetResponder]; -} - -- (UIResponder *)unPackageTargetResponder:(NSValue *)value { - if (!value) { - return nil; - } - id unPackageValue = [value nonretainedObjectValue]; - if (![unPackageValue isKindOfClass:[UIResponder class]]) { - return nil; - } - return (UIResponder *)unPackageValue; -} - -#pragma mark - Notification - -- (void)addKeyboardNotification { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShowNotification:) name:UIKeyboardDidShowNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHideNotification:) name:UIKeyboardDidHideNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrameNotification:) name:UIKeyboardWillChangeFrameNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidChangeFrameNotification:) name:UIKeyboardDidChangeFrameNotification object:nil]; -} - -- (void)keyboardWillShowNotification:(NSNotification *)notification { - - NSLog(@"keyboardWillShowNotification - %@", self); - NSLog(@"\n"); - - if (![self shouldReceiveShowNotification]) { - return; - } - - QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; - userInfo.targetResponder = kCurrentResponder ?: nil; - - if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardWillShowWithUserInfo:)]) { - [self.delegate keyboardWillShowWithUserInfo:userInfo]; - } - - // 额外处理iPad浮动键盘 - if (IS_IPAD) { - self.keyboardMoveUserInfo = userInfo; - [self keyboardDidChangedFrame:[self.class keyboardView]]; - } -} - -- (void)keyboardDidShowNotification:(NSNotification *)notification { - - NSLog(@"keyboardDidShowNotification - %@", self); - NSLog(@"\n"); - - QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; - userInfo.targetResponder = kCurrentResponder ?: nil; - - id firstResponder = [[UIApplication sharedApplication].keyWindow qmui_findFirstResponder]; - BOOL shouldReceiveDidShowNotification = self.targetResponderValues.count <= 0 || (firstResponder && firstResponder == kCurrentResponder); - - if (shouldReceiveDidShowNotification) { - - if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardDidShowWithUserInfo:)]) { - [self.delegate keyboardDidShowWithUserInfo:nil]; - } - - // 额外处理iPad浮动键盘 - if (IS_IPAD) { - self.keyboardMoveUserInfo = userInfo; - [self keyboardDidChangedFrame:[self.class keyboardView]]; - } - } -} - -- (void)keyboardWillHideNotification:(NSNotification *)notification { - - NSLog(@"keyboardWillHideNotification - %@", self); - NSLog(@"\n"); - - if (![self shouldReceiveHideNotification]) { - return; - } - - QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; - userInfo.targetResponder = kCurrentResponder ?: nil; - - if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardWillHideWithUserInfo:)]) { - [self.delegate keyboardWillHideWithUserInfo:userInfo]; - } - - // 额外处理iPad浮动键盘 - if (IS_IPAD) { - self.keyboardMoveUserInfo = userInfo; - [self keyboardDidChangedFrame:[self.class keyboardView]]; - } -} - -- (void)keyboardDidHideNotification:(NSNotification *)notification { - - NSLog(@"keyboardDidHideNotification - %@", self); - NSLog(@"\n"); - - QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; - userInfo.targetResponder = kCurrentResponder ?: nil; - - if ([self shouldReceiveHideNotification]) { - if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardDidHideWithUserInfo:)]) { - [self.delegate keyboardDidHideWithUserInfo:userInfo]; - } - } - - if (kCurrentResponder && - !kCurrentResponder.keyboardManager_isFirstResponder && - !IS_IPAD) { - kCurrentResponder = nil; - } - - // 额外处理iPad浮动键盘 - if (IS_IPAD) { - if (self.targetResponderValues.count <= 0 || kCurrentResponder) { - self.keyboardMoveUserInfo = userInfo; - [self keyboardDidChangedFrame:[self.class keyboardView]]; - } - } -} - -- (void)keyboardWillChangeFrameNotification:(NSNotification *)notification { - - NSLog(@"keyboardWillChangeFrameNotification - %@", self); - NSLog(@"\n"); - - QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; - - if ([self shouldReceiveShowNotification]) { - userInfo.targetResponder = kCurrentResponder ?: nil; - } else if ([self shouldReceiveHideNotification]) { - userInfo.targetResponder = kCurrentResponder ?: nil; - } else { - return; - } - - if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardWillChangeFrameWithUserInfo:)]) { - [self.delegate keyboardWillChangeFrameWithUserInfo:userInfo]; - } - - // 额外处理iPad浮动键盘 - if (IS_IPAD) { - self.keyboardMoveUserInfo = userInfo; - [self addFrameObserverIfNeeded]; - } -} - -- (void)keyboardDidChangeFrameNotification:(NSNotification *)notification { - - NSLog(@"keyboardDidChangeFrameNotification - %@", self); - NSLog(@"\n"); - - QMUIKeyboardUserInfo *userInfo = [self newUserInfoWithNotification:notification]; - - if ([self shouldReceiveShowNotification]) { - userInfo.targetResponder = kCurrentResponder ?: nil; - } else if ([self shouldReceiveHideNotification]) { - userInfo.targetResponder = kCurrentResponder ?: nil; - } else { - return; - } - - if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardDidChangeFrameWithUserInfo:)]) { - [self.delegate keyboardDidChangeFrameWithUserInfo:userInfo]; - } - - // 额外处理iPad浮动键盘 - if (IS_IPAD) { - self.keyboardMoveUserInfo = userInfo; - [self keyboardDidChangedFrame:[self.class keyboardView]]; - } -} - -- (QMUIKeyboardUserInfo *)newUserInfoWithNotification:(NSNotification *)notification { - QMUIKeyboardUserInfo *userInfo = [[QMUIKeyboardUserInfo alloc] init]; - userInfo.keyboardManager = self; - userInfo.notification = notification; - return userInfo; -} - -- (BOOL)shouldReceiveShowNotification { - - kCurrentResponder = [[UIApplication sharedApplication].keyWindow qmui_findFirstResponder]; - - if (self.targetResponderValues.count <= 0) { - return YES; - } else { - return kCurrentResponder && [self.targetResponderValues containsObject:[self packageTargetResponder:kCurrentResponder]]; - } -} - -- (BOOL)shouldReceiveHideNotification { - if (self.targetResponderValues.count <= 0) { - return YES; - } else { - if (kCurrentResponder) { - return [self.targetResponderValues containsObject:[self packageTargetResponder:kCurrentResponder]]; - } else { - return NO; - } - } -} - -#pragma mark - iPad浮动键盘 - -- (void)addFrameObserverIfNeeded { - if (![self.class keyboardView]) { - return; - } - __weak __typeof(self)weakSelf = self; - QMUIKeyboardViewFrameObserver *observer = [QMUIKeyboardViewFrameObserver observerForView:[self.class keyboardView]]; - if (!observer) { - observer = [[QMUIKeyboardViewFrameObserver alloc] init]; - observer.keyboardViewChangeFrameBlock = ^(UIView *keyboardView) { - [weakSelf keyboardDidChangedFrame:keyboardView]; - }; - [observer addToKeyboardView:[self.class keyboardView]]; - // 手动调用第一次 - [self keyboardDidChangedFrame:[self.class keyboardView]]; - } -} - -- (void)keyboardDidChangedFrame:(UIView *)keyboardView { - - if (keyboardView != [self.class keyboardView]) { - return; - } - - // 也需要判断targetResponder - if (![self shouldReceiveShowNotification] && ![self shouldReceiveHideNotification]) { - return; - } - - if (self.delegateEnabled && [self.delegate respondsToSelector:@selector(keyboardWillChangeFrameWithUserInfo:)]) { - - UIWindow *keyboardWindow = keyboardView.window; - - if (self.keyboardMoveBeginRect.size.width == 0 && self.keyboardMoveBeginRect.size.height == 0) { - // 第一次需要初始化 - self.keyboardMoveBeginRect = CGRectMake(0, keyboardWindow.bounds.size.height, keyboardWindow.bounds.size.width, 0); - } - - CGRect endFrame = CGRectZero; - if (keyboardWindow) { - endFrame = [keyboardWindow convertRect:keyboardView.frame toWindow:nil]; - } else { - endFrame = keyboardView.frame; - } - - // 自己构造一个QMUIKeyboardUserInfo,一些属性使用之前最后一个keyboardUserInfo的值 - QMUIKeyboardUserInfo *keyboardMoveUserInfo = [[QMUIKeyboardUserInfo alloc] init]; - keyboardMoveUserInfo.keyboardManager = self; - keyboardMoveUserInfo.targetResponder = self.keyboardMoveUserInfo ? self.keyboardMoveUserInfo.targetResponder : nil; - keyboardMoveUserInfo.animationDuration = self.keyboardMoveUserInfo ? self.keyboardMoveUserInfo.animationDuration : 0.25; - keyboardMoveUserInfo.animationCurve = self.keyboardMoveUserInfo ? self.keyboardMoveUserInfo.animationCurve : 7; - keyboardMoveUserInfo.animationOptions = self.keyboardMoveUserInfo ? self.keyboardMoveUserInfo.animationOptions : keyboardMoveUserInfo.animationCurve<<16; - keyboardMoveUserInfo.beginFrame = self.keyboardMoveBeginRect; - keyboardMoveUserInfo.endFrame = endFrame; - - NSLog(@"keyboardDidMoveNotification - %@", self); - NSLog(@"\n"); - - [self.delegate keyboardWillChangeFrameWithUserInfo:keyboardMoveUserInfo]; - - self.keyboardMoveBeginRect = endFrame; - - if (kCurrentResponder) { - UIWindow *mainWindow = [UIApplication sharedApplication].keyWindow ?: [[UIApplication sharedApplication] windows].firstObject; - if (mainWindow) { - CGRect keyboardRect = keyboardMoveUserInfo.endFrame; - CGFloat distanceFromBottom = [QMUIKeyboardManager distanceFromMinYToBottomInView:mainWindow keyboardRect:keyboardRect]; - if (distanceFromBottom < keyboardRect.size.height) { - if (!kCurrentResponder.keyboardManager_isFirstResponder) { - // willHide - kCurrentResponder = nil; - } - } else if (distanceFromBottom > keyboardRect.size.height && !kCurrentResponder.isFirstResponder) { - if (!kCurrentResponder.keyboardManager_isFirstResponder) { - // 浮动 - kCurrentResponder = nil; - } - } - } - } - - } -} - -#pragma mark - 工具方法 - -+ (void)animateWithAnimated:(BOOL)animated keyboardUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { - if (animated) { - [UIView animateWithDuration:keyboardUserInfo.animationDuration delay:0 options:keyboardUserInfo.animationOptions|UIViewAnimationOptionBeginFromCurrentState animations:^{ - if (animations) { - animations(); - } - } completion:^(BOOL finished) { - if (completion) { - completion(finished); - } - }]; - } else { - if (animations) { - animations(); - } - if (completion) { - completion(YES); - } - } -} - -+ (void)handleKeyboardNotificationWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo showBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))showBlock hideBlock:(void (^)(QMUIKeyboardUserInfo *keyboardUserInfo))hideBlock { - // 专门处理 iPad Pro 在键盘完全不显示的情况(不会调用willShow,所以通过是否focus来判断) - if ([QMUIKeyboardManager visiableKeyboardHeight] <= 0 && !keyboardUserInfo.isTargetResponderFocused) { - if (hideBlock) { - hideBlock(keyboardUserInfo); - } - } else { - if (showBlock) { - showBlock(keyboardUserInfo); - } - } -} - -+ (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 (IOS_VERSION < 9) { - // UITextEffectsWindow - if (windowName.length == 19 && - [windowName hasPrefix:@"UI"] && - [windowName hasSuffix:[NSString stringWithFormat:@"%@%@", @"TextEffects", @"Window"]]) { - if (!kbWindows) kbWindows = [NSMutableArray new]; - [kbWindows addObject:window]; - } - } else { - // UIRemoteKeyboardWindow - if (windowName.length == 22 && - [windowName hasPrefix:@"UI"] && - [windowName hasSuffix:[NSString stringWithFormat:@"%@%@", @"Remote", @"KeyboardWindow"]]) { - 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; - if (!mainWindow) { - if (view) { - [view convertRect:rect fromView:nil]; - } else { - return rect; - } - } - - rect = [mainWindow convertRect:rect fromWindow:nil]; - if (!view) { - return [mainWindow convertRect:rect toWindow:nil]; - } - if (view == mainWindow) { - return rect; - } - - UIWindow *toWindow = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window; - if (!mainWindow || !toWindow) { - return [mainWindow convertRect:rect toView:view]; - } - if (mainWindow == toWindow) { - return [mainWindow convertRect:rect toView:view]; - } - - rect = [mainWindow convertRect:rect toView:mainWindow]; - rect = [toWindow convertRect:rect fromWindow:mainWindow]; - rect = [view convertRect:rect fromView:toWindow]; - - return rect; -} - -+ (CGFloat)distanceFromMinYToBottomInView:(UIView *)view keyboardRect:(CGRect)rect { - rect = [self convertKeyboardRect:rect toView:view]; - CGFloat distance = CGRectGetHeight(view.bounds) - CGRectGetMinY(rect); - return distance; -} - -+ (UIView *)keyboardView { - for (UIWindow *window in [UIApplication sharedApplication].windows) { - UIView *view = [self getKeyboardViewFromWindow:window]; - if (view) { - return view; - } - } - return nil; -} - -+ (UIView *)getKeyboardViewFromWindow:(UIWindow *)window { - - if (!window) return nil; - - NSString *windowName = NSStringFromClass(window.class); - if (IOS_VERSION < 9) { - if (![windowName isEqualToString:@"UITextEffectsWindow"]) { - return nil; - } - } else { - if (![windowName isEqualToString:@"UIRemoteKeyboardWindow"]) { - return nil; - } - } - - if (IOS_VERSION < 8) { - for (UIView *view in window.subviews) { - NSString *viewName = NSStringFromClass(view.class); - if (![viewName isEqualToString:@"UIPeripheralHostView"]) { - continue; - } - return view; - } - } else { - 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 nil; -} - -+ (BOOL)isKeyboardVisible { - UIView *keyboardView = self.keyboardView; - UIWindow *keyboardWindow = keyboardView.window; - if (!keyboardView || !keyboardWindow) { - return NO; - } - CGRect rect = CGRectIntersection(keyboardWindow.bounds, keyboardView.frame); - if (CGRectIsNull(rect) || CGRectIsInfinite(rect)) { - return NO; - } - return rect.size.width > 0 && rect.size.height > 0; -} - -+ (CGRect)currentKeyboardFrame { - UIView *keyboardView = [self keyboardView]; - if (!keyboardView) { - return CGRectNull; - } - UIWindow *keyboardWindow = keyboardView.window; - if (keyboardWindow) { - return [keyboardWindow convertRect:keyboardView.frame toWindow:nil]; - } else { - return keyboardView.frame; - } -} - -+ (CGFloat)visiableKeyboardHeight { - UIView *keyboardView = [self keyboardView]; - UIWindow *keyboardWindow = keyboardView.window; - if (!keyboardView || !keyboardWindow) { - return 0; - } else { - CGRect visiableRect = CGRectIntersection(keyboardWindow.bounds, keyboardView.frame); - if (CGRectIsNull(visiableRect)) { - return 0; - } - return visiableRect.size.height; - } -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIMarqueeLabel.h b/QMUI/QMUIKit/UIComponents/QMUIMarqueeLabel.h deleted file mode 100644 index 6a98e6e8..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIMarqueeLabel.h +++ /dev/null @@ -1,65 +0,0 @@ -// -// QMUIMarqueeLabel.h -// qmui -// -// Created by MoLice on 2017/5/31. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import - -/** - * 简易的跑马灯 label 控件,在文字超过 label 可视区域时会自动开启跑马灯效果展示文字,文字滚动时是首尾连接的效果(参考播放音乐时系统锁屏界面顶部的音乐标题)。 - * @warning lineBreakMode 默认为 NSLineBreakByClipping(UILabel 默认值为 NSLineBreakByTruncatingTail)。 - * @warning textAlignment 暂不支持 NSTextAlignmentJustified 和 NSTextAlignmentNatural。 - * @warning 会忽略 numberOfLines 属性,强制以 1 来展示。 - */ -@interface QMUIMarqueeLabel : UILabel - -/// 控制滚动的速度,1 表示一帧滚动 1pt,10 表示一帧滚动 10pt,默认为 .5,与系统一致。 -@property(nonatomic, assign) CGFloat speed; - -/// 当文字第一次显示在界面上,以及重复滚动到开头时都要停顿一下,这个属性控制停顿的时长,默认为 2.5(也是与系统一致),单位为秒。 -@property(nonatomic, assign) NSTimeInterval pauseDurationWhenMoveToEdge; - -/// 用于控制首尾连接的文字之间的间距,默认为 40pt。 -@property(nonatomic, assign) CGFloat spacingBetweenHeadToTail; - -/** - * 自动判断 label 的 frame 是否超出当前的 UIWindow 可视范围,超出则自动停止动画。默认为 YES。 - * @warning 某些场景并无法触发这个自动检测(例如直接调整 label.superview 的 frame 而不是 label 自身的 frame),这种情况暂不处理。 - */ -@property(nonatomic, assign) BOOL automaticallyValidateVisibleFrame; - -/// 在文字滚动到左右边缘时,是否要显示一个阴影渐变遮罩,默认为 YES。 -@property(nonatomic, assign) BOOL shouldFadeAtEdge; - -/// 渐变遮罩的宽度,默认为 20。 -@property(nonatomic, assign) CGFloat fadeWidth; - -/// 渐变遮罩外边缘的颜色,请使用带 Alpha 通道的颜色 -@property(nonatomic, strong) UIColor *fadeStartColor; - -/// 渐变遮罩内边缘的颜色,一般是 fadeStartColor 的 alpha 通道为 0 的色值 -@property(nonatomic, strong) UIColor *fadeEndColor; - -/// 文字是否要在渐隐区域之后显示,默认为 NO,如果想避免停靠在初始位置时文字被遮罩盖住,可以把它改为 YES。 -@property(nonatomic, assign) BOOL textStartAfterFade; -@end - - -/// 如果在可复用的 UIView 里使用(例如 UITableViewCell、UICollectionViewCell),由于 UIView 可能重复被使用,因此需要在某些显示/隐藏的时机去手动开启/关闭 label 的动画。如果在普通的 UIView 里使用则无需关注这一部分的代码。 -@interface QMUIMarqueeLabel (ReusableView) - -/** - * 尝试开启 label 的滚动动画 - * @return 是否成功开启 - */ -- (BOOL)requestToStartAnimation; - -/** - * 尝试停止 label 的滚动动画 - * @return 是否成功停止 - */ -- (BOOL)requestToStopAnimation; -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIMarqueeLabel.m b/QMUI/QMUIKit/UIComponents/QMUIMarqueeLabel.m deleted file mode 100644 index b2e19413..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIMarqueeLabel.m +++ /dev/null @@ -1,281 +0,0 @@ -// -// QMUIMarqueeLabel.m -// qmui -// -// Created by MoLice on 2017/5/31. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import "QMUIMarqueeLabel.h" -#import "QMUICore.h" -#import "CALayer+QMUI.h" -#import "NSString+QMUI.h" - -@interface QMUIMarqueeLabel () - -@property(nonatomic, strong) CADisplayLink *displayLink; -@property(nonatomic, assign) CGFloat offsetX; -@property(nonatomic, assign) CGFloat textWidth; - -@property(nonatomic, strong) CAGradientLayer *fadeLeftLayer; -@property(nonatomic, strong) CAGradientLayer *fadeRightLayer; - -@property(nonatomic, assign) BOOL isFirstDisplay; - -/// 绘制文本时重复绘制的次数,用于实现首尾连接的滚动效果,1 表示不首尾连接,大于 1 表示首尾连接。 -@property(nonatomic, assign) NSInteger textRepeatCount; -@end - -@implementation QMUIMarqueeLabel - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - self.lineBreakMode = NSLineBreakByClipping; - self.clipsToBounds = YES;// 显示非英文字符时,滚动的时候字符会稍微露出两端,所以这里直接裁剪掉 - - self.speed = .5; - self.pauseDurationWhenMoveToEdge = 2.5; - self.spacingBetweenHeadToTail = 40; - self.automaticallyValidateVisibleFrame = YES; - self.fadeWidth = 20; - self.fadeStartColor = UIColorMakeWithRGBA(255, 255, 255, 1); - self.fadeEndColor = UIColorMakeWithRGBA(255, 255, 255, 0); - self.shouldFadeAtEdge = YES; - self.textStartAfterFade = NO; - - self.isFirstDisplay = YES; - self.textRepeatCount = 2; - } - return self; -} - -- (void)dealloc { - [self.displayLink invalidate]; - self.displayLink = nil; -} - -- (void)didMoveToWindow { - [super didMoveToWindow]; - if (self.window) { - self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)]; - [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - } else { - [self.displayLink invalidate]; - self.displayLink = nil; - } - self.offsetX = 0; - self.displayLink.paused = ![self shouldPlayDisplayLink]; -} - -- (void)setText:(NSString *)text { - [super setText:text]; - self.offsetX = 0; - self.textWidth = [self sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)].width; - self.displayLink.paused = ![self shouldPlayDisplayLink]; -} - -- (void)setAttributedText:(NSAttributedString *)attributedText { - [super setAttributedText:attributedText]; - self.offsetX = 0; - self.textWidth = [self sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)].width; - self.displayLink.paused = ![self shouldPlayDisplayLink]; -} - -- (void)setFrame:(CGRect)frame { - BOOL isSizeChanged = !CGSizeEqualToSize(frame.size, self.frame.size); - [super setFrame:frame]; - if (isSizeChanged) { - self.offsetX = 0; - self.displayLink.paused = ![self shouldPlayDisplayLink]; - } -} - -- (void)drawTextInRect:(CGRect)rect { - CGFloat textInitialX = 0; - if (self.textAlignment == NSTextAlignmentLeft) { - textInitialX = 0; - } else if (self.textAlignment == NSTextAlignmentCenter) { - textInitialX = fmax(0, CGFloatGetCenter(CGRectGetWidth(self.bounds), self.textWidth)); - } else if (self.textAlignment == NSTextAlignmentRight) { - textInitialX = fmax(0, CGRectGetWidth(self.bounds) - self.textWidth); - } - - // 考虑渐变遮罩的偏移 - CGFloat textOffsetXByFade = textInitialX < self.fadeWidth ? ((self.shouldFadeAtEdge && self.textStartAfterFade) ? self.fadeWidth : 0) : 0; - textInitialX += textOffsetXByFade; - - for (NSInteger i = 0; i < self.textRepeatCountConsiderTextWidth; i++) { - [self.attributedText drawInRect:CGRectMake(self.offsetX + (self.textWidth + self.spacingBetweenHeadToTail) * i + textInitialX, 0, self.textWidth, CGRectGetHeight(rect))]; - } - - // 自定义绘制就不需要调用 super -// [super drawTextInRect:rectToDrawAfterAnimated]; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - if (self.fadeLeftLayer) { - self.fadeLeftLayer.frame = CGRectMake(0, 0, self.fadeWidth, CGRectGetHeight(self.bounds)); - [self.layer qmui_bringSublayerToFront:self.fadeLeftLayer];// 显示非英文字符时,UILabel 内部会额外多出一层 layer 盖住了这里的 fadeLayer,所以要手动提到最前面 - } - if (self.fadeRightLayer) { - self.fadeRightLayer.frame = CGRectMake(CGRectGetWidth(self.bounds) - self.fadeWidth, 0, self.fadeWidth, CGRectGetHeight(self.bounds)); - [self.layer qmui_bringSublayerToFront:self.fadeRightLayer];// 显示非英文字符时,UILabel 内部会额外多出一层 layer 盖住了这里的 fadeLayer,所以要手动提到最前面 - } -} - -- (NSInteger)textRepeatCountConsiderTextWidth { - if (self.textWidth < CGRectGetWidth(self.bounds)) { - return 1; - } - return self.textRepeatCount; -} - -- (void)handleDisplayLink:(CADisplayLink *)displayLink { - if (self.offsetX == 0) { - displayLink.paused = YES; - [self setNeedsDisplay]; - - int64_t delay = (self.isFirstDisplay || self.textRepeatCount <= 1) ? self.pauseDurationWhenMoveToEdge : 0; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - displayLink.paused = ![self shouldPlayDisplayLink]; - if (!displayLink.paused) { - self.offsetX -= self.speed; - } - }); - - if (delay > 0 && self.textRepeatCount > 1) { - self.isFirstDisplay = NO; - } - - return; - } - - self.offsetX -= self.speed; - [self setNeedsDisplay]; - - if (-self.offsetX >= self.textWidth + (self.textRepeatCountConsiderTextWidth > 1 ? self.spacingBetweenHeadToTail : 0)) { - displayLink.paused = YES; - int64_t delay = self.textRepeatCount > 1 ? self.pauseDurationWhenMoveToEdge : 0; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - self.offsetX = 0; - [self handleDisplayLink:displayLink]; - }); - } -} - -- (BOOL)shouldPlayDisplayLink { - BOOL result = self.window && CGRectGetWidth(self.bounds) > 0 && self.textWidth > (CGRectGetWidth(self.bounds) - ((self.shouldFadeAtEdge && self.textStartAfterFade) ? self.fadeWidth : 0)); - - // 如果 label.frame 在 window 可视区域之外,也视为不可见,暂停掉 displayLink - if (result && self.automaticallyValidateVisibleFrame) { - CGRect rectInWindow = [self.window convertRect:self.frame fromView:self.superview]; - if (!CGRectIntersectsRect(self.window.bounds, rectInWindow)) { - return NO; - } - } - - return result; -} - -- (void)setOffsetX:(CGFloat)offsetX { - _offsetX = offsetX; - [self updateFadeLayersHidden]; -} - -- (void)setShouldFadeAtEdge:(BOOL)shouldFadeAtEdge { - _shouldFadeAtEdge = shouldFadeAtEdge; - if (shouldFadeAtEdge) { - [self initFadeLayersIfNeeded]; - } - [self updateFadeLayersHidden]; -} - -- (void)setFadeStartColor:(UIColor *)fadeStartColor { - _fadeStartColor = fadeStartColor; - [self updateFadeLayerColors]; -} - -- (void)setFadeEndColor:(UIColor *)fadeEndColor { - _fadeEndColor = fadeEndColor; - [self updateFadeLayerColors]; -} - -- (void)updateFadeLayerColors { - if (self.fadeLeftLayer) { - if (self.fadeStartColor && self.fadeEndColor) { - self.fadeLeftLayer.colors = @[(id)self.fadeStartColor.CGColor, - (id)self.fadeEndColor.CGColor]; - } else { - self.fadeLeftLayer.colors = nil; - } - } - if (self.fadeRightLayer) { - if (self.fadeStartColor && self.fadeEndColor) { - self.fadeRightLayer.colors = @[(id)self.fadeStartColor.CGColor, - (id)self.fadeEndColor.CGColor]; - } else { - self.fadeRightLayer.colors = nil; - } - } -} - -- (void)updateFadeLayersHidden { - if (!self.fadeLeftLayer || !self.fadeRightLayer) { - return; - } - - BOOL shouldShowFadeLeftLayer = self.shouldFadeAtEdge && (self.offsetX < 0 || (self.offsetX == 0 && !self.isFirstDisplay)); - self.fadeLeftLayer.hidden = !shouldShowFadeLeftLayer; - - BOOL shouldShowFadeRightLayer = self.shouldFadeAtEdge && (self.textWidth > CGRectGetWidth(self.bounds) && self.offsetX != self.textWidth - CGRectGetWidth(self.bounds)); - self.fadeRightLayer.hidden = !shouldShowFadeRightLayer; -} - -- (void)initFadeLayersIfNeeded { - if (!self.fadeLeftLayer) { - self.fadeLeftLayer = [CAGradientLayer layer];// 请保留自带的 hidden 动画 - self.fadeLeftLayer.startPoint = CGPointMake(0, .5); - self.fadeLeftLayer.endPoint = CGPointMake(1, .5); - [self.layer addSublayer:self.fadeLeftLayer]; - [self setNeedsLayout]; - } - - if (!self.fadeRightLayer) { - self.fadeRightLayer = [CAGradientLayer layer];// 请保留自带的 hidden 动画 - self.fadeRightLayer.startPoint = CGPointMake(1, .5); - self.fadeRightLayer.endPoint = CGPointMake(0, .5); - [self.layer addSublayer:self.fadeRightLayer]; - [self setNeedsLayout]; - } - - [self updateFadeLayerColors]; -} - -#pragma mark - Superclass - -- (void)setNumberOfLines:(NSInteger)numberOfLines { - numberOfLines = 1; - [super setNumberOfLines:numberOfLines]; -} - -@end - -@implementation QMUIMarqueeLabel (ReusableView) - -- (BOOL)requestToStartAnimation { - self.automaticallyValidateVisibleFrame = NO; - BOOL shouldPlayDisplayLink = [self shouldPlayDisplayLink]; - if (shouldPlayDisplayLink) { - self.displayLink.paused = NO; - } - return shouldPlayDisplayLink; -} - -- (BOOL)requestToStopAnimation { - self.displayLink.paused = YES; - return YES; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIModalPresentationViewController.h b/QMUI/QMUIKit/UIComponents/QMUIModalPresentationViewController.h deleted file mode 100644 index 04eb42e7..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIModalPresentationViewController.h +++ /dev/null @@ -1,267 +0,0 @@ -// -// QMUIModalPresentationViewController.h -// qmui -// -// Created by MoLice on 16/7/6. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import - -@class QMUIModalPresentationViewController; -@class QMUIModalPresentationWindow; - -typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) { - QMUIModalPresentationAnimationStyleFade, // 渐现渐隐,默认 - QMUIModalPresentationAnimationStylePopup, // 从中心点弹出 - QMUIModalPresentationAnimationStyleSlide // 从下往上升起 -}; - -@protocol QMUIModalPresentationContentViewControllerProtocol - -@optional - -/** - * 当浮层以 UIViewController 的形式展示(而非 UIView),并且使用 modalController 提供的默认布局时,则可通过这个方法告诉 modalController 当前浮层期望的大小 - * @param controller 当前的modalController - * @param limitSize 浮层最大的宽高,由当前 modalController 的大小及 `contentViewMargins`、`maximumContentViewWidth` 决定 - * @return 返回浮层在 `limitSize` 限定内的大小,如果业务自身不需要限制宽度/高度,则为 width/height 返回 `CGFLOAT_MAX` 即可 - */ -- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller limitSize:(CGSize)limitSize; - -@end - -@protocol QMUIModalPresentationViewControllerDelegate - -@optional - -/** - * 是否应该隐藏浮层,会在调用`hideWithAnimated:completion:`时,以及点击背景遮罩时被调用。默认为YES。 - * @param controller 当前的modalController - * @return 是否允许隐藏,YES表示允许隐藏,NO表示不允许隐藏 - */ -- (BOOL)shouldHideModalPresentationViewController:(QMUIModalPresentationViewController *)controller; - -/** - * modalController 即将隐藏时的回调方法,在调用完这个方法后才开始做一些隐藏前的准备工作,例如恢复 window 的 dimmed 状态等。 - * @param controller 当前的modalController - */ -- (void)willHideModalPresentationViewController:(QMUIModalPresentationViewController *)controller; - -/** - * modalController隐藏后的回调方法,不管是直接调用`hideWithAnimated:completion:`,还是通过点击遮罩触发的隐藏,都会调用这个方法。 - * 如果你想区分这两种方式的隐藏回调,请直接使用hideWithAnimated方法的completion参数,以及`didHideByDimmingViewTappedBlock`属性。 - * @param controller 当前的modalController - */ -- (void)didHideModalPresentationViewController:(QMUIModalPresentationViewController *)controller; - -- (void)requestHideAllModalPresentationViewController; - -@end - -/** - * 一个提供通用的弹出浮层功能的控件,可以将任意`UIView`或`UIViewController`以浮层的形式显示出来并自动布局。 - * - * 支持 3 种方式显示浮层: - * - * 1. **推荐** 新起一个 `UIWindow` 盖在当前界面上,将 `QMUIModalPresentationViewController` 以 `rootViewController` 的形式显示出来,可通过 `supportedOrientationMask` 支持横竖屏,不支持在浮层不消失的情况下做界面切换(因为 window 会把背后的 controller 盖住,看不到界面切换) - * @code - * [modalPresentationViewController showWithAnimated:YES completion:nil]; - * @endcode - * - * 2. 使用系统接口来显示,支持界面切换,**注意** 使用这种方法必定只能以动画的形式来显示浮层,无法以无动画的形式来显示,并且 `animated` 参数必须为 `NO`。可通过 `supportedOrientationMask` 支持横竖屏。 - * @code - * [self presentViewController:modalPresentationViewController animated:NO completion:nil]; - * @endcode - * - * 3. 将浮层作为一个 subview 添加到 `superview` 上,从而能够实现在浮层不消失的情况下进行界面切换,但需要 `superview` 自行管理浮层的大小和横竖屏旋转,而且 `QMUIModalPresentationViewController` 不能用局部变量来保存,会在显示后被释放,需要自行 retain。横竖屏跟随当前界面的设置。 - * @code - * self.modalPresentationViewController.view.frame = CGRectMake(50, 50, 100, 100); - * [self.view addSubview:self.modalPresentationViewController.view]; - * @endcode - * - * 默认的布局会将浮层居中显示,浮层的大小可通过接口控制: - * 1. 如果是用 `contentViewController`,则可通过 `preferredContentSizeInModalPresentationViewController:limitSize:` 来设置 - * 2. 如果使用 `contentView`,或者使用 `contentViewController` 但没实现 `preferredContentSizeInModalPresentationViewController:limitSize:`,则调用`contentView`的`sizeThatFits:`方法获取大小。 - * 3. 浮层大小会受 `maximumContentViewWidth` 属性的限制,以及 `contentViewMargins` 属性的影响。 - * - * 通过`layoutBlock`、`showingAnimation`、`hidingAnimation`可设置自定义的布局、打开及隐藏的动画,并允许你适配键盘升起时的场景。 - * - * 默认提供背景遮罩`dimmingView`,你也可以使用自己的遮罩 view。 - * - * 默认提供多种显示动画,可通过 `animationStyle` 来设置。 - * - * @warning 如果使用者retain了modalPresentationViewController,注意应该在`hideWithAnimated:completion:`里release - * - * @see QMUIAlertController - * @see QMUIDialogViewController - * @see QMUIMoreOperationController - */ -@interface QMUIModalPresentationViewController : UIViewController { - UITapGestureRecognizer *_dimmingViewTapGestureRecognizer; - CGFloat _keyboardHeight; -} - -@property(nonatomic, weak) IBOutlet id delegate; - -/** - * 要被弹出的浮层 - * @warning 当设置了`contentView`时,不要再设置`contentViewController` - */ -@property(nonatomic, strong) IBOutlet UIView *contentView; - -/** - * 要被弹出的浮层,适用于浮层以UIViewController的形式来管理的情况。 - * @warning 当设置了`contentViewController`时,`contentViewController.view`会被当成`contentView`使用,因此不要再自行设置`contentView` - * @warning 注意`contentViewController`是强引用,容易导致循环引用,使用时请注意 - */ -@property(nonatomic, strong) IBOutlet UIViewController *contentViewController; - -/** - * 设置`contentView`布局时与外容器的间距,默认为(20, 20, 20, 20) - * @warning 当设置了`layoutBlock`属性时,此属性不生效 - */ -@property(nonatomic, assign) UIEdgeInsets contentViewMargins UI_APPEARANCE_SELECTOR; - -/** - * 限制`contentView`布局时的最大宽度,默认为iPhone 6竖屏下的屏幕宽度减去`contentViewMargins`在水平方向的值,也即浮层在iPhone 6 Plus或iPad上的宽度以iPhone 6上的宽度为准。 - * @warning 当设置了`layoutBlock`属性时,此属性不生效 - */ -@property(nonatomic, assign) CGFloat maximumContentViewWidth UI_APPEARANCE_SELECTOR; - -/** - * 背景遮罩,默认为一个普通的`UIView`,背景色为`UIColorMask`,可设置为自己的view,注意`dimmingView`的大小将会盖满整个控件。 - * - * `QMUIModalPresentationViewController`会自动给自定义的`dimmingView`添加手势以实现点击遮罩隐藏浮层。 - */ -@property(nonatomic, strong) IBOutlet UIView *dimmingView; - -/** - * 由于点击遮罩导致浮层被隐藏时的回调(区分于`hideWithAnimated:completion:`里的completion,这里是特地用于点击遮罩的情况) - */ -@property(nonatomic, copy) void (^didHideByDimmingViewTappedBlock)(); - -/** - * 控制当前是否以模态的形式存在。如果以模态的形式存在,则点击空白区域不会隐藏浮层。 - * - * 默认为NO,也即点击空白区域将会自动隐藏浮层。 - */ -@property(nonatomic, assign, getter=isModal) BOOL modal; - -/** - * 标志当前浮层的显示/隐藏状态,默认为NO。 - */ -@property(nonatomic, assign, readonly, getter=isVisible) BOOL visible; - -/** - * 修改当前界面要支持的横竖屏方向,默认为 SupportedOrientationMask。 - */ -@property(nonatomic, assign) UIInterfaceOrientationMask supportedOrientationMask; - -/** - * 设置要使用的显示/隐藏动画的类型,默认为`QMUIModalPresentationAnimationStyleFade`。 - * @warning 当使用了`showingAnimation`和`hidingAnimation`时,该属性无效 - */ -@property(nonatomic, assign) QMUIModalPresentationAnimationStyle animationStyle UI_APPEARANCE_SELECTOR; - -/** - * 管理自定义的浮层布局,将会在浮层显示前、控件的容器大小发生变化时(例如横竖屏、来电状态栏)被调用 - * @arg containerBounds 浮层所在的父容器的大小,也即`self.view.bounds` - * @arg keyboardHeight 键盘在当前界面里的高度,若无键盘,则为0 - * @arg contentViewDefaultFrame 不使用自定义布局的情况下的默认布局,会受`contentViewMargins`、`maximumContentViewWidth`、`contentView sizeThatFits:`的影响 - * - * @see contentViewMargins - * @see maximumContentViewWidth - */ -@property(nonatomic, copy) void (^layoutBlock)(CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewDefaultFrame); - -/** - * 管理自定义的显示动画,需要管理的对象包括`contentView`和`dimmingView`,在`showingAnimation`被调用前,`contentView`已被添加到界面上。若使用了`layoutBlock`,则会先调用`layoutBlock`,再调用`showingAnimation`。在动画结束后,必须调用参数里的`completion` block。 - * @arg dimmingView 背景遮罩的View,请自行设置显示遮罩的动画 - * @arg containerBounds 浮层所在的父容器的大小,也即`self.view.bounds` - * @arg keyboardHeight 键盘在当前界面里的高度,若无键盘,则为0 - * @arg contentViewFrame 动画执行完后`contentView`的最终frame,若使用了`layoutBlock`,则也即`layoutBlock`计算完后的frame - * @arg completion 动画结束后给到modalController的回调,modalController会在这个回调里做一些状态设置,务必调用。 - */ -@property(nonatomic, copy) void (^showingAnimation)(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewFrame, void(^completion)(BOOL finished)); - -/** - * 管理自定义的隐藏动画,需要管理的对象包括`contentView`和`dimmingView`,在动画结束后,必须调用参数里的`completion` block。 - * @arg dimmingView 背景遮罩的View,请自行设置隐藏遮罩的动画 - * @arg containerBounds 浮层所在的父容器的大小,也即`self.view.bounds` - * @arg keyboardHeight 键盘在当前界面里的高度,若无键盘,则为0 - * @arg completion 动画结束后给到modalController的回调,modalController会在这个回调里做一些清理工作,务必调用 - */ -@property(nonatomic, copy) void (^hidingAnimation)(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, void(^completion)(BOOL finished)); - -/** - * 将浮层以 UIWindow 的方式显示出来 - * @param animated 是否以动画的形式显示 - * @param completion 显示动画结束后的回调 - */ -- (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; - -/** - * 将浮层隐藏掉 - * @param animated 是否以动画的形式隐藏 - * @param completion 隐藏动画结束后的回调 - * @warning 这里的`completion`只会在你显式调用`hideWithAnimated:completion:`方法来隐藏浮层时会被调用,如果你通过点击`dimmingView`来触发`hideWithAnimated:completion:`,则completion是不会被调用的,那种情况下如果你要在浮层隐藏后做一些事情,请使用`delegate`提供的`didHideModalPresentationViewController:`方法。 - */ -- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; - -/** - * 将浮层以 addSubview 的方式显示出来 - * - * @param view 要显示到哪个 view 上 - * @param animated 是否以动画的形式显示 - * @param completion 显示动画结束后的回调 - */ -- (void)showInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(BOOL finished))completion; - -/** - * 将某个 view 上显示的浮层隐藏掉 - * @param view 要隐藏哪个 view 上的浮层 - * @param animated 是否以动画的形式隐藏 - * @param completion 隐藏动画结束后的回调 - * @warning 这里的`completion`只会在你显式调用`hideInView:animated:completion:`方法来隐藏浮层时会被调用,如果你通过点击`dimmingView`来触发`hideInView:animated:completion:`,则completion是不会被调用的,那种情况下如果你要在浮层隐藏后做一些事情,请使用`delegate`提供的`didHideModalPresentationViewController:`方法。 - */ -- (void)hideInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(BOOL finished))completion; - -@end - - -@interface QMUIModalPresentationViewController (Manager) - -/** - * 判断当前App里是否有modalViewController正在显示(存在modalViewController但不可见的时候,也视为不存在) - * @return 只要存在正在显示的浮层,则返回YES,否则返回NO - */ -+ (BOOL)isAnyModalPresentationViewControllerVisible; - -/** - * 把所有正在显示的并且允许被隐藏的modalViewController都隐藏掉 - * @return 只要遇到一个正在显示的并且不能被隐藏的浮层,就会返回NO,否则都返回YES,表示成功隐藏掉所有可视浮层 - * @see shouldHideModalPresentationViewController: - */ -+ (BOOL)hideAllVisibleModalPresentationViewControllerIfCan; -@end - -@interface QMUIModalPresentationViewController (UIAppearance) - -+ (instancetype)appearance; - -@end - -/// 专用于QMUIModalPresentationViewController的UIWindow,这样才能在`[[UIApplication sharedApplication] windows]`里方便地区分出来 -@interface QMUIModalPresentationWindow : UIWindow - -@end - - -@interface UIViewController (QMUIModalPresentationViewController) - -/** - * 获取弹出当前vieController的QMUIModalPresentationViewController - */ -@property(nonatomic, weak, readonly) QMUIModalPresentationViewController *modalPresentedViewController; -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIModalPresentationViewController.m b/QMUI/QMUIKit/UIComponents/QMUIModalPresentationViewController.m deleted file mode 100644 index 391c1176..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIModalPresentationViewController.m +++ /dev/null @@ -1,594 +0,0 @@ -// -// QMUIModalPresentationViewController.m -// qmui -// -// Created by MoLice on 16/7/6. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIModalPresentationViewController.h" -#import "QMUICore.h" - -@interface UIViewController () - -@property(nonatomic, weak, readwrite) QMUIModalPresentationViewController *modalPresentedViewController; -@end - -@implementation QMUIModalPresentationViewController (UIAppearance) - -static QMUIModalPresentationViewController *appearance; -+ (instancetype)appearance { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self initDefaultAppearance]; - }); - return appearance; -} - -+ (void)initDefaultAppearance { - if (!appearance) { - appearance = [[self alloc] init]; - } - appearance.animationStyle = QMUIModalPresentationAnimationStyleFade; - appearance.contentViewMargins = UIEdgeInsetsMake(20, 20, 20, 20); - appearance.maximumContentViewWidth = ([QMUIHelper screenSizeFor47Inch].width - UIEdgeInsetsGetHorizontalValue(appearance.contentViewMargins)); -} - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; - }); -} - -@end - -@interface QMUIModalPresentationViewController () - -@property(nonatomic, strong) QMUIModalPresentationWindow *containerWindow; -@property(nonatomic, weak) UIWindow *previousKeyWindow; - -@property(nonatomic, assign) BOOL appearAnimated; -@property(nonatomic, copy) void (^appearCompletionBlock)(BOOL finished); - -@property(nonatomic, assign) BOOL disappearAnimated; -@property(nonatomic, copy) void (^disappearCompletionBlock)(BOOL finished); - -/// 标志是否已经走过一次viewWillAppear了,用于hideInView的情况 -@property(nonatomic, assign) BOOL hasAlreadyViewWillDisappear; - -@property(nonatomic, strong) UITapGestureRecognizer *dimmingViewTapGestureRecognizer; -@property(nonatomic, assign) CGFloat keyboardHeight; -@end - -@implementation QMUIModalPresentationViewController - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - if (appearance) { - self.animationStyle = appearance.animationStyle; - self.contentViewMargins = appearance.contentViewMargins; - self.maximumContentViewWidth = appearance.maximumContentViewWidth; - self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; - self.modalPresentationStyle = UIModalPresentationCustom; - self.supportedOrientationMask = SupportedOrientationMask; - } - - [self initDefaultDimmingViewWithoutAddToView]; -} - -- (void)awakeFromNib { - [super awakeFromNib]; - if (self.contentViewController) { - // 在 IB 里设置了 contentViewController 的话,通过这个调用去触发 contentView 的更新 - self.contentViewController = self.contentViewController; - } -} - -- (void)dealloc { - self.containerWindow = nil; -} - -- (BOOL)shouldAutomaticallyForwardAppearanceMethods { - // 屏蔽对childViewController的生命周期函数的自动调用,改为手动控制 - return NO; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - if (self.dimmingView && !self.dimmingView.superview) { - [self.view addSubview:self.dimmingView]; - } -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - - self.dimmingView.frame = self.view.bounds; - - CGRect contentViewFrame = [self contentViewFrameForShowing]; - if (self.layoutBlock) { - self.layoutBlock(self.view.bounds, self.keyboardHeight, contentViewFrame); - } else { - self.contentView.frame = contentViewFrame; - } -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - if (self.containerWindow) { - // 只有使用showWithAnimated:completion:显示出来的浮层,才需要修改之前就记住的animated的值 - animated = self.appearAnimated; - } - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; - - if (self.contentViewController) { - self.contentViewController.modalPresentedViewController = self; - [self.contentViewController beginAppearanceTransition:YES animated:animated]; - } - - [QMUIHelper dimmedApplicationWindow]; - - void (^didShownCompletion)(BOOL finished) = ^(BOOL finished) { - if (self.contentViewController) { - [self.contentViewController endAppearanceTransition]; - } - - _visible = YES; - - if (self.appearCompletionBlock) { - self.appearCompletionBlock(finished); - self.appearCompletionBlock = nil; - } - - self.appearAnimated = NO; - }; - - if (animated) { - [self.view addSubview:self.contentView]; - [self.view layoutIfNeeded]; - - CGRect contentViewFrame = [self contentViewFrameForShowing]; - if (self.showingAnimation) { - // 使用自定义的动画 - if (self.layoutBlock) { - self.layoutBlock(self.view.bounds, self.keyboardHeight, contentViewFrame); - contentViewFrame = self.contentView.frame; - } - self.showingAnimation(self.dimmingView, self.view.bounds, self.keyboardHeight, contentViewFrame, didShownCompletion); - } else { - self.contentView.frame = contentViewFrame; - [self.contentView setNeedsLayout]; - [self.contentView layoutIfNeeded]; - - [self showingAnimationWithCompletion:didShownCompletion]; - } - } else { - CGRect contentViewFrame = [self contentViewFrameForShowing]; - self.contentView.frame = contentViewFrame; - [self.view addSubview:self.contentView]; - self.dimmingView.alpha = 1; - didShownCompletion(YES); - } -} - -- (void)viewWillDisappear:(BOOL)animated { - if (self.hasAlreadyViewWillDisappear) { - return; - } - - [super viewWillDisappear:animated]; - if (self.containerWindow) { - animated = self.disappearAnimated; - } - - if ([self.delegate respondsToSelector:@selector(willHideModalPresentationViewController:)]) { - [self.delegate willHideModalPresentationViewController:self]; - } - - void (^didHiddenCompletion)(BOOL finished) = ^(BOOL finished) { - - if (self.containerWindow) { - // 恢复 keyWindow 之前做一下检查,避免这个问题 https://github.com/QMUI/QMUI_iOS/issues/90 - if ([[UIApplication sharedApplication] keyWindow] == self.containerWindow) { - [self.previousKeyWindow makeKeyWindow]; - } - self.containerWindow.hidden = YES; - self.containerWindow.rootViewController = nil; - self.previousKeyWindow = nil; - [self endAppearanceTransition]; - } - - if (self.view.superview) { - // 这句是给addSubview的形式显示的情况下使用,但会触发第二次viewWillDisappear:,所以要搭配self.hasAlreadyViewWillDisappear使用 - [self.view removeFromSuperview]; - self.hasAlreadyViewWillDisappear = NO; - } - - [self.contentView removeFromSuperview]; - self.contentView = nil; - if (self.contentViewController) { - [self.contentViewController endAppearanceTransition]; - } - - _visible = NO; - - if ([self.delegate respondsToSelector:@selector(didHideModalPresentationViewController:)]) { - [self.delegate didHideModalPresentationViewController:self]; - } - - if (self.disappearCompletionBlock) { - self.disappearCompletionBlock(YES); - self.disappearCompletionBlock = nil; - } - - if (self.contentViewController) { - self.contentViewController.modalPresentedViewController = nil; - self.contentViewController = nil; - } - - self.disappearAnimated = NO; - }; - - // 在降下键盘前取消对键盘事件的监听,从而避免键盘影响隐藏浮层的动画 - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; - - [QMUIHelper resetDimmedApplicationWindow]; - [self.view endEditing:YES]; - - if (self.contentViewController) { - [self.contentViewController beginAppearanceTransition:NO animated:animated]; - } - - if (animated) { - if (self.hidingAnimation) { - self.hidingAnimation(self.dimmingView, self.view.bounds, self.keyboardHeight, didHiddenCompletion); - } else { - [self hidingAnimationWithCompletion:didHiddenCompletion]; - } - } else { - didHiddenCompletion(YES); - } -} - -#pragma mark - Dimming View - -- (void)setDimmingView:(UIView *)dimmingView { - if (![self isViewLoaded]) { - _dimmingView = dimmingView; - } else { - [self.view insertSubview:dimmingView belowSubview:_dimmingView]; - [_dimmingView removeFromSuperview]; - _dimmingView = dimmingView; - [self.view setNeedsLayout]; - } - [self addTapGestureRecognizerToDimmingViewIfNeeded]; -} - -- (void)initDefaultDimmingViewWithoutAddToView { - if (!self.dimmingView) { - _dimmingView = [[UIView alloc] init]; - self.dimmingView.backgroundColor = UIColorMask; - [self addTapGestureRecognizerToDimmingViewIfNeeded]; - if ([self isViewLoaded]) { - [self.view addSubview:self.dimmingView]; - } - } -} - -// 要考虑用户可能创建了自己的dimmingView,则tap手势也要重新添加上去 -- (void)addTapGestureRecognizerToDimmingViewIfNeeded { - if (!self.dimmingView) { - return; - } - - if (self.dimmingViewTapGestureRecognizer.view == self.dimmingView) { - return; - } - - if (!self.dimmingViewTapGestureRecognizer) { - self.dimmingViewTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDimmingViewTapGestureRecognizer:)]; - } - [self.dimmingView addGestureRecognizer:self.dimmingViewTapGestureRecognizer]; - self.dimmingView.userInteractionEnabled = YES;// UIImageView默认userInteractionEnabled为NO,为了兼容UIImageView,这里必须主动设置为YES -} - -- (void)handleDimmingViewTapGestureRecognizer:(UITapGestureRecognizer *)tapGestureRecognizer { - if (self.modal) { - return; - } - - if (self.containerWindow) { - // 认为是以 UIWindow 的形式显示出来 - __weak __typeof(self)weakSelf = self; - [self hideWithAnimated:YES completion:^(BOOL finished) { - if (weakSelf.didHideByDimmingViewTappedBlock) { - weakSelf.didHideByDimmingViewTappedBlock(); - } - }]; - } else if (self.presentingViewController && self.presentingViewController.presentedViewController == self) { - // 认为是以 presentViewController 的形式显示出来 - [self dismissViewControllerAnimated:YES completion:^{ - if (self.didHideByDimmingViewTappedBlock) { - self.didHideByDimmingViewTappedBlock(); - } - }]; - } else { - // 认为是 addSubview 的形式显示出来 - __weak __typeof(self)weakSelf = self; - [self hideInView:self.view.superview animated:YES completion:^(BOOL finished) { - if (weakSelf.didHideByDimmingViewTappedBlock) { - weakSelf.didHideByDimmingViewTappedBlock(); - } - }]; - } -} - -#pragma mark - ContentView - -- (void)setContentViewController:(UIViewController *)contentViewController { - _contentViewController = contentViewController; - self.contentView = contentViewController.view; -} - -#pragma mark - Showing and Hiding - -- (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:^{ - self.dimmingView.alpha = 1.0; - self.contentView.alpha = 1.0; - } completion:^(BOOL finished) { - if (completion) { - completion(finished); - } - }]; - - } 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:^{ - self.dimmingView.alpha = 1.0; - self.contentView.transform = CGAffineTransformMakeScale(1, 1); - } completion:^(BOOL finished) { - self.contentView.transform = CGAffineTransformIdentity; - if (completion) { - completion(finished); - } - }]; - - } 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:^{ - self.dimmingView.alpha = 1.0; - self.contentView.transform = CGAffineTransformIdentity; - } completion:^(BOOL finished) { - if (completion) { - completion(finished); - } - }]; - } -} - -- (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { - // 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.windowLevel = UIWindowLevelQMUIAlertView; - self.containerWindow.backgroundColor = UIColorClear;// 避免横竖屏旋转时出现黑色 - } - self.supportedOrientationMask = [QMUIHelper visibleViewController].supportedInterfaceOrientations; - 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:^{ - self.dimmingView.alpha = 0.0; - self.contentView.alpha = 0.0; - } completion:^(BOOL finished) { - if (completion) { - completion(finished); - } - }]; - } else if (self.animationStyle == QMUIModalPresentationAnimationStylePopup) { - [UIView animateWithDuration:.3 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - self.dimmingView.alpha = 0.0; - self.contentView.transform = CGAffineTransformMakeScale(0.0, 0.0); - } completion:^(BOOL finished) { - if (completion) { - self.contentView.transform = CGAffineTransformIdentity; - completion(finished); - } - }]; - } else if (self.animationStyle == QMUIModalPresentationAnimationStyleSlide) { - [UIView animateWithDuration:.3 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - self.dimmingView.alpha = 0.0; - self.contentView.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(self.view.bounds) - CGRectGetMinY(self.contentView.frame)); - } completion:^(BOOL finished) { - if (completion) { - self.contentView.transform = CGAffineTransformIdentity; - completion(finished); - } - }]; - } -} - -- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { - self.disappearAnimated = animated; - self.disappearCompletionBlock = completion; - - BOOL shouldHide = YES; - if ([self.delegate respondsToSelector:@selector(shouldHideModalPresentationViewController:)]) { - shouldHide = [self.delegate shouldHideModalPresentationViewController:self]; - } - if (!shouldHide) { - return; - } - - // window模式下,通过手动触发viewWillDisappear:来做界面消失的逻辑 - if (self.containerWindow) { - [self beginAppearanceTransition:NO animated:animated]; - } -} - -- (void)showInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(BOOL))completion { - self.appearCompletionBlock = completion; - [self loadViewIfNeeded]; - [self beginAppearanceTransition:YES animated:animated]; - [view addSubview:self.view]; - [self endAppearanceTransition]; -} - -- (void)hideInView:(UIView *)view animated:(BOOL)animated completion:(void (^)(BOOL))completion { - self.disappearCompletionBlock = completion; - [self beginAppearanceTransition:NO animated:animated]; - self.hasAlreadyViewWillDisappear = YES; - [self endAppearanceTransition]; -} - -- (CGRect)contentViewFrameForShowing { - CGSize contentViewContainerSize = CGSizeMake(CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(self.contentViewMargins), CGRectGetHeight(self.view.bounds) - self.keyboardHeight - UIEdgeInsetsGetVerticalValue(self.contentViewMargins)); - CGSize contentViewLimitSize = CGSizeMake(fminf(self.maximumContentViewWidth, contentViewContainerSize.width), contentViewContainerSize.height); - CGSize contentViewSize = CGSizeZero; - if ([self.contentViewController respondsToSelector:@selector(preferredContentSizeInModalPresentationViewController:limitSize:)]) { - contentViewSize = [self.contentViewController preferredContentSizeInModalPresentationViewController:self limitSize:contentViewLimitSize]; - } else { - contentViewSize = [self.contentView sizeThatFits:contentViewLimitSize]; - } - contentViewSize.width = fminf(contentViewLimitSize.width, contentViewSize.width); - contentViewSize.height = fminf(contentViewLimitSize.height, contentViewSize.height); - CGRect contentViewFrame = CGRectMake(CGFloatGetCenter(contentViewContainerSize.width, contentViewSize.width) + self.contentViewMargins.left, CGFloatGetCenter(contentViewContainerSize.height, contentViewSize.height) + self.contentViewMargins.top, contentViewSize.width, contentViewSize.height); - - // showingAnimation、hidingAnimation里会通过设置contentView的transform来做动画,所以可能在showing的过程中设置了transform后,系统触发viewDidLayoutSubviews,在viewDidLayoutSubviews里计算的frame又是最终状态的frame,与showing时的transform冲突,导致动画过程中浮层跳动或者位置错误,所以为了保证layout时计算出来的frame与showing/hiding时计算的frame一致,这里给frame应用了transform。但这种处理方法也有局限:如果你在showingAnimation/hidingAnimation里对contentView.frame的更改不是通过修改transform而是直接修改frame来得到结果,那么这里这句CGRectApplyAffineTransform就没用了,viewDidLayoutSubviews里算出来的frame依然会和showingAnimation/hidingAnimation冲突。 - contentViewFrame = CGRectApplyAffineTransform(contentViewFrame, self.contentView.transform); - return contentViewFrame; -} - -#pragma mark - Keyboard - -- (void)handleKeyboardWillShow:(NSNotification *)notification { - CGFloat keyboardHeight = [QMUIHelper keyboardHeightWithNotification:notification inView:self.view]; - if (keyboardHeight > 0) { - self.keyboardHeight = keyboardHeight; - [self.view setNeedsLayout]; - } -} - -- (void)handleKeyboardWillHide:(NSNotification *)notification { - self.keyboardHeight = 0; - [self.view setNeedsLayout]; -} - -#pragma mark - 屏幕旋转 - -- (BOOL)shouldAutorotate { - UIViewController *visibleViewController = [QMUIHelper visibleViewController]; - if (visibleViewController != self && [visibleViewController respondsToSelector:@selector(shouldAutorotate)]) { - return [visibleViewController shouldAutorotate]; - } - return YES; -} - -- (UIInterfaceOrientationMask)supportedInterfaceOrientations { - UIViewController *visibleViewController = [QMUIHelper visibleViewController]; - if (visibleViewController != self && [visibleViewController respondsToSelector:@selector(supportedInterfaceOrientations)]) { - return [visibleViewController supportedInterfaceOrientations]; - } - return self.supportedOrientationMask; -} - -@end - -@implementation QMUIModalPresentationViewController (Manager) - -+ (BOOL)isAnyModalPresentationViewControllerVisible { - for (UIWindow *window in [[UIApplication sharedApplication] windows]) { - if ([window isKindOfClass:[QMUIModalPresentationWindow class]] && !window.hidden) { - return YES; - } - } - return NO; -} - -+ (BOOL)hideAllVisibleModalPresentationViewControllerIfCan { - - BOOL hideAllFinally = YES; - - for (UIWindow *window in [[UIApplication sharedApplication] windows]) { - if (![window isKindOfClass:[QMUIModalPresentationWindow class]]) { - continue; - } - - // 存在modalViewController,但并没有显示出来,所以不用处理 - if (window.hidden) { - continue; - } - - // 存在window,但不存在modalViewController,则直接把这个window移除 - if (!window.rootViewController) { - window.hidden = YES; - continue; - } - - QMUIModalPresentationViewController *modalViewController = (QMUIModalPresentationViewController *)window.rootViewController; - BOOL canHide = YES; - if ([modalViewController.delegate respondsToSelector:@selector(shouldHideModalPresentationViewController:)]) { - canHide = [modalViewController.delegate shouldHideModalPresentationViewController:modalViewController]; - } - if (canHide) { - if ([modalViewController.delegate respondsToSelector:@selector(requestHideAllModalPresentationViewController)]) { - [modalViewController.delegate requestHideAllModalPresentationViewController]; - } else { - [modalViewController hideWithAnimated:NO completion:nil]; - } - } else { - // 只要有一个modalViewController正在显示但却无法被隐藏,就返回NO - hideAllFinally = NO; - } - } - - return hideAllFinally; -} - -@end - -@implementation QMUIModalPresentationWindow - -@end - -@implementation UIViewController (QMUIModalPresentationViewController) - -static char kAssociatedObjectKey_ModalPresentationViewController; -- (void)setModalPresentedViewController:(QMUIModalPresentationViewController *)modalPresentedViewController { - objc_setAssociatedObject(self, &kAssociatedObjectKey_ModalPresentationViewController, modalPresentedViewController, OBJC_ASSOCIATION_ASSIGN); -} - -- (QMUIModalPresentationViewController *)modalPresentedViewController { - return (QMUIModalPresentationViewController *)objc_getAssociatedObject(self, &kAssociatedObjectKey_ModalPresentationViewController); -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIMoreOperationController.h b/QMUI/QMUIKit/UIComponents/QMUIMoreOperationController.h deleted file mode 100644 index deb9d77c..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIMoreOperationController.h +++ /dev/null @@ -1,124 +0,0 @@ -// -// QMUIMoreOperationController.h -// qmui -// -// Created by QQMail on 15/1/28. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import -#import -#import "QMUIModalPresentationViewController.h" -#import "QMUIButton.h" - -/// 操作面板上item的类型,QMUIMoreOperationItemTypeImportant类型的item会放到第一行的scrollView,QMUIMoreOperationItemTypeNormal类型的item会放到第二行的scrollView。 -typedef NS_ENUM(NSInteger, QMUIMoreOperationItemType) { - QMUIMoreOperationItemTypeImportant, // 将item放在第一行显示 - QMUIMoreOperationItemTypeNormal // 将item放在第二行显示 -}; - -@class QMUIModalPresentationViewController; -@class QMUIMoreOperationController; -@class QMUIMoreOperationItemView; -@class QMUIButton; - -/// 更多操作面板的delegate。 -@protocol QMUIMoreOperationDelegate - -@optional -/// 即将显示操作面板 -- (void)willPresentMoreOperationController:(QMUIMoreOperationController *)moreOperationController; -/// 已经显示操作面板 -- (void)didPresentMoreOperationController:(QMUIMoreOperationController *)moreOperationController; -/// 即将降下操作面板,cancelled参数是用来区分是否触发了maskView或者cancelButton按钮降下面板还是手动调用hide方法来降下面板。 -- (void)willDismissMoreOperationController:(QMUIMoreOperationController *)moreOperationController cancelled:(BOOL)cancelled; -/// 已经降下操作面板,cancelled参数是用来区分是否触发了maskView或者cancelButton按钮降下面板还是手动调用hide方法来降下面板。 -- (void)didDismissMoreOperationController:(QMUIMoreOperationController *)moreOperationController cancelled:(BOOL)cancelled; -/// 点击了操作面板上的一个item,可以通过参数拿到当前item的index和type -- (void)moreOperationController:(QMUIMoreOperationController *)moreOperationController didSelectItemAtIndex:(NSInteger)buttonIndex type:(QMUIMoreOperationItemType)type; -/// 点击了操作面板上的一个item,可以通过参数拿到当前item的tag -- (void)moreOperationController:(QMUIMoreOperationController *)moreOperationController didSelectItemAtTag:(NSInteger)tag; - -@end - -@interface QMUIMoreOperationItemView : QMUIButton - -@property (nonatomic, assign, readonly) QMUIMoreOperationItemType itemType; - -@end - - -/** - * 更多操作面板。在iOS上是一个比较常见的控件,比如系统的相册分享;或者微信的webview分享都会从底部弹出一个面板。
- * 这个控件一般分为上下两行,第一行会显示比较重要的操作入口,第二行是一些次要的操作入口。 - * QMUIMoreOperationController就是这样的一个控件,可以通过QMUIMoreOperationItemType来设置操作入口要放在第一行还是第二行。 - */ -@interface QMUIMoreOperationController : UIViewController - -@property(nonatomic, strong) UIColor *contentBackgroundColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *contentSeparatorColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *cancelButtonBackgroundColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *cancelButtonTitleColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *cancelButtonSeparatorColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *itemBackgroundColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *itemTitleColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIFont *itemTitleFont UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIFont *cancelButtonFont UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) CGFloat contentEdgeMargin UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) CGFloat contentMaximumWidth UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) CGFloat contentCornerRadius UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) CGFloat itemTitleMarginTop UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) UIEdgeInsets topScrollViewInsets UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) UIEdgeInsets bottomScrollViewInsets UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) CGFloat cancelButtonHeight UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) CGFloat cancelButtonMarginTop UI_APPEARANCE_SELECTOR; - -/// 代理 -@property(nonatomic, weak) id delegate; - -/// 获取当前所有的item -@property(nonatomic, copy, readonly) NSArray *items; - -/// 获取取消按钮 -@property(nonatomic, strong, readonly) QMUIButton *cancelButton; - -/// 更多操作面板是否正在显示 -@property(nonatomic, assign, getter=isShowing, readonly) BOOL showing; -@property(nonatomic, assign, getter=isAnimating, readonly) BOOL animating; - -/// 弹出更多操作面板,一般在init完并且设置好item之后就调用这个接口来显示面板 -- (void)showFromBottom; -/// 与showFromBottom相反 -- (void)hideToBottom; - -/// 下面几个`addItem`方法,是用来往面板里面增加item的 -- (NSInteger)addItemWithTitle:(NSString *)title selectedTitle:(NSString *)selectedTitle image:(UIImage *)image selectedImage:(UIImage *)selectedImage type:(QMUIMoreOperationItemType)itemType tag:(NSInteger)tag; -- (NSInteger)addItemWithTitle:(NSString *)title selectedTitle:(NSString *)selectedTitle image:(UIImage *)image selectedImage:(UIImage *)selectedImage type:(QMUIMoreOperationItemType)itemType; -- (NSInteger)addItemWithTitle:(NSString *)title image:(UIImage *)image type:(QMUIMoreOperationItemType)itemType tag:(NSInteger)tag; -- (NSInteger)addItemWithTitle:(NSString *)title image:(UIImage *)image type:(QMUIMoreOperationItemType)itemType; - -/// 初始化一个item,并通过下面的`insertItem`来将item插入到面板的某个位置 -- (QMUIMoreOperationItemView *)createItemWithTitle:(NSString *)title selectedTitle:(NSString *)selectedTitle image:(UIImage *)image selectedImage:(UIImage *)selectedImage type:(QMUIMoreOperationItemType)itemType tag:(NSInteger)tag; - -/// 将通过上面初始化的一个item插入到某个位置 -- (BOOL)insertItem:(QMUIMoreOperationItemView *)itemView toIndex:(NSInteger)index; - -/// 获取某种类型上的item -- (QMUIMoreOperationItemView *)itemAtIndex:(NSInteger)index type:(QMUIMoreOperationItemType)type; - -/// 获取某个tag的item -- (QMUIMoreOperationItemView *)itemAtTag:(NSInteger)tag; - -/// 下面两个`setItemHidden`方法可以隐藏某一个item -- (void)setItemHidden:(BOOL)hidden index:(NSInteger)index type:(QMUIMoreOperationItemType)type; -/// 同上 -- (void)setItemHidden:(BOOL)hidden tag:(NSInteger)tag; - -@end - - -@interface QMUIMoreOperationController (UIAppearance) - -+ (instancetype)appearance; - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIMoreOperationController.m b/QMUI/QMUIKit/UIComponents/QMUIMoreOperationController.m deleted file mode 100644 index 84c98484..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIMoreOperationController.m +++ /dev/null @@ -1,614 +0,0 @@ -// -// QMUIMoreOperationController.m -// qmui -// -// Created by QQMail on 15/1/28. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIMoreOperationController.h" -#import "QMUICore.h" -#import "CALayer+QMUI.h" -#import "UIControl+QMUI.h" - -#define TagOffset 999 - -@implementation QMUIMoreOperationController (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; - }); -} - -static QMUIMoreOperationController *moreOperationViewControllerAppearance; -+ (instancetype)appearance { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self resetAppearance]; - }); - return moreOperationViewControllerAppearance; -} - -+ (void)resetAppearance { - if (!moreOperationViewControllerAppearance) { - moreOperationViewControllerAppearance = [[QMUIMoreOperationController alloc] init]; - moreOperationViewControllerAppearance.contentBackgroundColor = UIColorWhite; - moreOperationViewControllerAppearance.contentSeparatorColor = UIColorMakeWithRGBA(0, 0, 0, .15f); - moreOperationViewControllerAppearance.cancelButtonBackgroundColor = UIColorWhite; - moreOperationViewControllerAppearance.cancelButtonTitleColor = UIColorBlue; - moreOperationViewControllerAppearance.cancelButtonSeparatorColor = UIColorMakeWithRGBA(0, 0, 0, .15f); - moreOperationViewControllerAppearance.itemBackgroundColor = UIColorClear; - moreOperationViewControllerAppearance.itemTitleColor = UIColorGrayDarken; - moreOperationViewControllerAppearance.itemTitleFont = UIFontMake(11); - moreOperationViewControllerAppearance.cancelButtonFont = UIFontBoldMake(17); - moreOperationViewControllerAppearance.contentEdgeMargin = 10; - moreOperationViewControllerAppearance.contentMaximumWidth = [QMUIHelper screenSizeFor55Inch].width - moreOperationViewControllerAppearance.contentEdgeMargin * 2; - moreOperationViewControllerAppearance.contentCornerRadius = 10; - moreOperationViewControllerAppearance.itemTitleMarginTop = 9; - moreOperationViewControllerAppearance.topScrollViewInsets = UIEdgeInsetsMake(18, 14, 12, 14); - moreOperationViewControllerAppearance.bottomScrollViewInsets = UIEdgeInsetsMake(18, 14, 12, 14); - moreOperationViewControllerAppearance.cancelButtonHeight = 52.0; - moreOperationViewControllerAppearance.cancelButtonMarginTop = 0; - } -} - -@end - - -@interface QMUIMoreOperationItemView () - -@property (nonatomic, assign, readwrite) QMUIMoreOperationItemType itemType; - -@end - - -@implementation QMUIMoreOperationItemView { - NSInteger _tag; -} - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - self.imagePosition = QMUIButtonImagePositionTop; - self.adjustsButtonWhenHighlighted = NO; - self.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; - self.titleLabel.numberOfLines = 0; - self.titleLabel.textAlignment = NSTextAlignmentCenter; - self.imageView.contentMode = UIViewContentModeCenter; - self.imageView.backgroundColor = UIColorClear; - } - return self; -} - -- (void)setHighlighted:(BOOL)highlighted { - [super setHighlighted:highlighted]; - self.imageView.alpha = highlighted ? ButtonHighlightedAlpha : 1; -} - -- (void)setTag:(NSInteger)tag { - _tag = tag + TagOffset; - [super setTag:_tag]; -} - -- (NSInteger)tag { - return _tag - TagOffset; -} - -@end - - -@interface QMUIMoreOperationController () - -@property(nonatomic, strong) UIView *containerView; -@property(nonatomic, strong) UIView *contentView; -@property(nonatomic, strong) UIControl *maskView; -@property(nonatomic, strong) UIScrollView *importantItemsScrollView; -@property(nonatomic, strong) UIScrollView *normalItemsScrollView; - -@property(nonatomic, strong) CALayer *scrollViewDividingLayer; -@property(nonatomic, strong) CALayer *cancelButtonDividingLayer; - -@property(nonatomic, strong) NSMutableArray *importantItems; -@property(nonatomic, strong) NSMutableArray *normalItems; -@property(nonatomic, strong) NSMutableArray *importantShowingItems; -@property(nonatomic, strong) NSMutableArray *normalShowingItems; - -@property(nonatomic, assign, readwrite) BOOL showing; -@property(nonatomic, assign, readwrite) BOOL animating; - -@end -@implementation QMUIMoreOperationController - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - if (moreOperationViewControllerAppearance) { - self.contentBackgroundColor = [QMUIMoreOperationController appearance].contentBackgroundColor; - self.contentSeparatorColor = [QMUIMoreOperationController appearance].contentSeparatorColor; - self.cancelButtonBackgroundColor = [QMUIMoreOperationController appearance].cancelButtonBackgroundColor; - self.cancelButtonTitleColor = [QMUIMoreOperationController appearance].cancelButtonTitleColor; - self.cancelButtonSeparatorColor = [QMUIMoreOperationController appearance].cancelButtonSeparatorColor; - self.itemBackgroundColor = [QMUIMoreOperationController appearance].itemBackgroundColor; - self.itemTitleColor = [QMUIMoreOperationController appearance].itemTitleColor; - self.itemTitleFont = [QMUIMoreOperationController appearance].itemTitleFont; - self.cancelButtonFont = [QMUIMoreOperationController appearance].cancelButtonFont; - self.contentEdgeMargin = [QMUIMoreOperationController appearance].contentEdgeMargin; - self.contentMaximumWidth = [QMUIMoreOperationController appearance].contentMaximumWidth; - self.contentCornerRadius = [QMUIMoreOperationController appearance].contentCornerRadius; - self.itemTitleMarginTop = [QMUIMoreOperationController appearance].itemTitleMarginTop; - self.topScrollViewInsets = [QMUIMoreOperationController appearance].topScrollViewInsets; - self.bottomScrollViewInsets = [QMUIMoreOperationController appearance].bottomScrollViewInsets; - self.cancelButtonHeight = [QMUIMoreOperationController appearance].cancelButtonHeight; - self.cancelButtonMarginTop = [QMUIMoreOperationController appearance].cancelButtonMarginTop; - } - self.importantItems = [[NSMutableArray alloc] init]; - self.normalItems = [[NSMutableArray alloc] init]; - self.importantShowingItems = [[NSMutableArray alloc] init]; - self.normalShowingItems = [[NSMutableArray alloc] init]; - - [self initSubviewsIfNeeded]; -} - -- (void)setContentBackgroundColor:(UIColor *)contentBackgroundColor { - _contentBackgroundColor = contentBackgroundColor; - if (self.contentView) { - self.contentView.backgroundColor = contentBackgroundColor; - } -} - -- (void)setContentSeparatorColor:(UIColor *)contentSeparatorColor { - _contentSeparatorColor = contentSeparatorColor; - if (self.scrollViewDividingLayer) { - self.scrollViewDividingLayer.backgroundColor = contentSeparatorColor.CGColor; - } -} - -- (void)setCancelButtonBackgroundColor:(UIColor *)cancelButtonBackgroundColor { - _cancelButtonBackgroundColor = cancelButtonBackgroundColor; - if (self.cancelButton) { - self.cancelButton.backgroundColor = cancelButtonBackgroundColor; - } -} - -- (void)setCancelButtonTitleColor:(UIColor *)cancelButtonTitleColor { - _cancelButtonTitleColor = cancelButtonTitleColor; - if (self.cancelButton) { - [self.cancelButton setTitleColor:cancelButtonTitleColor forState:UIControlStateNormal]; - [self.cancelButton setTitleColor:[cancelButtonTitleColor colorWithAlphaComponent:ButtonHighlightedAlpha] forState:UIControlStateHighlighted]; - } -} - -- (void)setCancelButtonSeparatorColor:(UIColor *)cancelButtonSeparatorColor { - _cancelButtonSeparatorColor = cancelButtonSeparatorColor; - if (self.cancelButtonDividingLayer) { - self.cancelButtonDividingLayer.backgroundColor = cancelButtonSeparatorColor.CGColor; - } -} - -- (void)setItemBackgroundColor:(UIColor *)itemBackgroundColor { - _itemBackgroundColor = itemBackgroundColor; - for (QMUIMoreOperationItemView *item in [self.importantItems arrayByAddingObjectsFromArray:self.normalItems]) { - item.imageView.backgroundColor = itemBackgroundColor; - } -} - -- (void)setItemTitleColor:(UIColor *)itemTitleColor { - _itemTitleColor = itemTitleColor; - for (QMUIMoreOperationItemView *item in [self.importantItems arrayByAddingObjectsFromArray:self.normalItems]) { - [item setTitleColor:itemTitleColor forState:UIControlStateNormal]; - } -} - -- (void)setItemTitleFont:(UIFont *)itemTitleFont { - _itemTitleFont = itemTitleFont; - for (QMUIMoreOperationItemView *item in [self.importantItems arrayByAddingObjectsFromArray:self.normalItems]) { - item.titleLabel.font = itemTitleFont; - } -} - -- (void)setCancelButtonFont:(UIFont *)cancelButtonFont { - _cancelButtonFont = cancelButtonFont; - if (self.cancelButton) { - self.cancelButton.titleLabel.font = cancelButtonFont; - } -} - -- (void)setContentCornerRadius:(CGFloat)contentCornerRadius { - _contentCornerRadius = contentCornerRadius; - [self updateCornerRadius]; -} - -- (void)setCancelButtonMarginTop:(CGFloat)cancelButtonMarginTop { - _cancelButtonMarginTop = cancelButtonMarginTop; - [self updateCornerRadius]; -} - -- (void)updateCornerRadius { - if (self.cancelButtonMarginTop > 0) { - self.contentView.layer.cornerRadius = self.contentCornerRadius; - self.containerView.layer.cornerRadius = 0; - self.cancelButton.layer.cornerRadius = self.contentCornerRadius; - } else { - self.containerView.layer.cornerRadius = self.contentCornerRadius; - self.contentView.layer.cornerRadius = 0; - self.cancelButton.layer.cornerRadius = 0; - } -} - -- (void)setItemTitleMarginTop:(CGFloat)itemTitleMarginTop { - _itemTitleMarginTop = itemTitleMarginTop; - for (QMUIMoreOperationItemView *item in [self.importantItems arrayByAddingObjectsFromArray:self.normalItems]) { - item.titleEdgeInsets = UIEdgeInsetsMake(itemTitleMarginTop, 0, 0, 0); - } -} - -- (void)initSubviewsIfNeeded { - - self.maskView = [[UIControl alloc] init]; - self.maskView.alpha = 0; - self.maskView.backgroundColor = UIColorMask; - [self.maskView addTarget:self action:@selector(handleMaskControlEvent:) forControlEvents:UIControlEventTouchUpInside]; - - self.containerView = [[UIView alloc] init]; - self.containerView.clipsToBounds = YES; - - self.contentView = [[UIView alloc] init]; - self.contentView.clipsToBounds = YES; - self.contentView.backgroundColor = self.contentBackgroundColor; - - self.scrollViewDividingLayer = [CALayer layer]; - self.scrollViewDividingLayer.hidden = YES; - self.scrollViewDividingLayer.backgroundColor = self.contentSeparatorColor.CGColor; - [self.scrollViewDividingLayer qmui_removeDefaultAnimations]; - - self.importantItemsScrollView = [[UIScrollView alloc] init]; - self.importantItemsScrollView.showsHorizontalScrollIndicator = NO; - self.importantItemsScrollView.showsVerticalScrollIndicator = NO; - - self.normalItemsScrollView = [[UIScrollView alloc] init]; - self.normalItemsScrollView.showsHorizontalScrollIndicator = NO; - self.normalItemsScrollView.showsVerticalScrollIndicator = NO; - self.normalItemsScrollView.hidden = YES; - - _cancelButton = [[QMUIButton alloc] init]; - 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 addTarget:self action:@selector(handleCancelButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - - self.cancelButtonDividingLayer = [CALayer layer]; - self.cancelButtonDividingLayer.backgroundColor = self.cancelButtonSeparatorColor.CGColor; - [self.cancelButtonDividingLayer qmui_removeDefaultAnimations]; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - [self.view addSubview:self.maskView]; - [self.view addSubview:self.containerView]; - [self.containerView addSubview:self.contentView]; - [self.contentView.layer addSublayer:self.scrollViewDividingLayer]; - [self.contentView addSubview:self.importantItemsScrollView]; - [self.contentView addSubview:self.normalItemsScrollView]; - [self.containerView addSubview:self.cancelButton]; - [self.containerView.layer addSublayer:self.cancelButtonDividingLayer]; - [self updateCornerRadius]; -} - -- (NSArray *)items { - return [self.importantItems arrayByAddingObjectsFromArray:self.normalItems]; -} - -- (void)resetShowingItemsArray { - [self.importantShowingItems removeAllObjects]; - [self.normalShowingItems removeAllObjects]; - for (QMUIMoreOperationItemView *item in self.importantItems) { - if (!item.hidden) { - [self.importantShowingItems addObject:item]; - } - } - for (QMUIMoreOperationItemView *item in self.normalItems) { - if (!item.hidden) { - [self.normalShowingItems addObject:item]; - } - } -} - -- (void)viewDidLayoutSubviews { - - [super viewDidLayoutSubviews]; - [self resetShowingItemsArray]; - - self.maskView.frame = self.view.bounds; - - CGFloat layoutOriginY = 0; - CGFloat contentWidth = fmin(CGRectGetWidth(self.view.bounds) - self.contentEdgeMargin * 2, self.contentMaximumWidth); - - UIEdgeInsets importantScrollViewInsets = self.topScrollViewInsets; - UIEdgeInsets normaltScrollViewInsets = self.bottomScrollViewInsets; - - if (self.importantShowingItems.count <= 0 || self.normalShowingItems.count <= 0) { - // 当两个scrollView其中一个没有的时候,需要调整对应的insets - if (self.importantShowingItems.count <= 0) { - normaltScrollViewInsets = UIEdgeInsetsSetTop(normaltScrollViewInsets, importantScrollViewInsets.top); - self.bottomScrollViewInsets = normaltScrollViewInsets; - } - if (self.normalShowingItems.count <= 0) { - importantScrollViewInsets = UIEdgeInsetsSetBottom(importantScrollViewInsets, normaltScrollViewInsets.bottom); - self.topScrollViewInsets = importantScrollViewInsets; - } - } - - BOOL isLargeSreen = CGRectGetWidth(self.view.bounds) > [QMUIHelper screenSizeFor40Inch].width; - NSInteger maxItemCountInScrollView = MAX(self.importantShowingItems.count, self.normalShowingItems.count); - NSInteger itemCountForTotallyVisibleItem = isLargeSreen ? 4 : 3; - - CGFloat itemWidth = flat((contentWidth - fmaxf(UIEdgeInsetsGetHorizontalValue(importantScrollViewInsets), UIEdgeInsetsGetHorizontalValue(normaltScrollViewInsets))) / itemCountForTotallyVisibleItem) - (maxItemCountInScrollView > itemCountForTotallyVisibleItem ? 11 : 0); - - CGFloat itemMaxHeight = 0; - CGFloat itemMaxX = 0; - if (self.importantShowingItems.count > 0) { - self.importantItemsScrollView.hidden = NO; - for (NSInteger i = 0; i < self.importantShowingItems.count; i++) { - QMUIMoreOperationItemView *itemView = [self.importantShowingItems objectAtIndex:i]; - [itemView sizeToFit]; - itemView.frame = CGRectFlatted(CGRectMake(itemWidth * i, 0, itemWidth, CGRectGetHeight(itemView.bounds))); - itemMaxX = CGRectGetMaxX(itemView.frame); - if (CGRectGetHeight(itemView.bounds) > itemMaxHeight) { - itemMaxHeight = CGRectGetHeight(itemView.bounds); - } - } - self.importantItemsScrollView.contentSize = CGSizeMake(flat(itemMaxX), flat(itemMaxHeight)); - self.importantItemsScrollView.contentInset = importantScrollViewInsets; - self.importantItemsScrollView.contentOffset = CGPointMake(-self.importantItemsScrollView.contentInset.left, -self.importantItemsScrollView.contentInset.top); - self.importantItemsScrollView.frame = CGRectFlatted(CGRectMake(0, 0, contentWidth, UIEdgeInsetsGetVerticalValue(self.importantItemsScrollView.contentInset) + self.importantItemsScrollView.contentSize.height)); - layoutOriginY = CGRectGetMaxY(self.importantItemsScrollView.frame); - } else { - self.importantItemsScrollView.hidden = YES; - } - - itemMaxHeight = 0; - itemMaxX = 0; - if (self.normalShowingItems.count > 0) { - self.normalItemsScrollView.hidden = NO; - self.scrollViewDividingLayer.hidden = !(self.importantShowingItems.count > 0); - self.scrollViewDividingLayer.frame = CGRectFlatted(CGRectMake(0, layoutOriginY, contentWidth, PixelOne)); - layoutOriginY = CGRectGetMaxY(self.scrollViewDividingLayer.frame); - for (NSInteger i = 0; i < self.normalShowingItems.count; i++) { - QMUIMoreOperationItemView *itemView = [self.normalShowingItems objectAtIndex:i]; - [itemView sizeToFit]; - itemView.frame = CGRectFlatted(CGRectMake(itemWidth * i, 0, itemWidth, CGRectGetHeight(itemView.bounds))); - itemMaxX = CGRectGetMaxX(itemView.frame); - if (CGRectGetHeight(itemView.bounds) > itemMaxHeight) { - itemMaxHeight = CGRectGetHeight(itemView.bounds); - } - } - self.normalItemsScrollView.contentSize = CGSizeMake(flat(itemMaxX), flat(itemMaxHeight)); - self.normalItemsScrollView.contentInset = normaltScrollViewInsets; - self.normalItemsScrollView.frame = CGRectFlatted(CGRectMake(0, layoutOriginY, contentWidth, UIEdgeInsetsGetVerticalValue(self.normalItemsScrollView.contentInset) + self.normalItemsScrollView.contentSize.height)); - self.normalItemsScrollView.contentOffset = CGPointMake(-self.normalItemsScrollView.contentInset.left, -self.normalItemsScrollView.contentInset.top); - layoutOriginY = CGRectGetMaxY(self.normalItemsScrollView.frame); - } else { - self.normalItemsScrollView.hidden = YES; - self.scrollViewDividingLayer.hidden = YES; - } - - self.contentView.frame = CGRectFlatted(CGRectMake(0, 0, contentWidth, layoutOriginY)); - layoutOriginY = CGRectGetMaxY(self.contentView.frame); - - self.cancelButtonDividingLayer.hidden = self.cancelButtonMarginTop > 0; - self.cancelButtonDividingLayer.frame = CGRectFlatted(CGRectMake(0, layoutOriginY + self.cancelButtonMarginTop, contentWidth, PixelOne)); - self.cancelButton.frame = CGRectFlatted(CGRectMake(0, CGRectGetMinY(self.cancelButtonDividingLayer.frame), contentWidth, self.cancelButtonHeight)); - - self.containerView.frame = CGRectFlatted(CGRectMake((CGRectGetWidth(self.view.bounds) - contentWidth) / 2, - CGRectGetHeight(self.view.bounds) - CGRectGetMaxY(self.cancelButton.frame) - self.contentEdgeMargin, - contentWidth, - CGRectGetMaxY(self.cancelButton.frame))); -} - -- (void)showFromBottom { - if (self.showing || self.animating) { - return; - } - - __weak __typeof(self)weakSelf = self; - - QMUIModalPresentationViewController *modalPresentationViewController = [[QMUIModalPresentationViewController alloc] init]; - modalPresentationViewController.maximumContentViewWidth = CGFLOAT_MAX; - modalPresentationViewController.contentViewMargins = UIEdgeInsetsZero; - modalPresentationViewController.dimmingView = nil; - modalPresentationViewController.contentViewController = self; - - modalPresentationViewController.showingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewFrame, void(^completion)(BOOL finished)) { - - if ([weakSelf.delegate respondsToSelector:@selector(willPresentMoreOperationController:)]) { - [weakSelf.delegate willPresentMoreOperationController:weakSelf]; - } - - weakSelf.containerView.frame = CGRectSetY(weakSelf.containerView.frame, CGRectGetHeight(weakSelf.view.bounds)); - [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^(void) { - weakSelf.maskView.alpha = 1; - weakSelf.containerView.frame = CGRectSetY(weakSelf.containerView.frame, CGRectGetHeight(weakSelf.view.bounds) - CGRectGetHeight(weakSelf.containerView.frame) - weakSelf.contentEdgeMargin); - } completion:^(BOOL finished) { - weakSelf.showing = YES; - weakSelf.animating = NO; - if ([weakSelf.delegate respondsToSelector:@selector(didPresentMoreOperationController:)]) { - [weakSelf.delegate didPresentMoreOperationController:weakSelf]; - } - if (completion) { - completion(finished); - } - }]; - }; - - modalPresentationViewController.hidingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, void(^completion)(BOOL finished)) { - [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^(void) { - weakSelf.maskView.alpha = 0; - weakSelf.containerView.frame = CGRectSetY(weakSelf.containerView.frame, CGRectGetHeight(containerBounds)); - } completion:^(BOOL finished) { - if (completion) { - completion(finished); - } - }]; - }; - - self.animating = YES; - [modalPresentationViewController showWithAnimated:YES completion:NULL]; -} - -- (void)hideToBottom { - [self hideToBottomCancelled:NO]; -} - -- (void)hideToBottomCancelled:(BOOL)cancelled { - - if (!self.showing || self.animating) { - return; - } - self.animating = YES; - - __weak __typeof(self)weakSelf = self; - - if ([self.delegate respondsToSelector:@selector(willDismissMoreOperationController:cancelled:)]) { - [self.delegate willDismissMoreOperationController:self cancelled:cancelled]; - } - - [self.modalPresentedViewController hideWithAnimated:YES completion:^(BOOL finished) { - weakSelf.showing = NO; - weakSelf.animating = NO; - if ([weakSelf.delegate respondsToSelector:@selector(didDismissMoreOperationController:cancelled:)]) { - [weakSelf.delegate didDismissMoreOperationController:weakSelf cancelled:cancelled]; - } - }]; -} - -- (void)handleCancelButtonEvent:(id)sender { - [self hideToBottomCancelled:YES]; -} - -- (void)handleMaskControlEvent:(id)sender { - [self hideToBottomCancelled:YES]; -} - -- (NSInteger)addItemWithTitle:(NSString *)title selectedTitle:(NSString *)selectedTitle image:(UIImage *)image selectedImage:(UIImage *)selectedImage type:(QMUIMoreOperationItemType)itemType tag:(NSInteger)tag { - QMUIMoreOperationItemView *itemView = [self createItemWithTitle:title selectedTitle:selectedTitle image:image selectedImage:selectedImage type:itemType tag:tag]; - if (itemView.itemType == QMUIMoreOperationItemTypeImportant) { - return [self insertItem:itemView toIndex:self.importantItems.count] ? [self.importantItems indexOfObject:itemView] : -1; - } else if (itemView.itemType == QMUIMoreOperationItemTypeNormal) { - return [self insertItem:itemView toIndex:self.normalItems.count] ? [self.normalItems indexOfObject:itemView] : -1; - } - return -1; -} - -- (NSInteger)addItemWithTitle:(NSString *)title image:(UIImage *)image type:(QMUIMoreOperationItemType)itemType tag:(NSInteger)tag { - return [self addItemWithTitle:title selectedTitle:title image:image selectedImage:image type:itemType tag:tag]; -} - -- (NSInteger)addItemWithTitle:(NSString *)title selectedTitle:(NSString *)selectedTitle image:(UIImage *)image selectedImage:(UIImage *)selectedImage type:(QMUIMoreOperationItemType)itemType { - return [self addItemWithTitle:title selectedTitle:selectedTitle image:image selectedImage:selectedImage type:itemType tag:-1]; -} - -- (NSInteger)addItemWithTitle:(NSString *)title image:(UIImage *)image type:(QMUIMoreOperationItemType)itemType { - return [self addItemWithTitle:title selectedTitle:title image:image selectedImage:image type:itemType tag:-1]; -} - -- (QMUIMoreOperationItemView *)createItemWithTitle:(NSString *)title selectedTitle:(NSString *)selectedTitle image:(UIImage *)image selectedImage:(UIImage *)selectedImage type:(QMUIMoreOperationItemType)itemType tag:(NSInteger)tag { - QMUIMoreOperationItemView *itemView = [[QMUIMoreOperationItemView alloc] init]; - itemView.itemType = itemType; - itemView.titleLabel.font = self.itemTitleFont; - itemView.titleEdgeInsets = UIEdgeInsetsMake(self.itemTitleMarginTop, 0, 0, 0); - [itemView setImage:image forState:UIControlStateNormal]; - [itemView setImage:selectedImage forState:UIControlStateSelected]; - [itemView setImage:selectedImage forState:UIControlStateHighlighted|UIControlStateSelected]; - [itemView setTitle:title forState:UIControlStateNormal]; - [itemView setTitle:selectedTitle forState:UIControlStateHighlighted|UIControlStateSelected]; - [itemView setTitle:selectedTitle forState:UIControlStateSelected]; - [itemView setTitleColor:self.itemTitleColor forState:UIControlStateNormal]; - itemView.imageView.backgroundColor = self.itemBackgroundColor; - itemView.tag = tag; - [itemView addTarget:self action:@selector(handleButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - return itemView; -} - -- (BOOL)insertItem:(QMUIMoreOperationItemView *)itemView toIndex:(NSInteger)index { - if (itemView.itemType == QMUIMoreOperationItemTypeImportant) { - [self.importantItems insertObject:itemView atIndex:index]; - [self.importantItemsScrollView addSubview:itemView]; - return YES; - } else if (itemView.itemType == QMUIMoreOperationItemTypeNormal) { - [self.normalItems insertObject:itemView atIndex:index]; - [self.normalItemsScrollView addSubview:itemView]; - return YES; - } - return NO; -} - -- (QMUIMoreOperationItemView *)itemAtIndex:(NSInteger)index type:(QMUIMoreOperationItemType)type { - if (type == QMUIMoreOperationItemTypeImportant) { - return [self.importantItems objectAtIndex:index]; - } else { - return [self.normalItems objectAtIndex:index]; - } -} - -- (QMUIMoreOperationItemView *)itemAtTag:(NSInteger)tag { - QMUIMoreOperationItemView *item = (QMUIMoreOperationItemView *)[self.importantItemsScrollView viewWithTag:tag + TagOffset]; - if (!item) { - item = (QMUIMoreOperationItemView *)[self.normalItemsScrollView viewWithTag:tag + TagOffset]; - } - return item; -} - -- (void)setItemHidden:(BOOL)hidden index:(NSInteger)index type:(QMUIMoreOperationItemType)type { - QMUIMoreOperationItemView *item = [self itemAtIndex:index type:type]; - item.hidden = hidden; -} - -- (void)setItemHidden:(BOOL)hidden tag:(NSInteger)tag { - QMUIMoreOperationItemView *item = [self itemAtTag:tag]; - item.hidden = hidden; -} - -- (void)handleButtonClick:(id)sender { - QMUIMoreOperationItemView *item = sender; - NSUInteger index; - QMUIMoreOperationItemType itemType; - if (item.superview == self.importantItemsScrollView) { - index = [self.importantItems indexOfObject:item]; - itemType = QMUIMoreOperationItemTypeImportant; - } else { - index = [self.normalItems indexOfObject:item]; - itemType = QMUIMoreOperationItemTypeNormal; - } - NSInteger tag = item.tag; - if ([self.delegate respondsToSelector:@selector(moreOperationController:didSelectItemAtIndex:type:)]) { - [self.delegate moreOperationController:self didSelectItemAtIndex:index type:itemType]; - } - if ([self.delegate respondsToSelector:@selector(moreOperationController:didSelectItemAtTag:)]) { - [self.delegate moreOperationController:self didSelectItemAtTag:tag]; - } -} - -#pragma mark - - -- (CGSize)preferredContentSizeInModalPresentationViewController:(QMUIModalPresentationViewController *)controller limitSize:(CGSize)limitSize { - return controller.view.bounds.size; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUINavigationTitleView.h b/QMUI/QMUIKit/UIComponents/QMUINavigationTitleView.h deleted file mode 100644 index 41c5e0d6..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUINavigationTitleView.h +++ /dev/null @@ -1,148 +0,0 @@ -// -// QMUINavigationTitleView.h -// qmui -// -// Created by QQMail on 14-7-2. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import -#import "QMUIButton.h" - -@class QMUINavigationTitleView; - -@protocol QMUINavigationTitleViewDelegate - -@optional - -/** - 点击 titleView 后的回调,只需设置 titleView.userInteractionEnabled = YES 后即可使用。不过一般都用于配合 QMUINavigationTitleViewAccessoryTypeDisclosureIndicator。 - - @param titleView 被点击的 titleView - @param isActive titleView 是否处于活跃状态(所谓的活跃,对应右边的箭头而言,就是点击后箭头向上的状态) - */ -- (void)didTouchTitleView:(QMUINavigationTitleView *)titleView isActive:(BOOL)isActive; - -/** - titleView 的活跃状态发生变化时会被调用,也即 [titleView setActive:] 被调用时。 - - @param active 是否处于活跃状态 - @param titleView 变换状态的 titleView - */ -- (void)didChangedActive:(BOOL)active forTitleView:(QMUINavigationTitleView *)titleView; - -@end - -/// 设置title和subTitle的布局方式,默认是水平布局。 -typedef NS_ENUM(NSInteger, QMUINavigationTitleViewStyle) { - QMUINavigationTitleViewStyleDefault, // 水平 - QMUINavigationTitleViewStyleSubTitleVertical // 垂直 -}; - -/// 设置titleView的样式,默认没有任何修饰 -typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) { - QMUINavigationTitleViewAccessoryTypeNone, // 默认 - QMUINavigationTitleViewAccessoryTypeDisclosureIndicator // 有下拉箭头 -}; - - -/** - * 可作为navgationItem.titleView 的标题控件。 - * - * 支持主副标题,且可控制主副标题的布局方式(水平或垂直);支持在左边显示loading,在右边显示accessoryView(如箭头)。 - * - * 默认情况下 titleView 是不支持点击的,需要支持点击的情况下,请把 `userInteractionEnabled` 设为 `YES`。 - * - * 若要监听 titleView 的点击事件,有两种方法: - * - * 1. 使用 UIControl 默认的 addTarget:action:forControlEvents: 方式。这种适用于单纯的点击,不需要涉及到状态切换等。 - * 2. 使用 QMUINavigationTitleViewDelegate 提供的接口。这种一般配合 titleView.accessoryType 来使用,这样就不用自己去做 accessoryView 的旋转、active 状态的维护等。 - */ -@interface QMUINavigationTitleView : UIControl - -@property(nonatomic, weak) id delegate; -@property(nonatomic, assign) QMUINavigationTitleViewStyle style; -@property(nonatomic, assign, getter=isActive) BOOL active; - -#pragma mark - Titles - -@property(nonatomic, strong, readonly) UILabel *titleLabel; -@property(nonatomic, copy) NSString *title; - -@property(nonatomic, strong, readonly) UILabel *subtitleLabel; -@property(nonatomic, copy) NSString *subtitle; - -/// 水平布局下的标题字体,默认为 NavBarTitleFont -@property(nonatomic, strong) UIFont *horizontalTitleFont UI_APPEARANCE_SELECTOR; - -/// 水平布局下的副标题的字体,默认为 NavBarTitleFont -@property(nonatomic, strong) UIFont *horizontalSubtitleFont UI_APPEARANCE_SELECTOR; - -/// 垂直布局下的标题字体,默认为 UIFontMake(15) -@property(nonatomic, strong) UIFont *verticalTitleFont UI_APPEARANCE_SELECTOR; - -/// 垂直布局下的副标题字体,默认为 UIFontLightMake(12) -@property(nonatomic, strong) UIFont *verticalSubtitleFont UI_APPEARANCE_SELECTOR; - -/// 标题的上下左右间距,当标题不显示时,计算大小及布局时也不考虑这个间距,默认为 UIEdgeInsetsZero -@property(nonatomic, assign) UIEdgeInsets titleEdgeInsets UI_APPEARANCE_SELECTOR; - -/// 副标题的上下左右间距,当副标题不显示时,计算大小及布局时也不考虑这个间距,默认为 UIEdgeInsetsZero -@property(nonatomic, assign) UIEdgeInsets subtitleEdgeInsets UI_APPEARANCE_SELECTOR; - -#pragma mark - Loading - -@property(nonatomic, strong, readonly) UIActivityIndicatorView *loadingView; - -/* - * 设置是否需要loading,只有开启了这个属性,loading才有可能显示出来。默认值为NO。 - */ -@property(nonatomic, assign) BOOL needsLoadingView; - -/* - * `needsLoadingView`开启之后,通过这个属性来控制loading的显示和隐藏,默认值为YES - * - * @see needsLoadingView - */ -@property(nonatomic, assign) BOOL loadingViewHidden; - -/* - * 如果为YES则title居中,loading放在title的左边,title右边有一个跟左边loading一样大的占位空间;如果为NO,loading和title整体居中。默认值为YES。 - */ -@property(nonatomic, assign) BOOL needsLoadingPlaceholderSpace; - -@property(nonatomic, assign) CGSize loadingViewSize UI_APPEARANCE_SELECTOR; - -/* - * 控制loading距离右边的距离 - */ -@property(nonatomic, assign) CGFloat loadingViewMarginRight UI_APPEARANCE_SELECTOR; - -#pragma mark - Accessory - -/* - * 当accessoryView不为空时,QMUINavigationTitleViewAccessoryType设置无效,一直都是None - */ -@property(nonatomic, strong) UIView *accessoryView; - -/* - * 只有当accessoryView为空时才有效 - */ -@property(nonatomic, assign) QMUINavigationTitleViewAccessoryType accessoryType; - -/* - * 用于微调accessoryView的位置 - */ -@property(nonatomic, assign) CGPoint accessoryViewOffset UI_APPEARANCE_SELECTOR; - -/* - * 如果为YES则title居中,`accessoryView`放在title的左边或右边;如果为NO,`accessoryView`和title整体居中。默认值为NO。 - */ -@property(nonatomic, assign) BOOL needsAccessoryPlaceholderSpace; - -/* - * 初始化方法 - */ -- (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style; - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUINavigationTitleView.m b/QMUI/QMUIKit/UIComponents/QMUINavigationTitleView.m deleted file mode 100644 index 12c084b4..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUINavigationTitleView.m +++ /dev/null @@ -1,573 +0,0 @@ -// -// QMUINavigationTitleView.m -// qmui -// -// Created by QQMail on 14-7-2. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUINavigationTitleView.h" -#import "QMUICore.h" -#import "UIImage+QMUI.h" -#import "UILabel+QMUI.h" -#import "UIActivityIndicatorView+QMUI.h" -#import "UIView+QMUI.h" - -@interface UINavigationBar (QMUI) - -@end - -@implementation UINavigationBar (QMUI) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - ReplaceMethod([self class], @selector(layoutSubviews), @selector(qmui_navigationBarLayoutSubviews)); - }); -} - -- (void)qmui_navigationBarLayoutSubviews { - QMUINavigationTitleView *titleView = (QMUINavigationTitleView *)self.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) { -// NSLog(@"【%@】修正布局前\ntitleView = %@", NSStringFromClass(titleView.class), titleView); - CGFloat titleViewMinY = flat(CGRectGetMinY(titleView.frame) - ((titleViewSize.height - CGRectGetHeight(titleView.bounds)) / 2.0));// 系统对titleView的y值布局是flat,注意,不能改,改了要测试 - titleView.frame = CGRectMake(CGRectGetMinX(titleView.frame), titleViewMinY, fminf(titleViewMaximumWidth, titleViewSize.width), titleViewSize.height); -// NSLog(@"【%@】修正布局后\ntitleView = %@", NSStringFromClass(titleView.class), titleView); - } - } else { - titleView = nil; - } - - [self qmui_navigationBarLayoutSubviews]; - - if (titleView) { -// NSLog(@"【%@】系统布局后\ntitleView = %@", NSStringFromClass(titleView.class), titleView); - } -} - -@end - -@interface QMUINavigationTitleView () - -@property(nonatomic, assign) BOOL accessoryViewAnimating; -@property(nonatomic, assign) CGSize titleLabelSize; -@property(nonatomic, assign) CGSize subtitleLabelSize; -@property(nonatomic, strong) UIImageView *accessoryTypeView; -@end - -@implementation QMUINavigationTitleView - -#pragma mark - 初始化 - -- (instancetype)initWithFrame:(CGRect)frame { - return [self initWithStyle:QMUINavigationTitleViewStyleDefault frame:frame]; -} - -- (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style { - return [self initWithStyle:style frame:CGRectZero]; -} - -- (instancetype)initWithStyle:(QMUINavigationTitleViewStyle)style frame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - - [self addTarget:self action:@selector(handleTouchTitleViewEvent) forControlEvents:UIControlEventTouchUpInside]; - - _titleLabel = [[UILabel alloc] init]; - self.titleLabel.textAlignment = NSTextAlignmentCenter; - self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; - [self addSubview:self.titleLabel]; - - _subtitleLabel = [[UILabel alloc] init]; - self.subtitleLabel.textAlignment = NSTextAlignmentCenter; - self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingTail; - [self addSubview:self.subtitleLabel]; - - self.userInteractionEnabled = NO; - self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter; - self.style = style; - self.needsLoadingView = NO; - self.loadingViewHidden = YES; - self.needsAccessoryPlaceholderSpace = NO; - self.needsLoadingPlaceholderSpace = YES; - self.accessoryType = QMUINavigationTitleViewAccessoryTypeNone; - - QMUINavigationTitleView *appearance = [QMUINavigationTitleView appearance]; - self.loadingViewSize = appearance.loadingViewSize; - self.loadingViewMarginRight = appearance.loadingViewMarginRight; - self.horizontalTitleFont = appearance.horizontalTitleFont; - self.horizontalSubtitleFont = appearance.horizontalSubtitleFont; - self.verticalTitleFont = appearance.verticalTitleFont; - self.verticalSubtitleFont = appearance.verticalSubtitleFont; - self.accessoryViewOffset = appearance.accessoryViewOffset; - self.tintColor = NavBarTitleColor; - } - return self; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"%@, title = %@, subtitle = %@", [super description], self.title, self.subtitle]; -} - -#pragma mark - 布局 - -- (void)refreshLayout { - [self.superview setNeedsLayout]; - [self setNeedsLayout]; -} - -- (void)updateTitleLabelSize { - if (self.titleLabel.text.length > 0) { - // 这里用 CGSizeCeil 是特地保证 titleView 的 sizeThatFits 计算出来宽度是 pt 取整,这样在 layoutSubviews 我们以 px 取整时,才能保证不会出现水平居中时出现半像素的问题,然后由于我们对半像素会认为一像素,所以导致总体宽度多了一像素,从而导致文字布局可能出现缩略... - self.titleLabelSize = CGSizeCeil([self.titleLabel sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]); - } else { - self.titleLabelSize = CGSizeZero; - } -} - -- (void)updateSubtitleLabelSize { - if (self.subtitleLabel.text.length > 0) { - // 这里用 CGSizeCeil 是特地保证 titleView 的 sizeThatFits 计算出来宽度是 pt 取整,这样在 layoutSubviews 我们以 px 取整时,才能保证不会出现水平居中时出现半像素的问题,然后由于我们对半像素会认为一像素,所以导致总体宽度多了一像素,从而导致文字布局可能出现缩略... - self.subtitleLabelSize = CGSizeCeil([self.subtitleLabel sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]); - } else { - self.subtitleLabelSize = CGSizeZero; - } -} - -- (CGSize)loadingViewSpacingSize { - if (self.needsLoadingView) { - return CGSizeMake(self.loadingViewSize.width + self.loadingViewMarginRight, self.loadingViewSize.height); - } - return CGSizeZero; -} - -- (CGSize)loadingViewSpacingSizeIfNeedsPlaceholder { - return CGSizeMake([self loadingViewSpacingSize].width * (self.needsLoadingPlaceholderSpace ? 2 : 1), [self loadingViewSpacingSize].height); -} - -- (CGSize)accessorySpacingSize { - if (self.accessoryView || self.accessoryTypeView) { - UIView *view = self.accessoryView ?: self.accessoryTypeView; - return CGSizeMake(CGRectGetWidth(view.bounds) + self.accessoryViewOffset.x, CGRectGetHeight(view.bounds)); - } - return CGSizeZero; -} - -- (CGSize)accessorySpacingSizeIfNeedesPlaceholder { - return CGSizeMake([self accessorySpacingSize].width * (self.needsAccessoryPlaceholderSpace ? 2 : 1), [self accessorySpacingSize].height); -} - -- (UIEdgeInsets)titleEdgeInsetsIfShowingTitleLabel { - return CGSizeIsEmpty(self.titleLabelSize) ? UIEdgeInsetsZero : self.titleEdgeInsets; -} - -- (UIEdgeInsets)subtitleEdgeInsetsIfShowingSubtitleLabel { - return CGSizeIsEmpty(self.subtitleLabelSize) ? UIEdgeInsetsZero : self.subtitleEdgeInsets; -} - -- (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); - - size.width = fmaxf(firstLineWidth, secondLineWidth); - - size.height = self.titleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsetsIfShowingTitleLabel) + self.subtitleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel); - return CGSizeFlatted(size); - } else { - CGSize size = CGSizeZero; - size.width = self.titleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsetsIfShowingTitleLabel) + self.subtitleLabelSize.width + UIEdgeInsetsGetHorizontalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel); - size.width += [self loadingViewSpacingSizeIfNeedsPlaceholder].width + [self accessorySpacingSizeIfNeedesPlaceholder].width; - size.height = fmaxf(self.titleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsetsIfShowingTitleLabel), self.subtitleLabelSize.height + UIEdgeInsetsGetVerticalValue(self.subtitleEdgeInsetsIfShowingSubtitleLabel)); - size.height = fmaxf(size.height, [self loadingViewSpacingSizeIfNeedsPlaceholder].height); - size.height = fmaxf(size.height, [self accessorySpacingSizeIfNeedesPlaceholder].height); - return CGSizeFlatted(size); - } -} - -- (CGSize)sizeThatFits:(CGSize)size { - CGSize resultSize = [self contentSize]; - return resultSize; -} - -- (void)layoutSubviews { - - if (CGSizeIsEmpty(self.bounds.size)) { - NSLog(@"%@, layoutSubviews, size = %@", NSStringFromClass([self class]), NSStringFromCGSize(self.bounds.size)); - return; - } - - if (self.accessoryViewAnimating) { - return; - } - - [super layoutSubviews]; - - BOOL alignLeft = self.contentHorizontalAlignment == UIControlContentHorizontalAlignmentLeft; - BOOL alignRight = self.contentHorizontalAlignment == UIControlContentHorizontalAlignmentRight; - - // 通过sizeThatFit计算出来的size,如果大于可使用的最大宽度,则会被系统改为最大限制的最大宽度 - CGSize maxSize = self.bounds.size; - - // 实际内容的size,小于等于maxSize - CGSize contentSize = [self contentSize]; - contentSize.width = fminf(maxSize.width, contentSize.width); - contentSize.height = fminf(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); - } - - // 计算loading占的单边宽度 - CGFloat loadingViewSpace = [self loadingViewSpacingSize].width; - - // 获取当前accessoryView - UIView *accessoryView = self.accessoryView ?: self.accessoryTypeView; - - // 计算accessoryView占的单边宽度 - CGFloat accessoryViewSpace = [self accessorySpacingSize].width; - - BOOL isTitleLabelShowing = self.titleLabel.text.length > 0; - BOOL isSubtitleLabelShowing = self.subtitleLabel.text.length > 0; - 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) { - - 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 (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; - } - if (isTitleLabelShowing) { - minX += titleEdgeInsets.left; - maxX -= titleEdgeInsets.right; - self.titleLabel.frame = CGRectFlatMake(minX, titleEdgeInsets.top, maxX - minX, self.titleLabelSize.height); - } else { - self.titleLabel.frame = CGRectZero; - } - if (isSubtitleLabelShowing) { - self.subtitleLabel.frame = CGRectFlatMake(subtitleEdgeInsets.left, (isTitleLabelShowing ? CGRectGetMaxY(self.titleLabel.frame) + titleEdgeInsets.bottom : 0) + subtitleEdgeInsets.top, maxSize.width - UIEdgeInsetsGetHorizontalValue(subtitleEdgeInsets), self.subtitleLabelSize.height); - } else { - self.subtitleLabel.frame = CGRectZero; - } - - } else { - - 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 (accessoryView) { - accessoryView.frame = CGRectSetXY(accessoryView.frame, maxX - CGRectGetWidth(accessoryView.bounds), CGFloatGetCenter(maxSize.height, CGRectGetHeight(accessoryView.bounds)) + self.accessoryViewOffset.y); - maxX = CGRectGetMinX(accessoryView.frame) - self.accessoryViewOffset.x; - } - if (isSubtitleLabelShowing) { - maxX -= subtitleEdgeInsets.right; - // 如果当前的 contentSize 就是以这个 label 的最大占位计算出来的,那么就不应该先计算 center 再计算偏移 - CGFloat 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); - maxX = CGRectGetMinX(self.subtitleLabel.frame) - subtitleEdgeInsets.left; - } else { - self.subtitleLabel.frame = CGRectZero; - } - if (isTitleLabelShowing) { - minX += titleEdgeInsets.left; - maxX -= titleEdgeInsets.right; - // 如果当前的 contentSize 就是以这个 label 的最大占位计算出来的,那么就不应该先计算 center 再计算偏移 - CGFloat shouldTitleLabelCenterVertically = self.titleLabelSize.height + UIEdgeInsetsGetVerticalValue(titleEdgeInsets) < contentSize.height; - CGFloat titleLabelMinY = shouldTitleLabelCenterVertically ? CGFloatGetCenter(maxSize.height, self.titleLabelSize.height) + titleEdgeInsets.top - titleEdgeInsets.bottom : titleEdgeInsets.top; - self.titleLabel.frame = CGRectFlatMake(minX, titleLabelMinY, maxX - minX, self.titleLabelSize.height); - } else { - self.titleLabel.frame = CGRectZero; - } - } -} - - -#pragma mark - setter / getter - -- (void)setContentHorizontalAlignment:(UIControlContentHorizontalAlignment)contentHorizontalAlignment { - [super setContentHorizontalAlignment:contentHorizontalAlignment]; - [self refreshLayout]; -} - -- (void)setNeedsLoadingPlaceholderSpace:(BOOL)needsLoadingPlaceholderSpace { - _needsLoadingPlaceholderSpace = needsLoadingPlaceholderSpace; - [self refreshLayout]; -} - -- (void)setNeedsAccessoryPlaceholderSpace:(BOOL)needsAccessoryPlaceholderSpace { - _needsAccessoryPlaceholderSpace = needsAccessoryPlaceholderSpace; - [self refreshLayout]; -} - -- (void)setAccessoryViewOffset:(CGPoint)accessoryViewOffset { - _accessoryViewOffset = accessoryViewOffset; - [self refreshLayout]; -} - -- (void)setLoadingViewMarginRight:(CGFloat)loadingViewMarginRight { - _loadingViewMarginRight = loadingViewMarginRight; - [self refreshLayout]; -} - -- (void)setHorizontalTitleFont:(UIFont *)horizontalTitleFont { - _horizontalTitleFont = horizontalTitleFont; - if (self.style == QMUINavigationTitleViewStyleDefault) { - self.titleLabel.font = horizontalTitleFont; - [self updateTitleLabelSize]; - [self refreshLayout]; - } -} - -- (void)setHorizontalSubtitleFont:(UIFont *)horizontalSubtitleFont { - _horizontalSubtitleFont = horizontalSubtitleFont; - if (self.style == QMUINavigationTitleViewStyleDefault) { - self.subtitleLabel.font = horizontalSubtitleFont; - [self updateSubtitleLabelSize]; - [self refreshLayout]; - } -} - -- (void)setVerticalTitleFont:(UIFont *)verticalTitleFont { - _verticalTitleFont = verticalTitleFont; - if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) { - self.titleLabel.font = verticalTitleFont; - [self updateTitleLabelSize]; - [self refreshLayout]; - } -} - -- (void)setVerticalSubtitleFont:(UIFont *)verticalSubtitleFont { - _verticalSubtitleFont = verticalSubtitleFont; - if (self.style == QMUINavigationTitleViewStyleSubTitleVertical) { - self.subtitleLabel.font = verticalSubtitleFont; - [self updateSubtitleLabelSize]; - [self refreshLayout]; - } -} - -- (void)setTitleEdgeInsets:(UIEdgeInsets)titleEdgeInsets { - _titleEdgeInsets = titleEdgeInsets; - [self refreshLayout]; -} - -- (void)setSubtitleEdgeInsets:(UIEdgeInsets)subtitleEdgeInsets { - _subtitleEdgeInsets = subtitleEdgeInsets; - [self refreshLayout]; -} - -- (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 refreshLayout]; -} - -- (void)setAccessoryType:(QMUINavigationTitleViewAccessoryType)accessoryType { - - // 如果已设置了accessoryView,则accessoryType不生效 - if (self.accessoryView) { - accessoryType = QMUINavigationTitleViewAccessoryTypeNone; - } - - _accessoryType = accessoryType; - - if (accessoryType == QMUINavigationTitleViewAccessoryTypeNone) { - [self.accessoryTypeView removeFromSuperview]; - self.accessoryTypeView = nil; - [self refreshLayout]; - return; - } - - if (!self.accessoryTypeView) { - self.accessoryTypeView = [[UIImageView alloc] init]; - self.accessoryTypeView.contentMode = UIViewContentModeCenter; - [self addSubview:self.accessoryTypeView]; - } - - UIImage *accessoryImage; - if (accessoryType == QMUINavigationTitleViewAccessoryTypeDisclosureIndicator) { - accessoryImage = [NavBarAccessoryViewTypeDisclosureIndicatorImage qmui_imageWithOrientation:UIImageOrientationUp]; - } - - self.accessoryTypeView.image = accessoryImage; - [self.accessoryTypeView sizeToFit]; - [self refreshLayout]; -} - -- (void)setAccessoryView:(UIView *)accessoryView { - if (_accessoryView != accessoryView) { - [_accessoryView removeFromSuperview]; - _accessoryView = nil; - } - if (accessoryView) { - _accessoryView = accessoryView; - self.accessoryType = QMUINavigationTitleViewAccessoryTypeNone; - [self.accessoryView sizeToFit]; - [self addSubview:self.accessoryView]; - } - [self refreshLayout]; -} - -- (void)setNeedsLoadingView:(BOOL)needsLoadingView { - _needsLoadingView = needsLoadingView; - if (needsLoadingView) { - if (!self.loadingView) { - _loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:NavBarActivityIndicatorViewStyle size:self.loadingViewSize]; - self.loadingView.color = self.tintColor; - [self.loadingView stopAnimating]; - [self addSubview:self.loadingView]; - } - } else { - if (self.loadingView) { - [self.loadingView stopAnimating]; - [self.loadingView removeFromSuperview]; - _loadingView = nil; - } - } - [self refreshLayout]; -} - -- (void)setLoadingViewHidden:(BOOL)loadingViewHidden { - _loadingViewHidden = loadingViewHidden; - if (self.needsLoadingView) { - loadingViewHidden ? [self.loadingView stopAnimating] : [self.loadingView startAnimating]; - } - [self refreshLayout]; -} - -- (void)setActive:(BOOL)active { - _active = active; - if ([self.delegate respondsToSelector:@selector(didChangedActive:forTitleView:)]) { - [self.delegate didChangedActive:active forTitleView:self]; - } - if (self.accessoryType == QMUINavigationTitleViewAccessoryTypeDisclosureIndicator) { - // 目前只对默认的accessoryView添加动画 - self.accessoryViewAnimating = YES; - if (active) { - [UIView animateWithDuration:.25f delay:0 options:QMUIViewAnimationOptionsCurveIn animations:^(void){ - self.accessoryTypeView.transform = CGAffineTransformMakeRotation(AngleWithDegrees(-180)); - } completion:^(BOOL finished) { - self.accessoryViewAnimating = NO; - }]; - } else { - [UIView animateWithDuration:.25f delay:0 options:QMUIViewAnimationOptionsCurveIn animations:^(void){ - self.accessoryTypeView.transform = CGAffineTransformMakeRotation(AngleWithDegrees(0.1)); - } completion:^(BOOL finished) { - self.accessoryViewAnimating = NO; - }]; - } - } -} - -#pragma mark - Style & Type - -- (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 refreshLayout]; -} - -- (void)tintColorDidChange { - [super tintColorDidChange]; - - 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; -} - -- (void)handleTouchTitleViewEvent { - BOOL active = !self.active; - if ([self.delegate respondsToSelector:@selector(didTouchTitleView:isActive:)]) { - [self.delegate didTouchTitleView:self isActive:active]; - } - self.active = active; - [self refreshLayout]; -} - -@end - -@interface QMUINavigationTitleView (UIAppearance) - -@end - -@implementation QMUINavigationTitleView (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearance]; - }); -} - -+ (void)setDefaultAppearance { - QMUINavigationTitleView *appearance = [QMUINavigationTitleView appearance]; - appearance.loadingViewSize = CGSizeMake(18, 18); - appearance.loadingViewMarginRight = 3; - appearance.horizontalTitleFont = NavBarTitleFont; - appearance.horizontalSubtitleFont = NavBarTitleFont; - appearance.verticalTitleFont = UIFontMake(15); - appearance.verticalSubtitleFont = UIFontLightMake(12); - appearance.accessoryViewOffset = CGPointMake(3, 0); - appearance.titleEdgeInsets = UIEdgeInsetsZero; - appearance.subtitleEdgeInsets = UIEdgeInsetsZero; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIOrderedDictionary.h b/QMUI/QMUIKit/UIComponents/QMUIOrderedDictionary.h deleted file mode 100644 index da2d084f..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIOrderedDictionary.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// QMUIOrderedDictionary.h -// qmui -// -// Created by MoLice on 16/7/21. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import - -@interface QMUIOrderedDictionary : NSObject - -- (instancetype)initWithKeysAndObjects:(id)firstKey,...; - -@property(readonly) NSUInteger count; -@property(nonatomic, strong, readonly) NSArray *allKeys; - -- (instancetype)objectForKey:(id)key; - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIOrderedDictionary.m b/QMUI/QMUIKit/UIComponents/QMUIOrderedDictionary.m deleted file mode 100644 index 32f15fcb..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIOrderedDictionary.m +++ /dev/null @@ -1,64 +0,0 @@ -// -// QMUIOrderedDictionary.m -// qmui -// -// Created by MoLice on 16/7/21. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIOrderedDictionary.h" - -@interface QMUIOrderedDictionary () - -@property(nonatomic, strong) NSMutableArray *mutableAllKeys; -@property(nonatomic, strong) NSMutableArray *mutableAllValues; -@property(nonatomic, strong) NSMutableDictionary *mutableDictionary; -@end - -@implementation QMUIOrderedDictionary - -- (instancetype)initWithKeysAndObjects:(id)firstKey, ... { - if (self = [super init]) { - self.mutableAllKeys = [[NSMutableArray alloc] init]; - self.mutableAllValues = [[NSMutableArray alloc] init]; - - if (firstKey) { - [self.mutableAllKeys addObject:firstKey]; - - va_list argumentList; - va_start(argumentList, firstKey); - id argument; - NSInteger i = 1; - while ((argument = va_arg(argumentList, id))) { - if (i % 2 == 0) { - [self.mutableAllKeys addObject:argument]; - } else { - [self.mutableAllValues addObject:argument]; - } - i++; - } - va_end(argumentList); - - self.mutableDictionary = [[NSMutableDictionary alloc] initWithObjects:self.mutableAllValues forKeys:self.mutableAllKeys]; - } - } - return self; -} - -- (NSUInteger)count { - return self.mutableDictionary.count; -} - -- (NSArray *)allKeys { - return self.mutableAllKeys; -} - -- (instancetype)objectForKey:(id)key { - return [self.mutableDictionary objectForKey:key]; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"%@, %@", [super description], self.mutableDictionary]; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIPieProgressView.h b/QMUI/QMUIKit/UIComponents/QMUIPieProgressView.h deleted file mode 100644 index 8ee47adc..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIPieProgressView.h +++ /dev/null @@ -1,39 +0,0 @@ -// -// QMUIPieProgressView.h -// qmui -// -// Created by MoLice on 15/9/8. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import - -/** - * 饼状进度条控件 - * - * 使用 `tintColor` 更改进度条饼状部分和边框部分的颜色 - * - * 使用 `backgroundColor` 更改圆形背景色 - * - * 通过 `UIControlEventValueChanged` 来监听进度变化 - */ -@interface QMUIPieProgressView : UIControl - -/** - 进度动画的时长,默认为 0.5 - */ -@property(nonatomic, assign) IBInspectable CFTimeInterval progressAnimationDuration; - -/** - 当前进度值,默认为 0.0。调用 `setProgress:` 相当于调用 `setProgress:animated:NO` - */ -@property(nonatomic, assign) IBInspectable float progress; - -/** - 修改当前的进度,会触发 UIControlEventValueChanged 事件 - - @param progress 当前的进度,取值范围 [0.0-1.0] - @param animated 是否以动画来表现 - */ -- (void)setProgress:(float)progress animated:(BOOL)animated; -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIPieProgressView.m b/QMUI/QMUIKit/UIComponents/QMUIPieProgressView.m deleted file mode 100644 index d7126aea..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIPieProgressView.m +++ /dev/null @@ -1,134 +0,0 @@ -// -// QMUIPieProgressView.m -// qmui -// -// Created by MoLice on 15/9/8. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIPieProgressView.h" -#import "QMUICore.h" - -@interface QMUIPieProgressLayer : CALayer - -@property(nonatomic, strong) UIColor *fillColor; -@property(nonatomic, assign) float progress; -@property(nonatomic, assign) CFTimeInterval progressAnimationDuration; -@property(nonatomic, assign) BOOL shouldChangeProgressWithAnimation; // default is YES -@end - -@implementation QMUIPieProgressLayer -// 加dynamic才能让自定义的属性支持动画 -@dynamic fillColor; -@dynamic progress; - -- (instancetype)init { - if (self = [super init]) { - self.shouldChangeProgressWithAnimation = YES; - } - return self; -} - -+ (BOOL)needsDisplayForKey:(NSString *)key { - return [key isEqualToString:@"progress"] || [super needsDisplayForKey:key]; -} - -- (id)actionForKey:(NSString *)event { - if ([event isEqualToString:@"progress"] && self.shouldChangeProgressWithAnimation) { - CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:event]; - animation.fromValue = [self.presentationLayer valueForKey:event]; - animation.duration = self.progressAnimationDuration; - return animation; - } - return [super actionForKey:event]; -} - -- (void)drawInContext:(CGContextRef)context { - if (CGRectIsEmpty(self.bounds)) { - return; - } - - // 绘制扇形进度区域 - CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); - CGFloat radius = MIN(center.x, center.y); - 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); - - [super drawInContext:context]; -} - -- (void)setFrame:(CGRect)frame { - [super setFrame:frame]; - self.cornerRadius = flat(CGRectGetHeight(frame) / 2); -} - -@end - -@implementation QMUIPieProgressView - -+ (Class)layerClass { - return [QMUIPieProgressLayer class]; -} - -- (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - self.backgroundColor = UIColorClear; - self.tintColor = UIColorBlue; - - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - // 从 xib 初始化的话,在 IB 里设置了 tintColor 也不会触发 tintColorDidChange,所以这里手动调用一下 - [self tintColorDidChange]; - } - return self; -} - -- (void)didInitialized { - self.progress = 0.0; - self.progressAnimationDuration = 0.5; - - self.layer.contentsScale = ScreenScale;// 要显示指定一个倍数 - self.layer.borderWidth = 1.0; - [self.layer setNeedsDisplay]; -} - -- (void)setProgress:(float)progress { - [self setProgress:progress animated:NO]; -} - -- (void)setProgress:(float)progress animated:(BOOL)animated { - _progress = fmaxf(0.0, fminf(1.0, progress)); - QMUIPieProgressLayer *layer = (QMUIPieProgressLayer *)self.layer; - layer.shouldChangeProgressWithAnimation = animated; - layer.progress = _progress; - - [self sendActionsForControlEvents:UIControlEventValueChanged]; -} - -- (void)setProgressAnimationDuration:(CFTimeInterval)progressAnimationDuration { - _progressAnimationDuration = progressAnimationDuration; - self.progressLayer.progressAnimationDuration = progressAnimationDuration; -} - -- (void)tintColorDidChange { - [super tintColorDidChange]; - self.progressLayer.fillColor = self.tintColor; - self.progressLayer.borderColor = self.tintColor.CGColor; -} - -- (QMUIPieProgressLayer *)progressLayer { - return (QMUIPieProgressLayer *)self.layer; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIPopupContainerView.h b/QMUI/QMUIKit/UIComponents/QMUIPopupContainerView.h deleted file mode 100644 index 71bb21a9..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIPopupContainerView.h +++ /dev/null @@ -1,145 +0,0 @@ -// -// QMUIPopupContainerView.h -// qmui -// -// Created by MoLice on 15/12/17. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import -#import "UIControl+QMUI.h" - -typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) { - QMUIPopupContainerViewLayoutDirectionAbove, - QMUIPopupContainerViewLayoutDirectionBelow -}; - -/** - * 带箭头的小tips浮层,自带 imageView 和 textLabel,可展示简单的图文信息。 - * QMUIPopupContainerView 支持以两种方式显示在界面上: - * 1. 添加到某个 UIView 上(适合于 viewController 切换时浮层跟着一起切换的场景),这种场景只能手动隐藏浮层。 - * 2. 在 QMUIPopupContainerView 自带的 UIWindow 里显示(适合于用完就消失的场景,不要涉及界面切换),这种场景支持点击空白地方自动隐藏浮层。 - * - * 使用步骤: - * 1. 调用 init 方法初始化。 - * 2. 选择一种显示方式: - * 2.1 如果要添加到某个 UIView 上,则先设置浮层 hidden = YES,然后调用 addSubview: 把浮层添加到目标 UIView 上。 - * 2.2 如果是轻量的场景用完即走,则 init 完浮层即可,无需设置 hidden,也无需调用 addSubview:,在后面第 4 步里会自动把浮层添加到 UIWindow 上显示出来。 - * 3. 在适当的时机(例如 layoutSubviews: 或 viewDidLayoutSubviews:)调用 layoutWithTargetView: 让浮层参考目标 view 布局,或者调用 layoutWithTargetRectInScreenCoordinate: 让浮层参考基于屏幕坐标系里的一个 rect 来布局。 - * 4. 调用 showWithAnimated: 或 showWithAnimated:completion: 显示浮层。 - * 5. 调用 hideWithAnimated: 或 hideWithAnimated:completion: 隐藏浮层。 - * - * @warning 如果使用方法 2.2,并且没有打开 automaticallyHidesWhenUserTap 属性,则记得在适当的时机(例如 viewWillDisappear:)隐藏浮层。 - * - * 如果默认功能无法满足需求,可继承它重写一个子类,继承要点: - * 1. 初始化时要做的事情请放在 didInitialized 里。 - * 2. 所有 subviews 请加到 contentView 上。 - * 3. 通过重写 sizeThatFitsInContentView:,在里面返回当前 subviews 的大小,控件最终会被布局为这个大小。 - * 4. 在 layoutSubviews: 里,所有 subviews 请相对于 contentView 布局。 - */ - -@interface QMUIPopupContainerView : UIControl { - CAShapeLayer *_backgroundLayer; - CGFloat _arrowMinX; -} - -@property(nonatomic, assign) BOOL debug; - -/// 在浮层显示时,点击空白地方是否要自动隐藏浮层,仅在用方法 2 显示时有效。 -/// 默认为 NO,也即需要手动调用代码去隐藏浮层。 -@property(nonatomic, assign) BOOL automaticallyHidesWhenUserTap; - -/// 所有subview都应该添加到contentView上,默认contentView.userInteractionEnabled = NO,需要事件操作时自行打开 -@property(nonatomic, strong, readonly) UIView *contentView; - -/// 预提供的UIImageView,默认为nil,调用到的时候才初始化 -@property(nonatomic, strong, readonly) UIImageView *imageView; - -/// 预提供的UILabel,默认为nil,调用到的时候才初始化。默认支持多行。 -@property(nonatomic, strong, readonly) UILabel *textLabel; - -/// 圆角矩形气泡内的padding(不包括三角箭头),默认是(8, 8, 8, 8) -@property(nonatomic, assign) UIEdgeInsets contentEdgeInsets UI_APPEARANCE_SELECTOR; - -/// 调整imageView的位置,默认为UIEdgeInsetsZero。top/left正值表示往下/右方偏移,bottom/right仅在对应位置存在下一个子View时生效(例如只有同时存在imageView和textLabel时,imageEdgeInsets.right才会生效)。 -@property(nonatomic, assign) UIEdgeInsets imageEdgeInsets UI_APPEARANCE_SELECTOR; - -/// 调整textLabel的位置,默认为UIEdgeInsetsZero。top/left/bottom/right的作用同imageEdgeInsets -@property(nonatomic, assign) UIEdgeInsets textEdgeInsets UI_APPEARANCE_SELECTOR; - -/// 三角箭头的大小,默认为 CGSizeMake(18, 9) -@property(nonatomic, assign) CGSize arrowSize 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 -@property(nonatomic, assign) CGFloat maximumHeight UI_APPEARANCE_SELECTOR; - -/// 最小高度(指整个控件的高度,而不是contentView部分),默认为0 -@property(nonatomic, assign) CGFloat minimumHeight UI_APPEARANCE_SELECTOR; - -/// 计算布局时期望的默认位置,默认为QMUIPopupContainerViewLayoutDirectionAbove,也即在目标的上方 -@property(nonatomic, assign) QMUIPopupContainerViewLayoutDirection preferLayoutDirection UI_APPEARANCE_SELECTOR; - -/// 最终的布局方向(preferLayoutDirection只是期望的方向,但有可能那个方向已经没有剩余空间可摆放控件了,所以会自动变换) -@property(nonatomic, assign, readonly) QMUIPopupContainerViewLayoutDirection currentLayoutDirection; - -/// 最终布局时箭头距离目标边缘的距离,默认为5 -@property(nonatomic, assign) CGFloat distanceBetweenTargetRect UI_APPEARANCE_SELECTOR; - -/// 最终布局时与父节点的边缘的临界点,默认为(10, 10, 10, 10) -@property(nonatomic, assign) UIEdgeInsets safetyMarginsOfSuperview UI_APPEARANCE_SELECTOR; - -@property(nonatomic, strong) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) 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, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR; - -/** - * 相对于某个 view 布局(布局后箭头不一定会水平居中) - * @param targetView 注意如果这个 targetView 自身的布局发生变化,需要重新调用 layoutWithTargetView:,否则浮层的布局不会自动更新。 - */ -- (void)layoutWithTargetView:(UIView *)targetView; - -/** - * 相对于给定的 itemRect 布局(布局后箭头不一定会水平居中) - * @param targetRect 注意这个 rect 应该是处于屏幕坐标系里的 rect,所以请自行做坐标系转换。 - */ -- (void)layoutWithTargetRectInScreenCoordinate:(CGRect)targetRect; - -- (void)showWithAnimated:(BOOL)animated; -- (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; -- (void)hideWithAnimated:(BOOL)animated; -- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL finished))completion; -- (BOOL)isShowing; - -/** - * 即将隐藏时的回调 - * @argv hidesByUserTap 用于区分此次隐藏是否因为用户手动点击空白区域导致浮层被隐藏 - */ -@property(nonatomic, copy) void (^willHideBlock)(BOOL hidesByUserTap); - -/** - * 已经隐藏后的回调 - * @argv hidesByUserTap 用于区分此次隐藏是否因为用户手动点击空白区域导致浮层被隐藏 - */ -@property(nonatomic, copy) void (^didHideBlock)(BOOL hidesByUserTap); -@end - -@interface QMUIPopupContainerView (UISubclassingHooks) - -/// 子类重写,在初始化时做一些操作 -- (void)didInitialized NS_REQUIRES_SUPER; - -/// 子类重写,告诉父类subviews的合适大小 -- (CGSize)sizeThatFitsInContentView:(CGSize)size; -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIPopupContainerView.m b/QMUI/QMUIKit/UIComponents/QMUIPopupContainerView.m deleted file mode 100644 index 12d7cc57..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIPopupContainerView.m +++ /dev/null @@ -1,647 +0,0 @@ -// -// QMUIPopupContainerView.m -// qmui -// -// Created by MoLice on 15/12/17. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIPopupContainerView.h" -#import "QMUICore.h" -#import "QMUICommonViewController.h" - -@interface QMUIPopupContainerViewWindow : UIWindow - -@end - -@interface QMUIPopContainerViewController : QMUICommonViewController - -@end - -@interface QMUIPopContainerMaskControl : UIControl - -@property(nonatomic, weak) QMUIPopupContainerView *popupContainerView; -@end - -@interface QMUIPopupContainerView (UIAppearance) - -- (void)updateAppearance; -@end - -@interface QMUIPopupContainerView () { - UIImageView *_imageView; - UILabel *_textLabel; -} - -@property(nonatomic, strong) QMUIPopupContainerViewWindow *popupWindow; -@property(nonatomic, weak) UIWindow *previousKeyWindow; -@property(nonatomic, assign) BOOL hidesByUserTap; -@end - -@implementation QMUIPopupContainerView - -- (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (UIView *)superviewIfExist { - BOOL isAddedToCustomView = self.superview && !self.popupWindow; - if (isAddedToCustomView) { - return self.superview; - } - - // https://github.com/QMUI/QMUI_iOS/issues/76 - BOOL shouldLayoutInPopupWindow = self.popupWindow && CGSizeEqualToSize(self.popupWindow.bounds.size, [[[UIApplication sharedApplication] delegate] window].bounds.size); - return shouldLayoutInPopupWindow ? self.popupWindow : [[[UIApplication sharedApplication] delegate] window]; -} - -- (UIImageView *)imageView { - if (!_imageView) { - _imageView = [[UIImageView alloc] init]; - _imageView.contentMode = UIViewContentModeCenter; - [self.contentView addSubview:_imageView]; - } - return _imageView; -} - -- (UILabel *)textLabel { - if (!_textLabel) { - _textLabel = [[UILabel alloc] init]; - _textLabel.font = UIFontMake(12); - _textLabel.textColor = UIColorBlack; - _textLabel.numberOfLines = 0; - [self.contentView addSubview:_textLabel]; - } - return _textLabel; -} - -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { - UIView *result = [super hitTest:point withEvent:event]; - if (result == self.contentView) { - return self; - } - return result; -} - -- (void)setBackgroundColor:(UIColor *)backgroundColor { - _backgroundColor = backgroundColor; - _backgroundLayer.fillColor = _backgroundColor.CGColor; -} - -- (void)setMaskViewBackgroundColor:(UIColor *)maskViewBackgroundColor { - _maskViewBackgroundColor = maskViewBackgroundColor; - if (self.popupWindow) { - self.popupWindow.rootViewController.view.backgroundColor = maskViewBackgroundColor; - } -} - -- (void)setShadowColor:(UIColor *)shadowColor { - _shadowColor = shadowColor; - _backgroundLayer.shadowColor = shadowColor.CGColor; -} - -- (void)setBorderColor:(UIColor *)borderColor { - _borderColor = borderColor; - _backgroundLayer.strokeColor = borderColor.CGColor; -} - -- (void)setBorderWidth:(CGFloat)borderWidth { - _borderWidth = borderWidth; - _backgroundLayer.lineWidth = _borderWidth; -} - -- (void)setCornerRadius:(CGFloat)cornerRadius { - _cornerRadius = cornerRadius; - [self setNeedsLayout]; -} - -- (void)setHighlighted:(BOOL)highlighted { - [super setHighlighted:highlighted]; - if (self.highlightedBackgroundColor) { - _backgroundLayer.fillColor = highlighted ? self.highlightedBackgroundColor.CGColor : self.backgroundColor.CGColor; - } -} - -- (CGSize)sizeThatFits:(CGSize)size { - size.width = fmin(size.width, CGRectGetWidth(self.superviewIfExist.bounds) - UIEdgeInsetsGetHorizontalValue(self.safetyMarginsOfSuperview)); - size.height = fmin(size.height, CGRectGetHeight(self.superviewIfExist.bounds) - UIEdgeInsetsGetVerticalValue(self.safetyMarginsOfSuperview)); - - CGSize contentLimitSize = [self contentSizeInSize:size]; - CGSize 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; - - 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]; - [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))]; - } - - [path addLineToPoint:CGPointMake(rightBottomArcCenter.x, CGRectGetMaxY(roundedRect))]; - [path addArcWithCenter:rightBottomArcCenter radius:cornerRadius startAngle:M_PI * 0.5 endAngle:0.0 clockwise:NO]; - [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))]; - } - [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(fmin(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, flat(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)); - } -} - -- (void)layoutWithTargetView:(UIView *)targetView { - CGRect targetViewFrameInMainWindow = CGRectZero; - UIWindow *mainWindow = [[[UIApplication sharedApplication] delegate] window]; - if (targetView.window == mainWindow) { - targetViewFrameInMainWindow = [targetView convertRect:targetView.bounds toView:targetView.window]; - } else { - CGRect targetViewFrameInLocalWindow = [targetView convertRect:targetView.bounds toView:targetView.window]; - targetViewFrameInMainWindow = [mainWindow convertRect:targetViewFrameInLocalWindow fromWindow:targetView.window]; - } - [self layoutWithTargetRect:targetViewFrameInMainWindow inReferenceWindow:targetView.window]; -} - -- (void)layoutWithTargetRectInScreenCoordinate:(CGRect)targetRect { - [self layoutWithTargetRect:targetRect inReferenceWindow:[[[UIApplication sharedApplication] delegate] window]]; -} - -- (void)layoutWithTargetRect:(CGRect)targetRect inReferenceWindow:(UIWindow *)window { - UIView *superview = self.superviewIfExist; - BOOL isLayoutInWindowMode = !(self.superview && !self.popupWindow); - CGRect superviewBoundsInWindow = isLayoutInWindowMode ? window.bounds : [superview convertRect:superview.bounds toView:window]; - - 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 = fmax(CGRectGetMinX(superviewBoundsInWindow) + self.safetyMarginsOfSuperview.left, a); - - CGFloat tipMaxX = tipMinX + tipSize.width; - if (tipMaxX + self.safetyMarginsOfSuperview.right > CGRectGetMaxX(superviewBoundsInWindow)) { - // 右边超出了 - // 先尝试把右边超出的部分往左边挪,看是否会令左边到达临界点 - CGFloat distanceCanMoveToLeft = tipMaxX - (CGRectGetMaxX(superviewBoundsInWindow) - self.safetyMarginsOfSuperview.right); - if (tipMinX - distanceCanMoveToLeft >= CGRectGetMinX(superviewBoundsInWindow) + self.safetyMarginsOfSuperview.left) { - // 可以往左边挪 - tipMinX -= distanceCanMoveToLeft; - } else { - // 不可以往左边挪,那么让左边靠到临界点,然后再把宽度减小,以让右边处于临界点以内 - tipMinX = CGRectGetMinX(superviewBoundsInWindow) + self.safetyMarginsOfSuperview.left; - tipMaxX = CGRectGetMaxX(superviewBoundsInWindow) - self.safetyMarginsOfSuperview.right; - tipSize.width = fmin(tipSize.width, tipMaxX - tipMinX); - } - } - - // 经过上面一番调整,可能tipSize.width发生变化,一旦宽度变化,高度要重新计算,所以重新调用一次sizeThatFits - BOOL tipWidthChanged = tipSize.width != preferredTipWidth; - if (tipWidthChanged) { - tipSize = [self sizeThatFits:tipSize]; - } - - _currentLayoutDirection = self.preferLayoutDirection; - - // 检查当前的最大高度是否超过任一方向的剩余空间,如果是,则强制减小最大高度,避免后面计算布局选择方向时死循环 - 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(superviewBoundsInWindow) - self.distanceBetweenTargetRect - self.safetyMarginsOfSuperview.top; - CGFloat maximumHeightBelow = CGRectGetMaxY(superviewBoundsInWindow) - self.safetyMarginsOfSuperview.bottom - self.distanceBetweenTargetRect - CGRectGetMaxY(targetRect); - self.maximumHeight = fmax(self.minimumHeight, fmax(maximumHeightAbove, maximumHeightBelow)); - tipSize.height = self.maximumHeight; - _currentLayoutDirection = maximumHeightAbove > maximumHeightBelow ? QMUIPopupContainerViewLayoutDirectionAbove : QMUIPopupContainerViewLayoutDirectionBelow; - - NSLog(@"%@, 因为上下都不够空间,所以最大高度被强制改为%@, 位于目标的%@", self, @(self.maximumHeight), maximumHeightAbove > maximumHeightBelow ? @"上方" : @"下方"); - - } 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(superviewBoundsInWindow) + self.safetyMarginsOfSuperview.top; - tipMinY = fmax(tipMinY, tipMinYIfAlignSafetyMarginTop); - } else if (_currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow) { - CGFloat tipMinYIfAlignSafetyMarginBottom = CGRectGetMaxY(superviewBoundsInWindow) - self.safetyMarginsOfSuperview.bottom - tipSize.height; - tipMinY = fmin(tipMinY, tipMinYIfAlignSafetyMarginBottom); - } - - // 上面计算得出的 tipMinX、tipMinY 是处于 window 坐标系里的,而浮层可能是以 addSubview: 的方式显示在某个 superview 上,所以要做一次坐标系转换 - CGPoint origin = CGPointMake(tipMinX, tipMinY); - origin = [window convertPoint:origin toView:superview]; - tipMinX = origin.x; - tipMinY = origin.y; - - self.frame = CGRectFlatMake(tipMinX, tipMinY, tipSize.width, tipSize.height); - - // 调整浮层里的箭头的位置 - CGPoint targetRectCenter = CGPointMake(CGRectGetMidX(targetRect), CGRectGetMidY(targetRect)); - CGFloat selfMidX = targetRectCenter.x - (CGRectGetMinX(superviewBoundsInWindow) + CGRectGetMinX(self.frame)); - _arrowMinX = selfMidX - self.arrowSize.width / 2; - [self setNeedsLayout]; - - if (self.debug) { - self.contentView.backgroundColor = UIColorTestGreen; - self.borderColor = UIColorRed; - self.borderWidth = PixelOne; - _imageView.backgroundColor = UIColorTestRed; - _textLabel.backgroundColor = UIColorTestBlue; - } -} - -- (CGFloat)tipMinYWithTargetRect:(CGRect)itemRect tipSize:(CGSize)tipSize preferLayoutDirection:(QMUIPopupContainerViewLayoutDirection)direction { - CGFloat tipMinY = 0; - if (direction == QMUIPopupContainerViewLayoutDirectionAbove) { - tipMinY = CGRectGetMinY(itemRect) - tipSize.height - self.distanceBetweenTargetRect; - } else if (direction == QMUIPopupContainerViewLayoutDirectionBelow) { - tipMinY = CGRectGetMaxY(itemRect) + self.distanceBetweenTargetRect; - } - return tipMinY; -} - -- (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.superviewIfExist.bounds); - } - return canShow; -} - -- (void)showWithAnimated:(BOOL)animated { - [self showWithAnimated:animated completion:nil]; -} - -- (void)showWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { - - BOOL isShowingByWindowMode = NO; - if (!self.superview) { - [self initPopupContainerViewWindowIfNeeded]; - - QMUICommonViewController *viewController = (QMUICommonViewController *)self.popupWindow.rootViewController; - viewController.supportedOrientationMask = [QMUIHelper visibleViewController].supportedInterfaceOrientations; - - self.previousKeyWindow = [UIApplication sharedApplication].keyWindow; - [self.popupWindow makeKeyAndVisible]; - - isShowingByWindowMode = YES; - } else { - self.hidden = NO; - } - - if (animated) { - if (isShowingByWindowMode) { - self.popupWindow.alpha = 0; - } 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]; - } else { - if (isShowingByWindowMode) { - self.popupWindow.alpha = 1; - } else { - self.alpha = 1; - } - if (completion) { - completion(YES); - } - } -} - -- (void)hideWithAnimated:(BOOL)animated { - [self hideWithAnimated:animated completion:nil]; -} - -- (void)hideWithAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { - if (self.willHideBlock) { - self.willHideBlock(self.hidesByUserTap); - } - - BOOL isShowingByWindowMode = !!self.popupWindow; - - if (animated) { - [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{ - if (isShowingByWindowMode) { - self.popupWindow.alpha = 0; - } else { - self.alpha = 0; - } - } completion:^(BOOL finished) { - [self hideCompletionWithWindowMode:isShowingByWindowMode completion:completion]; - }]; - } else { - [self hideCompletionWithWindowMode:isShowingByWindowMode completion:completion]; - } -} - -- (void)hideCompletionWithWindowMode:(BOOL)windowMode completion:(void (^)(BOOL))completion { - if (windowMode) { - // 恢复 keyWindow 之前做一下检查,避免类似问题 https://github.com/QMUI/QMUI_iOS/issues/90 - if ([[UIApplication sharedApplication] keyWindow] == self.popupWindow) { - [self.previousKeyWindow makeKeyWindow]; - } - - // iOS 9 下(iOS 8 和 10 都没问题)需要主动移除,才能令 rootViewController 和 popupWindow 立即释放,不影响后续的 layout 判断,如果不加这两句,虽然 popupWindow 指针被置为 nil,但其实对象还存在,View 层级关系也还在 - // https://github.com/QMUI/QMUI_iOS/issues/75 - [self removeFromSuperview]; - self.popupWindow.rootViewController = nil; - - self.popupWindow.hidden = YES; - self.popupWindow = nil; - } else { - self.hidden = YES; - } - if (completion) { - completion(YES); - } - if (self.didHideBlock) { - self.didHideBlock(self.hidesByUserTap); - } - self.hidesByUserTap = NO; -} - -- (BOOL)isShowing { - BOOL isShowingIfAddedToView = self.superview && !self.hidden && !self.popupWindow; - BOOL isShowingIfInWindow = self.superview && self.popupWindow && !self.popupWindow.hidden; - return isShowingIfAddedToView || isShowingIfInWindow; -} - -#pragma mark - Private Tools - -- (BOOL)isSubviewShowing:(UIView *)subview { - return subview && !subview.hidden && subview.superview; -} - -- (void)initPopupContainerViewWindowIfNeeded { - if (!self.popupWindow) { - self.popupWindow = [[QMUIPopupContainerViewWindow alloc] init]; - self.popupWindow.backgroundColor = UIColorClear; - self.popupWindow.windowLevel = UIWindowLevelQMUIAlertView; - QMUIPopContainerViewController *viewController = [[QMUIPopContainerViewController alloc] init]; - ((QMUIPopContainerMaskControl *)viewController.view).popupContainerView = self; - if (self.automaticallyHidesWhenUserTap) { - viewController.view.backgroundColor = self.maskViewBackgroundColor; - } else { - viewController.view.backgroundColor = UIColorClear; - } - viewController.supportedOrientationMask = [QMUIHelper visibleViewController].supportedInterfaceOrientations; - self.popupWindow.rootViewController = viewController;// 利用 rootViewController 来管理横竖屏 - [self.popupWindow.rootViewController.view addSubview:self]; - } -} - -/// 根据一个给定的大小,计算出符合这个大小的内容大小 -- (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); - return contentSize; -} - -/// 根据内容大小和外部限制的大小,计算出合适的self size(包含箭头) -- (CGSize)sizeWithContentSize:(CGSize)contentSize sizeThatFits:(CGSize)sizeThatFits { - CGFloat resultWidth = contentSize.width + UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) + self.borderWidth * 2; - resultWidth = fmin(resultWidth, sizeThatFits.width);// 宽度不能超过传进来的size.width - resultWidth = fmax(fmin(resultWidth, self.maximumWidth), self.minimumWidth);// 宽度必须在最小值和最大值之间 - resultWidth = ceil(resultWidth); - - CGFloat resultHeight = contentSize.height + UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) + self.arrowSize.height + self.borderWidth * 2; - resultHeight = fmin(resultHeight, sizeThatFits.height); - resultHeight = fmax(fmin(resultHeight, self.maximumHeight), self.minimumHeight); - resultHeight = ceil(resultHeight); - - return CGSizeMake(resultWidth, resultHeight); -} - -@end - -@implementation QMUIPopupContainerView (UISubclassingHooks) - -- (void)didInitialized { - _backgroundLayer = [CAShapeLayer layer]; - _backgroundLayer.shadowOffset = CGSizeMake(0, 2); - _backgroundLayer.shadowOpacity = 1; - _backgroundLayer.shadowRadius = 10; - [self.layer addSublayer:_backgroundLayer]; - - _contentView = [[UIView alloc] init]; - self.contentView.clipsToBounds = YES; - [self addSubview:self.contentView]; - - // 由于浮层是在调用 showWithAnimated: 时才会被添加到 window 上,所以 appearance 也是在 showWithAnimated: 后才生效,这太晚了,会导致 showWithAnimated: 之前用到那些支持 appearance 的属性值都不准确,所以这里手动提前触发。 - [self updateAppearance]; -} - -- (CGSize)sizeThatFitsInContentView:(CGSize)size { - // 如果没内容则返回自身大小 - if (![self isSubviewShowing:_imageView] && ![self isSubviewShowing:_textLabel]) { - CGSize selfSize = [self contentSizeInSize:self.bounds.size]; - return selfSize; - } - - CGSize resultSize = CGSizeZero; - - 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; - } - - BOOL isTextLabelShowing = [self isSubviewShowing:_textLabel]; - if (isTextLabelShowing) { - CGSize textLabelLimitSize = CGSizeMake(size.width - resultSize.width - self.imageEdgeInsets.right, size.height); - CGSize textLabelSize = [_textLabel sizeThatFits:textLabelLimitSize]; - resultSize.width += (isImageViewShowing ? self.imageEdgeInsets.right : 0) + ceil(textLabelSize.width) + self.textEdgeInsets.left; - resultSize.height = fmax(resultSize.height, ceil(textLabelSize.height) + self.textEdgeInsets.top); - } - resultSize.width = fmin(size.width, resultSize.width); - resultSize.height = fmin(size.height, resultSize.height); - return resultSize; -} - -@end - -@implementation QMUIPopupContainerView (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearance]; - }); -} - -+ (void)setDefaultAppearance { - QMUIPopupContainerView *appearance = [QMUIPopupContainerView appearance]; - appearance.contentEdgeInsets = UIEdgeInsetsMake(8, 8, 8, 8); - appearance.arrowSize = CGSizeMake(18, 9); - appearance.maximumWidth = CGFLOAT_MAX; - appearance.minimumWidth = 0; - appearance.maximumHeight = CGFLOAT_MAX; - appearance.minimumHeight = 0; - appearance.preferLayoutDirection = QMUIPopupContainerViewLayoutDirectionAbove; - appearance.distanceBetweenTargetRect = 5; - appearance.safetyMarginsOfSuperview = UIEdgeInsetsMake(10, 10, 10, 10); - appearance.backgroundColor = UIColorWhite; - appearance.maskViewBackgroundColor = UIColorMask; - appearance.highlightedBackgroundColor = nil; - appearance.shadowColor = UIColorMakeWithRGBA(0, 0, 0, .1); - appearance.borderColor = UIColorGrayLighten; - appearance.borderWidth = PixelOne; - appearance.cornerRadius = 10; - appearance.qmui_outsideEdge = UIEdgeInsetsZero; - -} - -- (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.distanceBetweenTargetRect = appearance.distanceBetweenTargetRect; - 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 - -- (void)loadView { - QMUIPopContainerMaskControl *maskControl = [[QMUIPopContainerMaskControl alloc] init]; - self.view = maskControl; -} - -@end - -@implementation QMUIPopContainerMaskControl - -- (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - [self addTarget:self action:@selector(handleMaskEvent:) forControlEvents:UIControlEventTouchDown]; - } - return self; -} - -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { - UIView *result = [super hitTest:point withEvent:event]; - if (result == self) { - if (!self.popupContainerView.automaticallyHidesWhenUserTap) { - return nil; - } - } - return result; -} - -// 把点击遮罩的事件放在 addTarget: 里而不直接在 hitTest:withEvent: 里处理是因为 hitTest:withEvent: 总是会走两遍 -- (void)handleMaskEvent:(id)sender { - if (self.popupContainerView.automaticallyHidesWhenUserTap) { - self.popupContainerView.hidesByUserTap = YES; - [self.popupContainerView hideWithAnimated:YES]; - } -} - -@end - -@implementation QMUIPopupContainerViewWindow - -// 避免 UIWindow 拦截掉事件,保证让事件继续往背后传递 -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { - UIView *result = [super hitTest:point withEvent:event]; - if (result == self) { - return nil; - } - return result; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIPopupMenuView.h b/QMUI/QMUIKit/UIComponents/QMUIPopupMenuView.h deleted file mode 100644 index 658ce4bc..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIPopupMenuView.h +++ /dev/null @@ -1,55 +0,0 @@ -// -// QMUIPopupMenuView.h -// qmui -// -// Created by MoLice on 2017/2/24. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import "QMUIPopupContainerView.h" - -@class QMUIPopupMenuItem; -@class QMUIButton; - -/** - * 用于弹出浮层里显示一行一行的菜单的控件。 - * 使用方式: - * 1. 调用 init 方法初始化。 - * 2. 按需设置分隔线、item 高度等样式。 - * 3. 设置完样式后再通过 items 或 itemSections 添加菜单项。 - * 4. 调用 layoutWithTargetView: 或 layoutWithTargetRectInScreenCoordinate: 来布局菜单(参考父类)。 - * 5. 调用 showWithAnimated: 即可显示(参考父类)。 - */ -@interface QMUIPopupMenuView : QMUIPopupContainerView - -@property(nonatomic, assign) BOOL shouldShowItemSeparator; -@property(nonatomic, assign) BOOL shouldShowSectionSeparatorOnly; -@property(nonatomic, strong) UIColor *separatorColor UI_APPEARANCE_SELECTOR; - -@property(nonatomic, strong) UIFont *itemTitleFont UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIColor *itemHighlightedBackgroundColor UI_APPEARANCE_SELECTOR; - -@property(nonatomic, assign) UIEdgeInsets padding UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) CGFloat itemHeight UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) CGFloat imageMarginRight UI_APPEARANCE_SELECTOR; -@property(nonatomic, assign) UIEdgeInsets separatorInset UI_APPEARANCE_SELECTOR; - -@property(nonatomic, copy) NSArray *items; -@property(nonatomic, copy) NSArray *> *itemSections; - -@end - -/** - * 配合 QMUIPopupMenuView 使用,用于表示一项菜单项。 - * 支持显示图片和标题,以及点击事件的回调。 - * 可在 QMUIPopupMenuView 里统一修改菜单项的样式,如果某个菜单项需要特殊调整,可获取到对应的 QMUIPopupMenuItem.button 并进行调整。 - */ -@interface QMUIPopupMenuItem : NSObject - -@property(nonatomic, strong) UIImage *image; -@property(nonatomic, copy) NSString *title; -@property(nonatomic, strong, readonly) QMUIButton *button; -@property(nonatomic, copy) void (^handler)(); - -+ (instancetype)itemWithImage:(UIImage *)image title:(NSString *)title handler:(void (^)())handler; -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIPopupMenuView.m b/QMUI/QMUIKit/UIComponents/QMUIPopupMenuView.m deleted file mode 100644 index 18a3965b..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIPopupMenuView.m +++ /dev/null @@ -1,205 +0,0 @@ -// -// QMUIPopupMenuView.m -// qmui -// -// Created by MoLice on 2017/2/24. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import "QMUIPopupMenuView.h" -#import "QMUIButton.h" -#import "UIView+QMUI.h" -#import "CALayer+QMUI.h" -#import "UIButton+QMUI.h" -#import "QMUICore.h" - -@interface QMUIPopupMenuItem () - -@property(nonatomic, strong, readwrite) QMUIButton *button; -@end - -@interface QMUIPopupMenuView () - -@property(nonatomic, strong) UIScrollView *scrollView; -@property(nonatomic, strong) NSMutableArray *itemSeparatorLayers; -@end - -@interface QMUIPopupMenuView (UIAppearance) - -- (void)updateAppearanceForPopupMenuView; -@end - -@implementation QMUIPopupMenuView - -- (void)setItems:(NSArray *)items { - _items = items; - self.itemSections = @[_items]; -} - -- (void)setItemSections:(NSArray *> *)itemSections { - _itemSections = itemSections; - [self configureItems]; -} - -- (BOOL)shouldShowSeparatorAtRow:(NSInteger)row rowCount:(NSInteger)rowCount inSection:(NSInteger)section sectionCount:(NSInteger)sectionCount { - return (!self.shouldShowSectionSeparatorOnly && self.shouldShowItemSeparator && row < rowCount - 1) || (self.shouldShowSectionSeparatorOnly && row == rowCount - 1 && section < sectionCount - 1); -} - -- (void)configureItems { - NSInteger globalItemIndex = 0; - - // 移除所有 item - [self.scrollView qmui_removeAllSubviews]; - - 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 ++) { - QMUIPopupMenuItem *item = items[row]; - item.button.titleLabel.font = self.itemTitleFont; - item.button.highlightedBackgroundColor = self.itemHighlightedBackgroundColor; - item.button.imageEdgeInsets = UIEdgeInsetsMake(0, -self.imageMarginRight, 0, self.imageMarginRight); - item.button.contentEdgeInsets = UIEdgeInsetsMake(0, self.padding.left - item.button.imageEdgeInsets.left, 0, self.padding.right); - [self.scrollView addSubview:item.button]; - - // 配置分隔线,注意每一个 section 里的最后一行是不显示分隔线的 - BOOL shouldShowSeparatorAtRow = [self shouldShowSeparatorAtRow:row rowCount:rowCount inSection:section sectionCount:sectionCount]; - if (globalItemIndex < self.itemSeparatorLayers.count) { - CALayer *separatorLayer = self.itemSeparatorLayers[globalItemIndex]; - if (shouldShowSeparatorAtRow) { - separatorLayer.hidden = NO; - separatorLayer.backgroundColor = self.separatorColor.CGColor; - } else { - separatorLayer.hidden = YES; - } - } else if (shouldShowSeparatorAtRow) { - CALayer *separatorLayer = [CALayer layer]; - [separatorLayer qmui_removeDefaultAnimations]; - separatorLayer.backgroundColor = self.separatorColor.CGColor; - [self.scrollView.layer addSublayer:separatorLayer]; - [self.itemSeparatorLayers addObject:separatorLayer]; - } - - globalItemIndex++; - } - } -} - -#pragma mark - (UISubclassingHooks) - -- (void)didInitialized { - [super didInitialized]; - self.contentEdgeInsets = UIEdgeInsetsZero; - - self.scrollView = [[UIScrollView alloc] init]; - self.scrollView.scrollsToTop = NO; - self.scrollView.showsHorizontalScrollIndicator = NO; - self.scrollView.showsVerticalScrollIndicator = NO; - [self.contentView addSubview:self.scrollView]; - - self.itemSeparatorLayers = [[NSMutableArray alloc] init]; - - [self updateAppearanceForPopupMenuView]; -} - -- (CGSize)sizeThatFitsInContentView:(CGSize)size { - CGFloat height = UIEdgeInsetsGetVerticalValue(self.padding); - for (NSArray *section in self.itemSections) { - height += section.count * self.itemHeight; - } - size.height = fminf(height, size.height); - return size; -} - -- (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 ++) { - QMUIButton *button = items[row].button; - button.frame = CGRectMake(0, minY, contentWidth, self.itemHeight); - minY = CGRectGetMaxY(button.frame); - - BOOL shouldShowSeparatorAtRow = [self shouldShowSeparatorAtRow:row rowCount:rowCount inSection:section sectionCount:sectionCount]; - if (shouldShowSeparatorAtRow) { - self.itemSeparatorLayers[separatorIndex].frame = CGRectMake(self.separatorInset.left, minY - PixelOne + self.separatorInset.top - self.separatorInset.bottom, contentWidth - UIEdgeInsetsGetHorizontalValue(self.separatorInset), PixelOne); - separatorIndex++; - } - } - } - minY += self.padding.bottom; - self.scrollView.contentSize = CGSizeMake(contentWidth, minY); -} - -@end - -@implementation QMUIPopupMenuView (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearanceForPopupMenuView]; - }); -} - -+ (void)setDefaultAppearanceForPopupMenuView { - QMUIPopupMenuView *appearance = [QMUIPopupMenuView appearance]; - appearance.separatorColor = UIColorSeparator; - appearance.itemTitleFont = UIFontMake(16); - appearance.itemHighlightedBackgroundColor = TableViewCellSelectedBackgroundColor; - appearance.padding = UIEdgeInsetsMake([QMUIPopupContainerView appearance].cornerRadius / 2.0, 16, [QMUIPopupContainerView appearance].cornerRadius / 2.0, 16); - appearance.itemHeight = 44; - appearance.imageMarginRight = 6; - appearance.separatorInset = UIEdgeInsetsZero; -} - -- (void)updateAppearanceForPopupMenuView { - QMUIPopupMenuView *appearance = [QMUIPopupMenuView appearance]; - self.separatorColor = appearance.separatorColor; - self.itemTitleFont = appearance.itemTitleFont; - self.itemHighlightedBackgroundColor = appearance.itemHighlightedBackgroundColor; - self.padding = appearance.padding; - self.itemHeight = appearance.itemHeight; - self.imageMarginRight = appearance.imageMarginRight; - self.separatorInset = appearance.separatorInset; -} - -@end - -@implementation QMUIPopupMenuItem - -+ (instancetype)itemWithImage:(UIImage *)image title:(NSString *)title handler:(void (^)())handler { - QMUIPopupMenuItem *item = [[QMUIPopupMenuItem alloc] init]; - item.image = image; - item.title = title; - item.handler = handler; - - QMUIButton *button = [[QMUIButton alloc] initWithImage:image title:title]; - button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; - button.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; - [button addTarget:item action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - item.button = button; - return item; -} - -- (void)setTitle:(NSString *)title { - _title = title; - [self.button setTitle:title forState:UIControlStateNormal]; -} - -- (void)setImage:(UIImage *)image { - _image = image; - [self.button setImage:image forState:UIControlStateNormal]; -} - -- (void)handleButtonEvent:(id)sender { - if (self.handler) { - self.handler(); - } -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIQQEmotionManager.h b/QMUI/QMUIKit/UIComponents/QMUIQQEmotionManager.h deleted file mode 100644 index ac23531b..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIQQEmotionManager.h +++ /dev/null @@ -1,63 +0,0 @@ -// -// QMUIQQEmotionManager.h -// qmui -// -// Created by MoLice on 16/9/8. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import -#import -#import "QMUIEmotionView.h" - -/** - * 提供一个QQ表情面板,能为绑定的`UITextField`或`UITextView`提供表情的相关功能,包括点击表情输入对应的表情名字、点击删除按钮删除表情。由于表情的插入、删除都会受当前输入框的光标所在位置的影响,所以请在适当的时机更新`selectedRangeForBoundTextInput`的值,具体情况请查看属性的注释。 - * @warning 由于QQ表情图片较多(文件大小约400K),因此表情图片被以bundle的形式存放在 - * @warning 一个`QMUIQQEmotionManager`无法同时绑定`boundTextField`和`boundTextView`,在两者都绑定的情况下,优先使用`boundTextField`。 - * @warning 由于`QMUIQQEmotionManager`里面多个地方会调用`boundTextView.text`,而`setText:`并不会触发`UITextViewDelegate`的`textViewDidChange:`或`UITextViewTextDidChangeNotification`,从而在刷新表情面板里的发送按钮的enabled状态时可能不及时,所以`QMUIQQEmotionManager`要求绑定的`QMUITextView`必须打开`shouldResponseToProgrammaticallyTextChanges`属性 - */ -@interface QMUIQQEmotionManager : NSObject - -/// 要绑定的UITextField -@property(nonatomic, weak) UITextField *boundTextField; - -/// 要绑定的UITextView -@property(nonatomic, weak) UITextView *boundTextView; - -/** - * `selectedRangeForBoundTextInput`决定了表情将会被插入(删除)的位置,因此使用控件的时候需要及时更新它。 - * - * 通常用到的更新时机包括: - * - 降下键盘显示QQ表情面板之前(调用resignFirstResponder、endEditing:之前) - * - 的`textViewDidChangeSelection:`回调里 - * - 输入框里的文字发生变化时,例如点了发送按钮后输入框文字会被清空,此时要重置`selectedRangeForBoundTextInput`为0 - */ -@property(nonatomic, assign) NSRange selectedRangeForBoundTextInput; - -/** - * 显示QQ表情的表情面板,已被设置了默认的`didSelectEmotionBlock`和`didSelectDeleteButtonBlock`,在`QMUIQQEmotionManager`初始化完后,即可将`emotionView`添加到界面上。 - */ -@property(nonatomic, strong, readonly) QMUIEmotionView *emotionView; - -/** - * 将当前光标所在位置的表情删除,在调用前请注意更新`selectedRangeForBoundTextInput` - * @param forceDelete 当没有删除掉表情的情况下(可能光标前面并不是一个表情字符),要不要强制删掉光标前的字符。YES表示强制删掉,NO表示不删,交给系统键盘处理 - * @return 表示是否成功删除了文字(如果并不是删除表情,而是删除普通字符,也是返回YES) - */ -- (BOOL)deleteEmotionDisplayNameAtCurrentSelectedRangeForce:(BOOL)forceDelete; - -/** - * 在 `UITextViewDelegate` 的 `textView:shouldChangeTextInRange:replacementText:` 或者 `QMUITextFieldDelegate` 的 `textField:shouldChangeTextInRange:replacementText:` 方法里调用,根据返回值来决定是否应该调用 `deleteEmotionDisplayNameAtCurrentSelectedRangeForce:` - - @param range 要发生变化的文字所在的range - @param text 要被替换为的文字 - - @return 是否会接管键盘的删除按钮事件,`YES` 表示接管,可调用 `deleteEmotionDisplayNameAtCurrentSelectedRangeForce:` 方法,`NO` 表示不可接管,应该使用系统自身的删除事件响应。 - */ -- (BOOL)shouldTakeOverControlDeleteKeyWithChangeTextInRange:(NSRange)range replacementText:(NSString *)text; - -/** - * QQ表情的数组,会做缓存,图片只会加载一次 - */ -+ (NSArray *)emotionsForQQ; -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIQQEmotionManager.m b/QMUI/QMUIKit/UIComponents/QMUIQQEmotionManager.m deleted file mode 100644 index bb144669..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIQQEmotionManager.m +++ /dev/null @@ -1,168 +0,0 @@ -// -// QMUIQQEmotionManager.m -// qmui -// -// Created by MoLice on 16/9/8. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIQQEmotionManager.h" -#import "QMUICore.h" -#import "NSString+QMUI.h" - -NSString *const QQEmotionString = @"0-[微笑];1-[撇嘴];2-[色];3-[发呆];4-[得意];5-[流泪];6-[害羞];7-[闭嘴];8-[睡];9-[大哭];10-[尴尬];11-[发怒];12-[调皮];13-[呲牙];14-[惊讶];15-[难过];16-[酷];17-[冷汗];18-[抓狂];19-[吐];20-[偷笑];21-[可爱];22-[白眼];23-[傲慢];24-[饥饿];25-[困];26-[惊恐];27-[流汗];28-[憨笑];29-[大兵];30-[奋斗];31-[咒骂];32-[疑问];33-[嘘];34-[晕];35-[折磨];36-[衰];37-[骷髅];38-[敲打];39-[再见];40-[擦汗];41-[抠鼻];42-[鼓掌];43-[糗大了];44-[坏笑];45-[左哼哼];46-[右哼哼];47-[哈欠];48-[鄙视];49-[委屈];50-[快哭了];51-[阴险];52-[亲亲];53-[吓];54-[可怜];55-[菜刀];56-[西瓜];57-[啤酒];58-[篮球];59-[乒乓];60-[咖啡];61-[饭];62-[猪头];63-[玫瑰];64-[凋谢];65-[示爱];66-[爱心];67-[心碎];68-[蛋糕];69-[闪电];70-[炸弹];71-[刀];72-[足球];73-[瓢虫];74-[便便];75-[月亮];76-[太阳];77-[礼物];78-[拥抱];79-[强];80-[弱];81-[握手];82-[胜利];83-[抱拳];84-[勾引];85-[拳头];86-[差劲];87-[爱你];88-[NO];89-[OK];90-[爱情];91-[飞吻];92-[跳跳];93-[发抖];94-[怄火];95-[转圈];96-[磕头];97-[回头];98-[跳绳];99-[挥手];100-[激动];101-[街舞];102-[献吻];103-[左太极];104-[右太极];105-[嘿哈];106-[捂脸];107-[奸笑];108-[机智];109-[皱眉];110-[耶];111-[红包];112-[鸡]"; - -static NSArray *QQEmotionArray; - -@protocol QMUIQQEmotionInputViewProtocol - -@property(nonatomic, copy) NSString *text; -@property(nonatomic, assign, readonly) NSRange selectedRange; -@end - -@implementation QMUIQQEmotionManager - -- (instancetype)init { - self = [super init]; - if (self) { - _emotionView = [[QMUIEmotionView alloc] init]; - self.emotionView.emotions = [QMUIQQEmotionManager emotionsForQQ]; - __weak QMUIQQEmotionManager *weakSelf = self; - self.emotionView.didSelectEmotionBlock = ^(NSInteger index, QMUIEmotion *emotion) { - if (!weakSelf.boundInputView) return; - - NSString *inputText = weakSelf.boundInputView.text; - // 用一个局部变量先保存selectedRangeForBoundTextInput的值,是为了避免在接下来这段代码执行的过程中,外部可能修改了self.selectedRangeForBoundTextInput的值,导致计算错误 - NSRange selectedRange = weakSelf.selectedRangeForBoundTextInput; - if (selectedRange.location <= inputText.length) { - // 在输入框文字的中间插入表情 - NSMutableString *mutableText = [NSMutableString stringWithString:inputText ?: @""]; - [mutableText insertString:emotion.displayName atIndex:selectedRange.location]; - weakSelf.boundInputView.text = mutableText;// UITextView setText:会触发textViewDidChangeSelection:,而如果在这个delegate里更新self.selectedRangeForBoundTextInput,就会导致计算错误 - selectedRange = NSMakeRange(selectedRange.location + emotion.displayName.length, 0); - } else { - // 在输入框文字的结尾插入表情 - inputText = [inputText stringByAppendingString:emotion.displayName]; - weakSelf.boundInputView.text = inputText; - selectedRange = NSMakeRange(inputText.length, 0); - } - weakSelf.selectedRangeForBoundTextInput = selectedRange; - }; - self.emotionView.didSelectDeleteButtonBlock = ^{ - [weakSelf deleteEmotionDisplayNameAtCurrentSelectedRangeForce:YES]; - }; - } - return self; -} - -- (UIView *)boundInputView { - if (self.boundTextField) { - return (UIView *)self.boundTextField; - } else if (self.boundTextView) { - return (UIView *)self.boundTextView; - } - return nil; -} - -- (BOOL)deleteEmotionDisplayNameAtCurrentSelectedRangeForce:(BOOL)forceDelete { - if (!self.boundInputView) return NO; - - NSRange selectedRange = self.selectedRangeForBoundTextInput; - NSString *text = self.boundInputView.text; - - // 没有文字或者光标位置前面没文字 - if (!text.length || NSMaxRange(selectedRange) == 0) { - return NO; - } - - BOOL hasDeleteEmotionDisplayNameSuccess = NO; - NSInteger emotionDisplayNameMinimumLength = 3;// QQ表情里的最短displayName的长度 - NSInteger lengthForStringBeforeSelectedRange = selectedRange.location; - NSString *lastCharacterBeforeSelectedRange = [text substringWithRange:NSMakeRange(selectedRange.location - 1, 1)]; - if ([lastCharacterBeforeSelectedRange isEqualToString:@"]"] && lengthForStringBeforeSelectedRange >= emotionDisplayNameMinimumLength) { - NSInteger beginIndex = lengthForStringBeforeSelectedRange - (emotionDisplayNameMinimumLength - 1);// 从"]"之前的第n个字符开始查找 - NSInteger endIndex = MAX(0, lengthForStringBeforeSelectedRange - 5);// 直到"]"之前的第n个字符结束查找,这里写5只是简单的限定,这个数字只要比所有QQ表情的displayName长度长就行了 - for (NSInteger i = beginIndex; i >= endIndex; i --) { - NSString *checkingCharacter = [text substringWithRange:NSMakeRange(i, 1)]; - if ([checkingCharacter isEqualToString:@"]"]) { - // 查找过程中还没遇到"["就已经遇到"]"了,说明是非法的表情字符串,所以直接终止 - break; - } - - if ([checkingCharacter isEqualToString:@"["]) { - NSRange deletingDisplayNameRange = NSMakeRange(i, lengthForStringBeforeSelectedRange - i); - self.boundInputView.text = [text stringByReplacingCharactersInRange:deletingDisplayNameRange withString:@""]; - self.selectedRangeForBoundTextInput = NSMakeRange(deletingDisplayNameRange.location, 0); - hasDeleteEmotionDisplayNameSuccess = YES; - break; - } - } - } - - if (hasDeleteEmotionDisplayNameSuccess) { - return YES; - } - - if (forceDelete) { - if (NSMaxRange(selectedRange) <= text.length) { - if (selectedRange.length > 0) { - // 如果选中区域是一段文字,则删掉这段文字 - self.boundInputView.text = [text stringByReplacingCharactersInRange:selectedRange withString:@""]; - self.selectedRangeForBoundTextInput = NSMakeRange(selectedRange.location, 0); - } else if (selectedRange.location > 0) { - // 如果并没有选中一段文字,则删掉光标前一个字符 - NSString *textAfterDelete = [text qmui_stringByRemoveCharacterAtIndex:selectedRange.location - 1]; - self.boundInputView.text = textAfterDelete; - self.selectedRangeForBoundTextInput = NSMakeRange(selectedRange.location - (text.length - textAfterDelete.length), 0); - } - } else { - // 选中区域超过文字长度了,非法数据,则直接删掉最后一个字符 - self.boundInputView.text = [text qmui_stringByRemoveLastCharacter]; - self.selectedRangeForBoundTextInput = NSMakeRange(self.boundInputView.text.length, 0); - } - - return YES; - } - - return NO; -} - -- (BOOL)shouldTakeOverControlDeleteKeyWithChangeTextInRange:(NSRange)range replacementText:(NSString *)text { - BOOL isDeleteKeyPressed = text.length == 0 && self.boundInputView.text.length - 1 == range.location; - BOOL hasMarkedText = !!self.boundInputView.markedTextRange; - return isDeleteKeyPressed && !hasMarkedText; -} - -+ (UIImage *)imageForQQEmotionWithIdentifier:(NSString *)identifier { - return [QMUIHelper imageInBundle:[QMUIHelper resourcesBundleWithName:QMUIResourcesQQEmotionBundleName] withName:identifier]; -} - -+ (NSArray *)emotionsForQQ { - if (QQEmotionArray) { - return QQEmotionArray; - } - - NSMutableArray *emotions = [[NSMutableArray alloc] init]; - NSArray *emotionStringArray = [QQEmotionString componentsSeparatedByString:@";"]; - for (NSString *emotionString in emotionStringArray) { - NSArray *emotionItem = [emotionString componentsSeparatedByString:@"-"]; - NSString *identifier = [NSString stringWithFormat:@"smiley_%@", emotionItem.firstObject]; - QMUIEmotion *emotion = [QMUIEmotion emotionWithIdentifier:identifier displayName:emotionItem.lastObject]; - [emotions addObject:emotion]; - } - - QQEmotionArray = [NSArray arrayWithArray:emotions]; - [self asyncLoadImages:emotions]; - return QQEmotionArray; -} - -// 在子线程预加载 -+ (void)asyncLoadImages:(NSArray *)emotions { - dispatch_async(dispatch_get_global_queue(0, 0), ^{ - for (QMUIEmotion *e in emotions) { - [e image]; - } - }); -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUITestView.h b/QMUI/QMUIKit/UIComponents/QMUITestView.h deleted file mode 100644 index f5652199..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUITestView.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// QMUITestView.h -// qmui -// -// Created by MoLice on 16/1/28. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import - -@interface QMUITestView : UIView - -@end - - -@interface QMUITestWindow : UIWindow - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUITestView.m b/QMUI/QMUIKit/UIComponents/QMUITestView.m deleted file mode 100644 index a19bb230..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUITestView.m +++ /dev/null @@ -1,124 +0,0 @@ -// -// QMUITestView.m -// qmui -// -// Created by MoLice on 16/1/28. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUITestView.h" - -@implementation QMUITestView - -- (instancetype)init { - if (self = [super init]) { - - } - return self; -} - -- (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - - } - return self; -} - -- (void)dealloc { - NSLog(@"%@, dealloc", self); -} - -- (void)setFrame:(CGRect)frame { - CGRect oldFrame = self.frame; - BOOL isFrameChanged = CGRectEqualToRect(oldFrame, frame); - if (isFrameChanged) { - NSLog(@"frame发生变化, old is %@, new is %@", NSStringFromCGRect(oldFrame), NSStringFromCGRect(frame)); - } - [super setFrame:frame]; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - NSLog(@"%s, frame = %@", __func__, NSStringFromCGRect(self.frame)); -} - -- (void)didMoveToSuperview { - [super didMoveToSuperview]; - NSLog(@"%s, superview is %@", __func__, self.superview); -} - -- (void)didMoveToWindow { - [super didMoveToWindow]; - NSLog(@"%s, self.window is %@", __func__, self.window); -} - -- (void)addSubview:(UIView *)view { - [super addSubview:view]; - NSLog(@"%s, subview is %@, subviews.count before addSubview is %@", __func__, view, @(self.subviews.count)); -} - -- (void)setHidden:(BOOL)hidden { - [super setHidden:hidden]; - NSLog(@"%s, hidden is %@", __func__, @(hidden)); -} - -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { - UIView *view = [super hitTest:point withEvent:event]; - return view; -} - -@end - -@implementation QMUITestWindow - -- (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - - } - return self; -} - -- (void)dealloc { - NSLog(@"dealloc, %@", self); -} - -- (void)setRootViewController:(UIViewController *)rootViewController { - [super setRootViewController:rootViewController]; -} - -- (void)makeKeyAndVisible { - [super makeKeyAndVisible]; -} - -- (void)makeKeyWindow { - [super makeKeyWindow]; -} - -- (void)setHidden:(BOOL)hidden { - [super setHidden:hidden]; -} - -- (void)addSubview:(UIView *)view { - [super addSubview:view]; - NSLog(@"QMUITestWindow, subviews = %@, view = %@", self.subviews, view); -} - -- (void)setFrame:(CGRect)frame { - CGRect oldFrame = self.frame; - BOOL isFrameChanged = CGRectEqualToRect(oldFrame, frame); - if (isFrameChanged) { - NSLog(@"QMUITestWindow, frame发生变化, old is %@, new is %@", NSStringFromCGRect(oldFrame), NSStringFromCGRect(frame)); - } - [super setFrame:frame]; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - NSLog(@"QMUITestWindow, layoutSubviews"); -} - -- (void)setAlpha:(CGFloat)alpha { - [super setAlpha:alpha]; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUITips.h b/QMUI/QMUIKit/UIComponents/QMUITips.h deleted file mode 100644 index a372737e..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUITips.h +++ /dev/null @@ -1,83 +0,0 @@ -// -// QMUITips.h -// qmui -// -// Created by zhoonchen on 15/12/25. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIToastView.h" - -/** - * 简单封装了 QMUIToastView,支持弹出纯文本、loading、succeed、error、info 等五种 tips。如果这些接口还满足不了业务的需求,可以通过 QMUITips 的分类自行添加接口。 - * 注意用类方法显示 tips 的话,会导致父类的 willShowBlock 无法正常工作,具体请查看 willShowBlock 的注释。 - * @see [QMUIToastView willShowBlock] - */ - -@interface QMUITips : QMUIToastView - -NS_ASSUME_NONNULL_BEGIN - -/// 实例方法:需要自己addSubview,hide之后不会自动removeFromSuperView - -- (void)showWithText:(nullable NSString *)text; -- (void)showWithText:(nullable NSString *)text hideAfterDelay:(NSTimeInterval)delay; -- (void)showWithText:(nullable NSString *)text detailText:(nullable NSString *)detailText; -- (void)showWithText:(nullable NSString *)text detailText:(nullable NSString *)detailText hideAfterDelay:(NSTimeInterval)delay; - -- (void)showLoading; -- (void)showLoading:(nullable NSString *)text; -- (void)showLoadingHideAfterDelay:(NSTimeInterval)delay; -- (void)showLoading:(nullable NSString *)text hideAfterDelay:(NSTimeInterval)delay; -- (void)showLoading:(nullable NSString *)text detailText:(nullable NSString *)detailText; -- (void)showLoading:(nullable NSString *)text detailText:(nullable NSString *)detailText hideAfterDelay:(NSTimeInterval)delay; - -- (void)showSucceed:(nullable NSString *)text; -- (void)showSucceed:(nullable NSString *)text hideAfterDelay:(NSTimeInterval)delay; -- (void)showSucceed:(nullable NSString *)text detailText:(nullable NSString *)detailText; -- (void)showSucceed:(nullable NSString *)text detailText:(nullable NSString *)detailText hideAfterDelay:(NSTimeInterval)delay; - -- (void)showError:(nullable NSString *)text; -- (void)showError:(nullable NSString *)text hideAfterDelay:(NSTimeInterval)delay; -- (void)showError:(nullable NSString *)text detailText:(nullable NSString *)detailText; -- (void)showError:(nullable NSString *)text detailText:(nullable NSString *)detailText hideAfterDelay:(NSTimeInterval)delay; - -- (void)showInfo:(nullable NSString *)text; -- (void)showInfo:(nullable NSString *)text hideAfterDelay:(NSTimeInterval)delay; -- (void)showInfo:(nullable NSString *)text detailText:(nullable NSString *)detailText; -- (void)showInfo:(nullable NSString *)text detailText:(nullable NSString *)detailText hideAfterDelay:(NSTimeInterval)delay; - -/// 类方法:主要用在局部一次性使用的场景,hide之后会自动removeFromSuperView - -+ (QMUITips *)createTipsToView:(UIView *)view; - -+ (QMUITips *)showWithText:(nullable NSString *)text inView:(UIView *)view; -+ (QMUITips *)showWithText:(nullable NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; -+ (QMUITips *)showWithText:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view; -+ (QMUITips *)showWithText:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; - -+ (QMUITips *)showLoadingInView:(UIView *)view; -+ (QMUITips *)showLoading:(nullable NSString *)text inView:(UIView *)view; -+ (QMUITips *)showLoadingInView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; -+ (QMUITips *)showLoading:(nullable NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; -+ (QMUITips *)showLoading:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view; -+ (QMUITips *)showLoading:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; - -+ (QMUITips *)showSucceed:(nullable NSString *)text inView:(UIView *)view; -+ (QMUITips *)showSucceed:(nullable NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; -+ (QMUITips *)showSucceed:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view; -+ (QMUITips *)showSucceed:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; - -+ (QMUITips *)showError:(nullable NSString *)text inView:(UIView *)view; -+ (QMUITips *)showError:(nullable NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; -+ (QMUITips *)showError:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view; -+ (QMUITips *)showError:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; - -+ (QMUITips *)showInfo:(nullable NSString *)text inView:(UIView *)view; -+ (QMUITips *)showInfo:(nullable NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; -+ (QMUITips *)showInfo:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view; -+ (QMUITips *)showInfo:(nullable NSString *)text detailText:(nullable NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay; - -NS_ASSUME_NONNULL_END - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUITips.m b/QMUI/QMUIKit/UIComponents/QMUITips.m deleted file mode 100644 index 8e31aae2..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUITips.m +++ /dev/null @@ -1,236 +0,0 @@ -// -// QMUITips.m -// qmui -// -// Created by zhoonchen on 15/12/25. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import "QMUITips.h" -#import "QMUICore.h" -#import "QMUIToastContentView.h" -#import "QMUIToastBackgroundView.h" - -@interface QMUITips () - -@property(nonatomic, strong) UIView *contentCustomView; - -@end - -@implementation QMUITips - -- (void)showWithText:(NSString *)text { - [self showWithText:text detailText:nil hideAfterDelay:0]; -} - -- (void)showWithText:(NSString *)text hideAfterDelay:(NSTimeInterval)delay { - [self showWithText:text detailText:nil hideAfterDelay:delay]; -} - -- (void)showWithText:(NSString *)text detailText:(NSString *)detailText { - [self showWithText:text detailText:detailText hideAfterDelay:0]; -} - -- (void)showWithText:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { - self.contentCustomView = nil; - [self showTipWithText:text detailText:detailText hideAfterDelay:delay]; -} - -- (void)showLoading { - [self showLoading:nil hideAfterDelay:0]; -} - -- (void)showLoadingHideAfterDelay:(NSTimeInterval)delay { - [self showLoading:nil hideAfterDelay:delay]; -} - -- (void)showLoading:(NSString *)text { - [self showLoading:text hideAfterDelay:0]; -} - -- (void)showLoading:(NSString *)text hideAfterDelay:(NSTimeInterval)delay { - [self showLoading:text detailText:nil hideAfterDelay:delay]; -} - -- (void)showLoading:(NSString *)text detailText:(NSString *)detailText { - [self showLoading:text detailText:detailText hideAfterDelay:0]; -} -- (void)showLoading:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { - UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; - [indicator startAnimating]; - self.contentCustomView = indicator; - [self showTipWithText:text detailText:detailText hideAfterDelay:delay]; -} - -- (void)showSucceed:(NSString *)text { - [self showSucceed:text detailText:nil hideAfterDelay:0]; -} - -- (void)showSucceed:(NSString *)text hideAfterDelay:(NSTimeInterval)delay { - [self showSucceed:text detailText:nil hideAfterDelay:delay]; -} - -- (void)showSucceed:(NSString *)text detailText:(NSString *)detailText { - [self showSucceed:text detailText:detailText hideAfterDelay:0]; -} - -- (void)showSucceed:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { - self.contentCustomView = [[UIImageView alloc] initWithImage:[QMUIHelper imageWithName:@"QMUI_tips_done"]]; - [self showTipWithText:text detailText:detailText hideAfterDelay:delay]; -} - -- (void)showError:(NSString *)text { - [self showError:text detailText:nil hideAfterDelay:0]; -} - -- (void)showError:(NSString *)text hideAfterDelay:(NSTimeInterval)delay { - [self showError:text detailText:nil hideAfterDelay:delay]; -} - -- (void)showError:(NSString *)text detailText:(NSString *)detailText { - [self showError:text detailText:detailText hideAfterDelay:0]; -} - -- (void)showError:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { - self.contentCustomView = [[UIImageView alloc] initWithImage:[QMUIHelper imageWithName:@"QMUI_tips_error"]]; - [self showTipWithText:text detailText:detailText hideAfterDelay:delay]; -} - -- (void)showInfo:(NSString *)text { - [self showInfo:text detailText:nil hideAfterDelay:0]; -} - -- (void)showInfo:(NSString *)text hideAfterDelay:(NSTimeInterval)delay { - [self showInfo:text detailText:nil hideAfterDelay:delay]; -} - -- (void)showInfo:(NSString *)text detailText:(NSString *)detailText { - [self showInfo:text detailText:detailText hideAfterDelay:0]; -} - -- (void)showInfo:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { - self.contentCustomView = [[UIImageView alloc] initWithImage:[QMUIHelper imageWithName:@"QMUI_tips_info"]]; - [self showTipWithText:text detailText:detailText hideAfterDelay:delay]; -} - -- (void)showTipWithText:(NSString *)text detailText:(NSString *)detailText hideAfterDelay:(NSTimeInterval)delay { - - QMUIToastContentView *contentView = (QMUIToastContentView *)self.contentView; - contentView.customView = self.contentCustomView; - - contentView.textLabelText = text ?: @""; - contentView.detailTextLabelText = detailText ?: @""; - - [self showAnimated:YES]; - - if (delay > 0) { - [self hideAnimated:YES afterDelay:delay]; - } -} - -+ (QMUITips *)showWithText:(NSString *)text inView:(UIView *)view { - return [self showWithText:text detailText:nil inView:view hideAfterDelay:0]; -} - -+ (QMUITips *)showWithText:(NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { - return [self showWithText:text detailText:nil inView:view hideAfterDelay:delay]; -} - -+ (QMUITips *)showWithText:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view { - return [self showWithText:text detailText:detailText inView:view hideAfterDelay:0]; -} - -+ (QMUITips *)showWithText:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { - QMUITips *tips = [self createTipsToView:view]; - [tips showWithText:text detailText:detailText hideAfterDelay:delay]; - return tips; -} - -+ (QMUITips *)showLoadingInView:(UIView *)view { - return [self showLoading:nil detailText:nil inView:view hideAfterDelay:0]; -} - -+ (QMUITips *)showLoading:(NSString *)text inView:(UIView *)view { - return [self showLoading:text detailText:nil inView:view hideAfterDelay:0]; -} - -+ (QMUITips *)showLoadingInView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { - return [self showLoading:nil detailText:nil inView:view hideAfterDelay:delay]; -} - -+ (QMUITips *)showLoading:(NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { - return [self showLoading:text detailText:nil inView:view hideAfterDelay:delay]; -} - -+ (QMUITips *)showLoading:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view { - return [self showLoading:text detailText:detailText inView:view hideAfterDelay:0]; -} - -+ (QMUITips *)showLoading:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { - QMUITips *tips = [self createTipsToView:view]; - [tips showLoading:text detailText:detailText hideAfterDelay:delay]; - return tips; -} - -+ (QMUITips *)showSucceed:(NSString *)text inView:(UIView *)view { - return [self showSucceed:text detailText:nil inView:view hideAfterDelay:0]; -} - -+ (QMUITips *)showSucceed:(NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { - return [self showSucceed:text detailText:nil inView:view hideAfterDelay:delay]; -} - -+ (QMUITips *)showSucceed:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view { - return [self showSucceed:text detailText:detailText inView:view hideAfterDelay:0]; -} - -+ (QMUITips *)showSucceed:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { - QMUITips *tips = [self createTipsToView:view]; - [tips showSucceed:text detailText:detailText hideAfterDelay:delay]; - return tips; -} - -+ (QMUITips *)showError:(NSString *)text inView:(UIView *)view { - return [self showError:text detailText:nil inView:view hideAfterDelay:0]; -} - -+ (QMUITips *)showError:(NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { - return [self showError:text detailText:nil inView:view hideAfterDelay:delay]; -} - -+ (QMUITips *)showError:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view { - return [self showError:text detailText:detailText inView:view hideAfterDelay:0]; -} - -+ (QMUITips *)showError:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { - QMUITips *tips = [self createTipsToView:view]; - [tips showError:text detailText:detailText hideAfterDelay:delay]; - return tips; -} - -+ (QMUITips *)showInfo:(NSString *)text inView:(UIView *)view { - return [self showInfo:text detailText:nil inView:view hideAfterDelay:0]; -} - -+ (QMUITips *)showInfo:(NSString *)text inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { - return [self showInfo:text detailText:nil inView:view hideAfterDelay:delay]; -} - -+ (QMUITips *)showInfo:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view { - return [self showInfo:text detailText:detailText inView:view hideAfterDelay:0]; -} - -+ (QMUITips *)showInfo:(NSString *)text detailText:(NSString *)detailText inView:(UIView *)view hideAfterDelay:(NSTimeInterval)delay { - QMUITips *tips = [self createTipsToView:view]; - [tips showInfo:text detailText:detailText hideAfterDelay:delay]; - return tips; -} - -+ (QMUITips *)createTipsToView:(UIView *)view { - QMUITips *tips = [[QMUITips alloc] initWithView:view]; - [view addSubview:tips]; - tips.removeFromSuperViewWhenHide = YES; - return tips; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIToastAnimator.h b/QMUI/QMUIKit/UIComponents/QMUIToastAnimator.h deleted file mode 100644 index 48e93b6a..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIToastAnimator.h +++ /dev/null @@ -1,61 +0,0 @@ -// -// QMUIToastAnimator.h -// qmui -// -// Created by zhoonchen on 2016/12/12. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import - -@class QMUIToastView; - -/** - * `QMUIToastAnimatorDelegate`是所有`QMUIToastAnimator`或者其子类必须遵循的协议,是整个动画过程实现的地方。 - */ -@protocol QMUIToastAnimatorDelegate - -@required - -- (void)showWithCompletion:(void (^)(BOOL finished))completion; - -- (void)hideWithCompletion:(void (^)(BOOL finished))completion; - -- (BOOL)isShowing; - -- (BOOL)isAnimating; - -@end - - -// TODO: 实现多种animation类型 - -typedef NS_ENUM(NSInteger, QMUIToastAnimationType) { - QMUIToastAnimationTypeFade = 0, - QMUIToastAnimationTypeZoom, - QMUIToastAnimationTypeSlide -}; - -/** - * `QMUIToastAnimator`可以让你通过实现一些协议来自定义ToastView显示和隐藏的动画。你可以继承`QMUIToastAnimator`,然后实现`QMUIToastAnimatorDelegate`中的方法,即可实现自定义的动画。QMUIToastAnimator默认也提供了几种type的动画:1、QMUIToastAnimationTypeFade;2、QMUIToastAnimationTypeZoom;3、QMUIToastAnimationTypeSlide; - */ -@interface QMUIToastAnimator : NSObject - -/** - * 初始化方法,请务必使用这个方法来初始化。 - * - * @param toastView 要使用这个animator的QMUIToastView实例。 - */ -- (instancetype)initWithToastView:(QMUIToastView *)toastView NS_DESIGNATED_INITIALIZER; - -/** - * 获取初始化传进来的QMUIToastView。 - */ -@property(nonatomic, weak, readonly) QMUIToastView *toastView; - -/** - * 指定QMUIToastAnimator做动画的类型type。此功能暂时未实现,目前所有动画类型都是QMUIToastAnimationTypeFade。 - */ -@property(nonatomic, assign) QMUIToastAnimationType animationType; - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIToastAnimator.m b/QMUI/QMUIKit/UIComponents/QMUIToastAnimator.m deleted file mode 100644 index f43e6a1f..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIToastAnimator.m +++ /dev/null @@ -1,72 +0,0 @@ -// -// QMUIToastAnimator.m -// qmui -// -// Created by zhoonchen on 2016/12/12. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIToastAnimator.h" -#import "QMUICore.h" -#import "QMUIToastView.h" - -@implementation QMUIToastAnimator { - BOOL _isShowing; - BOOL _isAnimating; -} - -- (instancetype)init { - NSAssert(NO, @"请使用initWithToastView:初始化"); - return [self initWithToastView:nil]; -} - -- (instancetype)initWithCoder:(NSCoder *)coder { - NSAssert(NO, @"请使用initWithToastView:初始化"); - return [self initWithToastView:nil]; -} - -- (instancetype)initWithToastView:(QMUIToastView *)toastView { - NSAssert(toastView, @"toastView不能为空"); - if (self = [super init]) { - _toastView = toastView; - } - return self; -} - -- (void)showWithCompletion:(void (^)(BOOL finished))completion { - _isShowing = YES; - _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; - } completion:^(BOOL finished) { - _isAnimating = NO; - if (completion) { - completion(finished); - } - }]; -} - -- (void)hideWithCompletion:(void (^)(BOOL finished))completion { - _isShowing = NO; - _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; - } completion:^(BOOL finished) { - _isAnimating = NO; - if (completion) { - completion(finished); - } - }]; -} - -- (BOOL)isShowing { - return _isShowing; -} - -- (BOOL)isAnimating { - return _isAnimating; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIToastBackgroundView.h b/QMUI/QMUIKit/UIComponents/QMUIToastBackgroundView.h deleted file mode 100644 index ab4a1e0e..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIToastBackgroundView.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// QMUIToastBackgroundView.h -// qmui -// -// Created by zhoonchen on 2016/12/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import - -@interface QMUIToastBackgroundView : UIView - -/** - * 是否需要磨砂,默认NO。仅支持iOS8及以上版本。可以通过修改`styleColor`来控制磨砂的效果。 - */ -@property(nonatomic, assign) BOOL shouldBlurBackgroundView; - -/** - * 如果不设置磨砂,则styleColor直接作为`QMUIToastBackgroundView`的backgroundColor;如果需要磨砂,则会新增加一个`UIVisualEffectView`放在`QMUIToastBackgroundView`上面 - */ -@property(nonatomic, strong) UIColor *styleColor UI_APPEARANCE_SELECTOR; - -/** - * 设置圆角。 - */ -@property(nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR; - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIToastBackgroundView.m b/QMUI/QMUIKit/UIComponents/QMUIToastBackgroundView.m deleted file mode 100644 index f00bec8b..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIToastBackgroundView.m +++ /dev/null @@ -1,94 +0,0 @@ -// -// QMUIToastBackgroundView.m -// qmui -// -// Created by zhoonchen on 2016/12/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIToastBackgroundView.h" -#import "QMUICore.h" - -@interface QMUIToastBackgroundView () - -@property(nonatomic, strong) UIView *effectView; - -@end - -@implementation QMUIToastBackgroundView - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - self.layer.allowsGroupOpacity = NO; - self.backgroundColor = self.styleColor; - self.layer.cornerRadius = self.cornerRadius; - - } - return self; -} - -- (void)setShouldBlurBackgroundView:(BOOL)shouldBlurBackgroundView { - _shouldBlurBackgroundView = shouldBlurBackgroundView; - if (shouldBlurBackgroundView) { - if (NSClassFromString(@"UIBlurEffect")) { - UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; - UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect]; - effectView.layer.cornerRadius = self.cornerRadius; - effectView.layer.masksToBounds = YES; - [self addSubview:effectView]; - self.effectView = effectView; - } - } else { - if (self.effectView) { - [self.effectView removeFromSuperview]; - self.effectView = nil; - } - } -} - -- (void)layoutSubviews { - [super layoutSubviews]; - if (self.effectView) { - self.effectView.frame = self.bounds; - } -} - -#pragma mark - UIAppearance - -- (void)setStyleColor:(UIColor *)styleColor { - _styleColor = styleColor; - self.backgroundColor = styleColor; -} - -- (void)setCornerRadius:(CGFloat)cornerRadius { - _cornerRadius = cornerRadius; - self.layer.cornerRadius = cornerRadius; - if (self.effectView) { - self.effectView.layer.cornerRadius = cornerRadius; - } -} - -@end - - -@interface QMUIToastBackgroundView (UIAppearance) - -@end - -@implementation QMUIToastBackgroundView (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearance]; - }); -} - -+ (void)setDefaultAppearance { - QMUIToastBackgroundView *appearance = [QMUIToastBackgroundView appearance]; - appearance.styleColor = UIColorMakeWithRGBA(0, 0, 0, 0.8); - appearance.cornerRadius = 10.0; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIToastContentView.h b/QMUI/QMUIKit/UIComponents/QMUIToastContentView.h deleted file mode 100644 index 9f487a10..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIToastContentView.h +++ /dev/null @@ -1,78 +0,0 @@ -// -// QMUIToastContentView.h -// qmui -// -// Created by zhoonchen on 2016/12/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import - -/** - * `QMUIToastView`默认使用的contentView。其结构是:customView->textLabel->detailTextLabel等三个view依次往下排列。其中customView可以赋值任意的UIView或者自定义的view。 - * - * @TODO: 增加多种类型的progressView的支持。 - */ -@interface QMUIToastContentView : UIView - -/** - * 设置一个UIView,可以是:菊花、图片等等 - */ -@property(nonatomic, strong) UIView *customView; - -/** - * 设置第一行大文字label - */ -@property(nonatomic, strong, readonly) UILabel *textLabel; - -/** - * 通过textLabelText设置可以应用textLabelAttributes的样式,如果通过textLabel.text设置则可能导致一些样式失效。 - */ -@property(nonatomic, copy) NSString *textLabelText; - -/** - * 设置第二行小文字label - */ -@property(nonatomic, strong, readonly) UILabel *detailTextLabel; - -/** - * 通过detailTextLabelText设置可以应用detailTextLabelAttributes的样式,如果通过detailTextLabel.text设置则可能导致一些样式失效。 - */ -@property(nonatomic, copy) NSString *detailTextLabelText; - -/** - * 设置上下左右的padding。 - */ -@property(nonatomic, assign) UIEdgeInsets insets UI_APPEARANCE_SELECTOR; - -/** - * 设置最小size。 - */ -@property(nonatomic, assign) CGSize minimumSize UI_APPEARANCE_SELECTOR; - -/** - * 设置customView的marginBottom - */ -@property(nonatomic, assign) CGFloat customViewMarginBottom UI_APPEARANCE_SELECTOR; - -/** - * 设置textLabel的marginBottom - */ -@property(nonatomic, assign) CGFloat textLabelMarginBottom UI_APPEARANCE_SELECTOR; - -/** - * 设置detailTextLabel的marginBottom - */ -@property(nonatomic, assign) CGFloat detailTextLabelMarginBottom UI_APPEARANCE_SELECTOR; - -/** - * 设置textLabel的attributes - */ -@property(nonatomic, strong) NSDictionary *textLabelAttributes UI_APPEARANCE_SELECTOR; - -/** - * 设置detailTextLabel的attributes - */ -@property(nonatomic, strong) NSDictionary *detailTextLabelAttributes UI_APPEARANCE_SELECTOR; - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIToastContentView.m b/QMUI/QMUIKit/UIComponents/QMUIToastContentView.m deleted file mode 100644 index 2907e864..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIToastContentView.m +++ /dev/null @@ -1,262 +0,0 @@ -// -// QMUIToastContentView.m -// qmui -// -// Created by zhoonchen on 2016/12/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIToastContentView.h" -#import "QMUICore.h" -#import "UIView+QMUI.h" -#import "NSParagraphStyle+QMUI.h" - -#define DefaultTextLabelFont UIFontBoldMake(16) -#define DefaultDetailTextLabelFont UIFontBoldMake(12) -#define DefaultLabelColor UIColorWhite - -@implementation QMUIToastContentView - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - - self.layer.allowsGroupOpacity = NO; - - [self initSubviews]; - } - return self; -} - -- (void)initSubviews { - - _textLabel = [[UILabel alloc] init]; - self.textLabel.numberOfLines = 0; - self.textLabel.textAlignment = NSTextAlignmentCenter; - self.textLabel.textColor = DefaultLabelColor; - self.textLabel.font = DefaultTextLabelFont; - self.textLabel.opaque = NO; - [self addSubview:self.textLabel]; - - _detailTextLabel = [[UILabel alloc] init]; - self.detailTextLabel.numberOfLines = 0; - self.detailTextLabel.textAlignment = NSTextAlignmentCenter; - self.detailTextLabel.textColor = DefaultLabelColor; - self.detailTextLabel.font = DefaultDetailTextLabelFont; - self.detailTextLabel.opaque = NO; - [self addSubview:self.detailTextLabel]; -} - -- (void)setCustomView:(UIView *)customView { - if (self.customView) { - [self.customView removeFromSuperview]; - _customView = nil; - } - _customView = customView; - [self addSubview:self.customView]; - [self updateCustomViewTintColor]; - [self setNeedsLayout]; -} - -- (void)setTextLabelText:(NSString *)textLabelText { - _textLabelText = textLabelText; - if (textLabelText) { - self.textLabel.attributedText = [[NSAttributedString alloc] initWithString:textLabelText attributes:self.textLabelAttributes]; - self.textLabel.textAlignment = NSTextAlignmentCenter; - [self setNeedsLayout]; - } -} - -- (void)setDetailTextLabelText:(NSString *)detailTextLabelText { - _detailTextLabelText = detailTextLabelText; - if (detailTextLabelText) { - self.detailTextLabel.attributedText = [[NSAttributedString alloc] initWithString:detailTextLabelText attributes:self.detailTextLabelAttributes]; - self.detailTextLabel.textAlignment = NSTextAlignmentCenter; - } -} - -- (CGSize)sizeThatFits:(CGSize)size { - - BOOL hasCustomView = !!self.customView; - BOOL hasTextLabel = self.textLabel.text.length > 0; - BOOL hasDetailTextLabel = self.detailTextLabel.text.length > 0; - - CGFloat width = 0; - CGFloat height = 0; - - CGFloat maxContentWidth = size.width - UIEdgeInsetsGetHorizontalValue(self.insets); - CGFloat maxContentHeight = size.height - UIEdgeInsetsGetVerticalValue(self.insets); - - if (hasCustomView) { - width = fmaxf(width, CGRectGetWidth(self.customView.bounds)); - height += (CGRectGetHeight(self.customView.bounds) + ((hasTextLabel || hasDetailTextLabel) ? self.customViewMarginBottom : 0)); - } - - if (hasTextLabel) { - CGSize textLabelSize = [self.textLabel sizeThatFits:CGSizeMake(maxContentWidth, maxContentHeight)]; - width = fmaxf(width, textLabelSize.width); - height += (textLabelSize.height + (hasDetailTextLabel ? self.textLabelMarginBottom : 0)); - } - - if (hasDetailTextLabel) { - CGSize detailTextLabelSize = [self.detailTextLabel sizeThatFits:CGSizeMake(maxContentWidth, maxContentHeight)]; - width = fmaxf(width, detailTextLabelSize.width); - height += (detailTextLabelSize.height + self.detailTextLabelMarginBottom); - } - - width += UIEdgeInsetsGetHorizontalValue(self.insets); - height += UIEdgeInsetsGetVerticalValue(self.insets); - - if (!CGSizeEqualToSize(self.minimumSize, CGSizeZero)) { - width = fmaxf(width, self.minimumSize.width); - height = fmaxf(height, self.minimumSize.height); - } - - return CGSizeMake(fminf(size.width, width), fminf(size.height, height)); -} - -- (void)layoutSubviews { - [super layoutSubviews]; - - BOOL hasCustomView = !!self.customView; - BOOL hasTextLabel = self.textLabel.text.length > 0; - BOOL hasDetailTextLabel = self.detailTextLabel.text.length > 0; - - CGFloat contentWidth = CGRectGetWidth(self.bounds); - CGFloat maxContentWidth = contentWidth - UIEdgeInsetsGetHorizontalValue(self.insets); - - CGFloat minY = self.insets.top; - - if (hasCustomView) { - if (!hasTextLabel && !hasDetailTextLabel) { - // 处理有minimumSize的情况 - minY = CGFloatGetCenter(CGRectGetHeight(self.bounds), CGRectGetHeight(self.customView.bounds)); - } - self.customView.frame = CGRectFlatMake(CGFloatGetCenter(contentWidth, CGRectGetWidth(self.customView.bounds)), minY, CGRectGetWidth(self.customView.bounds), CGRectGetHeight(self.customView.bounds)); - minY = CGRectGetMaxY(self.customView.frame) + self.customViewMarginBottom; - } - - if (hasTextLabel) { - CGSize textLabelSize = [self.textLabel sizeThatFits:CGSizeMake(maxContentWidth, CGFLOAT_MAX)]; - if (!hasCustomView && !hasDetailTextLabel) { - // 处理有minimumSize的情况 - minY = CGFloatGetCenter(CGRectGetHeight(self.bounds), textLabelSize.height); - } - self.textLabel.frame = CGRectFlatMake(CGFloatGetCenter(contentWidth, maxContentWidth), minY, maxContentWidth, textLabelSize.height); - minY = CGRectGetMaxY(self.textLabel.frame) + self.textLabelMarginBottom; - } - - if (hasDetailTextLabel) { - // 暂时没考虑剩余高度不够用的情况 - CGSize detailTextLabelSize = [self.detailTextLabel sizeThatFits:CGSizeMake(maxContentWidth, CGFLOAT_MAX)]; - if (!hasCustomView && !hasTextLabel) { - // 处理有minimumSize的情况 - minY = CGFloatGetCenter(CGRectGetHeight(self.bounds), detailTextLabelSize.height); - } - self.detailTextLabel.frame = CGRectFlatMake(CGFloatGetCenter(contentWidth, maxContentWidth), minY, maxContentWidth, detailTextLabelSize.height); - } -} - -- (void)tintColorDidChange { - - if (self.customView) { - [self updateCustomViewTintColor]; - } - - NSMutableDictionary *textLabelAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.textLabelAttributes]; - textLabelAttributes[NSForegroundColorAttributeName] = self.tintColor; - self.textLabelAttributes = textLabelAttributes; - self.textLabelText = self.textLabelText; - - NSMutableDictionary *detailTextLabelAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.detailTextLabelAttributes]; - detailTextLabelAttributes[NSForegroundColorAttributeName] = self.tintColor; - self.detailTextLabelAttributes = detailTextLabelAttributes; - self.detailTextLabelText = self.detailTextLabelText; -} - -- (void)updateCustomViewTintColor { - if (!self.customView) { - return; - } - self.customView.tintColor = self.tintColor; - if ([self.customView isKindOfClass:[UIImageView class]]) { - UIImageView *customView = (UIImageView *)self.customView; - customView.image = [customView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - } - if ([self.customView isKindOfClass:[UIActivityIndicatorView class]]) { - UIActivityIndicatorView *customView = (UIActivityIndicatorView *)self.customView; - customView.color = self.tintColor; - } -} - -#pragma mark - UIAppearance - -- (void)setInsets:(UIEdgeInsets)insets { - _insets = insets; - [self setNeedsLayout]; -} - -- (void)setMinimumSize:(CGSize)minimumSize { - _minimumSize = minimumSize; - [self setNeedsLayout]; -} - -- (void)setCustomViewMarginBottom:(CGFloat)customViewMarginBottom { - _customViewMarginBottom = customViewMarginBottom; - [self setNeedsLayout]; -} - -- (void)setTextLabelMarginBottom:(CGFloat)textLabelMarginBottom { - _textLabelMarginBottom = textLabelMarginBottom; - [self setNeedsLayout]; -} - -- (void)setDetailTextLabelMarginBottom:(CGFloat)detailTextLabelMarginBottom { - _detailTextLabelMarginBottom = detailTextLabelMarginBottom; - [self setNeedsLayout]; -} - -- (void)setTextLabelAttributes:(NSDictionary *)textLabelAttributes { - _textLabelAttributes = textLabelAttributes; - if (self.textLabelText && self.textLabelText.length > 0) { - // 刷新label的attributes - self.textLabelText = self.textLabelText; - } -} - -- (void)setDetailTextLabelAttributes:(NSDictionary *)detailTextLabelAttributes { - _detailTextLabelAttributes = detailTextLabelAttributes; - if (self.detailTextLabelText && self.detailTextLabelText.length > 0) { - // 刷新label的attributes - self.detailTextLabelText = self.detailTextLabelText; - } -} - -@end - - -@interface QMUIToastContentView (UIAppearance) - -@end - -@implementation QMUIToastContentView (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearance]; - }); -} - -+ (void)setDefaultAppearance { - QMUIToastContentView *appearance = [QMUIToastContentView appearance]; - appearance.insets = UIEdgeInsetsMake(16, 16, 16, 16); - appearance.minimumSize = CGSizeZero; - appearance.customViewMarginBottom = 8; - appearance.textLabelMarginBottom = 4; - appearance.detailTextLabelMarginBottom = 0; - appearance.textLabelAttributes = @{NSFontAttributeName: DefaultTextLabelFont, NSForegroundColorAttributeName: DefaultLabelColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight: 22]}; - appearance.detailTextLabelAttributes = @{NSFontAttributeName: DefaultDetailTextLabelFont, NSForegroundColorAttributeName: DefaultLabelColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight: 18]}; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIToastView.h b/QMUI/QMUIKit/UIComponents/QMUIToastView.h deleted file mode 100644 index 7e028044..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIToastView.h +++ /dev/null @@ -1,160 +0,0 @@ -// -// QMUIToastView.h -// qmui -// -// Created by zhoonchen on 2016/12/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import - -@class QMUIToastAnimator; - -typedef NS_ENUM(NSInteger, QMUIToastViewPosition) { - QMUIToastViewPositionTop, - QMUIToastViewPositionCenter, - QMUIToastViewPositionBottom -}; - -/** - * `QMUIToastView`是一个用来显示toast的控件,其主要结构包括:`backgroundView`、`contentView`,这两个view都是通过外部赋值获取,默认使用`QMUIToastBackgroundView`和`QMUIToastContentView`。 - * - * 拓展性:`QMUIToastBackgroundView`和`QMUIToastContentView`是QMUI提供的默认的view,这两个view都可以通过appearance来修改样式,如果这两个view满足不了需求,那么也可以通过新建自定义的view来代替这两个view。另外,QMUI也提供了默认的toastAnimator来实现ToastView的显示和隐藏动画,如果需要重新定义一套动画,可以继承`QMUIToastAnimator`并且实现`QMUIToastViewAnimatorDelegate`中的协议就可以自定义自己的一套动画。 - * - * 建议使用`QMUIToastView`的时候,再封装一层,具体可以参考`QMUITips`这个类。 - * - * @see QMUIToastBackgroundView - * @see QMUIToastContentView - * @see QMUIToastAnimator - * @see QMUITips - */ -@interface QMUIToastView : UIView - -/** - * 生成一个ToastView的唯一初始化方法,`view`的bound将会作为ToastView默认frame。 - * - * @param view ToastView的superView。 - */ -- (instancetype)initWithView:(UIView *)view NS_DESIGNATED_INITIALIZER; - -/** - * parentView是ToastView初始化的时候传进去的那个view。 - */ -@property(nonatomic, weak, readonly) UIView *parentView; - -/** - * 显示ToastView。 - * - * @param animated 是否需要通过动画显示。 - * - * @see toastAnimator - */ -- (void)showAnimated:(BOOL)animated; - -/** - * 隐藏ToastView。 - * - * @param animated 是否需要通过动画隐藏。 - * - * @see toastAnimator - */ -- (void)hideAnimated:(BOOL)animated; - -/** - * 在`delay`时间后隐藏ToastView。 - * - * @param animated 是否需要通过动画隐藏。 - * @param delay 多少秒后隐藏。 - * - * @see toastAnimator - */ -- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay; - -/// @warning 如果使用 [QMUITips showXxx] 系列快捷方法来显示 tips,willShowBlock 将会在 show 之后才被设置,最终并不会被调用。这种场景建议自己在调用 [QMUITips showXxx] 之前执行一段代码,或者不要使用 [QMUITips showXxx] 的方式显示 tips -@property(nonatomic, copy) void (^willShowBlock)(UIView *showInView, BOOL animated); -@property(nonatomic, copy) void (^didShowBlock)(UIView *showInView, BOOL animated); -@property(nonatomic, copy) void (^willHideBlock)(UIView *hideInView, BOOL animated); -@property(nonatomic, copy) void (^didHideBlock)(UIView *hideInView, BOOL animated); - -/** - * `QMUIToastAnimator`可以让你通过实现一些协议来自定义ToastView显示和隐藏的动画。你可以继承`QMUIToastAnimator`,然后实现`QMUIToastAnimatorDelegate`中的方法,即可实现自定义的动画。如果不赋值,则会使用`QMUIToastAnimator`中的默认动画。 - */ -@property(nonatomic, strong) QMUIToastAnimator *toastAnimator; - -/** - * 决定QMUIToastView的位置,目前有上中下三个位置,默认值是center。 - - * 如果设置了top或者bottom,那么ToastView的布局规则是:顶部从marginInsets.top开始往下布局(QMUIToastViewPositionTop) 和 底部从marginInsets.bottom开始往上布局(QMUIToastViewPositionBottom)。 - */ -@property(nonatomic, assign) QMUIToastViewPosition toastPosition; - -/** - * 是否在ToastView隐藏的时候顺便把它从superView移除,默认为NO。 - */ -@property(nonatomic, assign) BOOL removeFromSuperViewWhenHide; - - -/////////////////// - - -/** - * 会盖住整个superView,防止手指可以点击到ToastView下面的内容,默认透明。 - */ -@property(nonatomic, strong, readonly) UIView *maskView; - -/**s - * 承载Toast内容的UIView,可以自定义并赋值给contentView。如果contentView需要跟随ToastView的tintColor变化而变化,可以重写自定义view的`tintColorDidChange`来实现。默认使用`QMUIToastContentView`实现。 - */ -@property(nonatomic, strong) __kindof UIView *contentView; - -/** - * `contentView`下面的黑色背景UIView,默认使用`QMUIToastBackgroundView`实现,可以通过`QMUIToastBackgroundView`的 cornerRadius 和 styleColor 来修改圆角和背景色。 - */ -@property(nonatomic, strong) __kindof UIView *backgroundView; - - -/////////////////// - - -/** - * 上下左右的偏移值。 - */ -@property(nonatomic, assign) CGPoint offset UI_APPEARANCE_SELECTOR; - -/** - * ToastView距离上下左右的最小间距。 - */ -@property(nonatomic, assign) UIEdgeInsets marginInsets UI_APPEARANCE_SELECTOR; - -@end - - -@interface QMUIToastView (ToastTool) - -/** - * 工具方法。隐藏`view`里面的所有ToastView。 - * - * @param view 即将隐藏的ToastView的superView。 - * @param animated 是否需要通过动画隐藏。 - * - * @return 如果成功隐藏一个ToastView则返回YES,失败则NO。 - */ -+ (BOOL)hideAllToastInView:(UIView *)view animated:(BOOL)animated; - -/** - * 工具方法。返回`view`里面最顶级的ToastView,如果没有则返回nil。 - * - * @param view ToastView的superView。 - * @return 返回一个QMUIToastView的实例。 - */ -+ (instancetype)toastInView:(UIView *)view; - -/** - * 工具方法。返回`view`里面所有的ToastView,如果没有则返回nil。 - * - * @param view ToastView的superView。 - * @return 包含所有QMUIToastView的数组。 - */ -+ (NSArray *)allToastInView:(UIView *)view; - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIToastView.m b/QMUI/QMUIKit/UIComponents/QMUIToastView.m deleted file mode 100644 index 2c389a3f..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIToastView.m +++ /dev/null @@ -1,320 +0,0 @@ -// -// QMUIToastView.m -// qmui -// -// Created by zhoonchen on 2016/12/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QMUIToastView.h" -#import "QMUICore.h" -#import "QMUIToastAnimator.h" -#import "QMUIToastContentView.h" -#import "QMUIToastBackgroundView.h" - -@interface QMUIToastView () - -@property(nonatomic, weak) NSTimer *hideDelayTimer; - -@end - -@implementation QMUIToastView - -#pragma mark - 初始化 - -- (instancetype)initWithFrame:(CGRect)frame { - NSAssert(NO, @"请使用initWithView:初始化"); - return [self initWithView:nil]; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - NSAssert(NO, @"请使用initWithView:初始化"); - return [self initWithView:nil]; -} - -- (instancetype)initWithView:(UIView *)view { - NSAssert(view, @"view不能为空"); - if (self = [super initWithFrame:view.bounds]) { - _parentView = view; - [self commonInit]; - } - return self; -} - -- (void)dealloc { - [self removeNotifications]; -} - -- (void)commonInit { - - self.toastPosition = QMUIToastViewPositionCenter; - - // 顺序不能乱,先添加backgroundView再添加contentView - self.backgroundView = [self defaultBackgrondView]; - self.contentView = [self defaultContentView]; - - self.opaque = NO; - self.alpha = 0.0; - self.backgroundColor = UIColorClear; - self.layer.allowsGroupOpacity = NO; - - self.tintColor = UIColorWhite; - - _maskView = [[UIView alloc] init]; - self.maskView.backgroundColor = UIColorClear; - [self addSubview:self.maskView]; - - [self registerNotifications]; -} - -- (QMUIToastAnimator *)defaultAnimator { - QMUIToastAnimator *toastAnimator = [[QMUIToastAnimator alloc] initWithToastView:self]; - return toastAnimator; -} - -- (UIView *)defaultBackgrondView { - QMUIToastBackgroundView *backgroundView = [[QMUIToastBackgroundView alloc] init]; - return backgroundView; -} - -- (UIView *)defaultContentView { - QMUIToastContentView *contentView = [[QMUIToastContentView alloc] init]; - return contentView; -} - -- (void)removeFromSuperview { - [super removeFromSuperview]; - _parentView = nil; -} - -- (void)setBackgroundView:(UIView *)backgroundView { - if (self.backgroundView) { - [self.backgroundView removeFromSuperview]; - _backgroundView = nil; - } - _backgroundView = backgroundView; - self.backgroundView.alpha = 0.0; - [self addSubview:self.backgroundView]; - [self setNeedsLayout]; -} - -- (void)setContentView:(UIView *)contentView { - if (self.contentView) { - [self.contentView removeFromSuperview]; - _contentView = nil; - } - _contentView = contentView; - self.contentView.alpha = 0.0; - [self addSubview:self.contentView]; - [self setNeedsLayout]; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - - self.frame = self.parentView.bounds; - self.maskView.frame = self.bounds; - - CGFloat contentWidth = CGRectGetWidth(self.parentView.bounds); - CGFloat contentHeight = CGRectGetHeight(self.parentView.bounds); - - CGFloat limitWidth = contentWidth - UIEdgeInsetsGetHorizontalValue(self.marginInsets); - CGFloat limitHeight = contentHeight - UIEdgeInsetsGetVerticalValue(self.marginInsets); - - if ([QMUIHelper isKeyboardVisible]) { - // 处理键盘相关逻辑 - contentHeight -= [QMUIHelper lastKeyboardHeightInApplicationWindowWhenVisible]; - } - - if (self.contentView) { - - CGSize contentViewSize = [self.contentView sizeThatFits:CGSizeMake(limitWidth, limitHeight)]; - CGFloat contentViewX = fmaxf(self.marginInsets.left, (contentWidth - contentViewSize.width) / 2) + self.offset.x; - CGFloat contentViewY = fmaxf(self.marginInsets.top, (contentHeight - contentViewSize.height) / 2) + self.offset.y; - - if (self.toastPosition == QMUIToastViewPositionTop) { - contentViewY = self.marginInsets.top + self.offset.y; - } else if (self.toastPosition == QMUIToastViewPositionBottom) { - contentViewY = contentHeight - contentViewSize.height - self.marginInsets.bottom + self.offset.y; - } - - CGRect contentRect = CGRectFlatMake(contentViewX, contentViewY, contentViewSize.width, contentViewSize.height); - self.contentView.frame = CGRectApplyAffineTransform(contentRect, self.contentView.transform); - } - if (self.backgroundView) { - // backgroundView的frame跟contentView一样,contentView里面的subviews如果需要在视觉上跟backgroundView有个padding,那么就自己在自定义的contentView里面做。 - self.backgroundView.frame = self.contentView.frame; - } -} - -#pragma mark - 横竖屏 - -- (void)registerNotifications { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(statusBarOrientationDidChange:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; -} - -- (void)removeNotifications { - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidChangeStatusBarOrientationNotification object:nil]; -} - -- (void)statusBarOrientationDidChange:(NSNotification *)notification { - if (!self.parentView) { - return; - } - [self setNeedsLayout]; - [self layoutIfNeeded]; -} - -#pragma mark - Show and Hide - -- (void)showAnimated:(BOOL)animated { - - // show之前需要layout以下,防止同一个tip切换不同的状态导致layout没更新 - [self setNeedsLayout]; - - [self.hideDelayTimer invalidate]; - self.alpha = 1.0; - - if (self.willShowBlock) { - self.willShowBlock(self.parentView, animated); - } - - if (animated) { - if (!self.toastAnimator) { - self.toastAnimator = [self defaultAnimator]; - } - if (self.toastAnimator) { - __weak __typeof(self)weakSelf = self; - [self.toastAnimator showWithCompletion:^(BOOL finished) { - if (weakSelf.didShowBlock) { - weakSelf.didShowBlock(weakSelf.parentView, animated); - } - }]; - } - } else { - self.backgroundView.alpha = 1.0; - self.contentView.alpha = 1.0; - if (self.didShowBlock) { - self.didShowBlock(self.parentView, animated); - } - } -} - -- (void)hideAnimated:(BOOL)animated { - - if (self.willHideBlock) { - self.willHideBlock(self.parentView, animated); - } - - if (animated) { - if (!self.toastAnimator) { - self.toastAnimator = [self defaultAnimator]; - } - if (self.toastAnimator) { - __weak __typeof(self)weakSelf = self; - [self.toastAnimator hideWithCompletion:^(BOOL finished) { - [weakSelf didHideWithAnimated:animated]; - }]; - } - } else { - self.backgroundView.alpha = 0.0; - self.contentView.alpha = 0.0; - [self didHideWithAnimated:animated]; - } -} - -- (void)didHideWithAnimated:(BOOL)animated { - - if (self.didHideBlock) { - self.didHideBlock(self.parentView, animated); - } - - [self.hideDelayTimer invalidate]; - self.alpha = 0.0; - if (self.removeFromSuperViewWhenHide) { - [self removeFromSuperview]; - } -} - -- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay { - NSTimer *timer = [NSTimer timerWithTimeInterval:delay target:self selector:@selector(handleHideTimer:) userInfo:@(animated) repeats:NO]; - [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; - self.hideDelayTimer = timer; -} - -- (void)handleHideTimer:(NSTimer *)timer { - [self hideAnimated:[timer.userInfo boolValue]]; -} - -#pragma mark - UIAppearance - -- (void)setOffset:(CGPoint)offset { - _offset = offset; - [self setNeedsLayout]; -} - -- (void)setMarginInsets:(UIEdgeInsets)marginInsets { - _marginInsets = marginInsets; - [self setNeedsLayout]; -} - -@end - - -@interface QMUIToastView (UIAppearance) - -@end - -@implementation QMUIToastView (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearance]; - }); -} - -+ (void)setDefaultAppearance { - QMUIToastView *appearance = [QMUIToastView appearance]; - appearance.offset = CGPointZero; - appearance.marginInsets = UIEdgeInsetsMake(20, 20, 20, 20); -} - -@end - -@implementation QMUIToastView (ToastTool) - -+ (BOOL)hideAllToastInView:(UIView *)view animated:(BOOL)animated { - NSArray *toastViews = [self allToastInView:view]; - BOOL returnFlag = NO; - for (QMUIToastView *toastView in toastViews) { - if (toastView) { - toastView.removeFromSuperViewWhenHide = YES; - [toastView hideAnimated:animated]; - returnFlag = YES; - } - } - return returnFlag; -} - -+ (instancetype)toastInView:(UIView *)view { - NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator]; - for (UIView *subview in subviewsEnum) { - if ([subview isKindOfClass:self]) { - return (QMUIToastView *)subview; - } - } - return nil; -} - -+ (NSArray *)allToastInView:(UIView *)view { - NSMutableArray *toastViews = [[NSMutableArray alloc] init]; - for (UIView *subview in view.subviews) { - if ([subview isKindOfClass:self]) { - [toastViews addObject:subview]; - } - } - return toastViews; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIVisualEffectView.h b/QMUI/QMUIKit/UIComponents/QMUIVisualEffectView.h deleted file mode 100644 index bbb88eed..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIVisualEffectView.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// QMUIVisualEffectView.h -// qmui -// -// Created by ZhoonChen on 14/12/1. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import - -typedef NS_ENUM(NSInteger, QMUIVisualEffectViewStyle) { - QMUIVisualEffectViewStyleExtraLight, - QMUIVisualEffectViewStyleLight, - QMUIVisualEffectViewStyleDark -}; - -@interface QMUIVisualEffectView : UIView - -@property(nonatomic,assign,readonly) QMUIVisualEffectViewStyle style; - -- (instancetype)initWithStyle:(QMUIVisualEffectViewStyle)style; -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIVisualEffectView.m b/QMUI/QMUIKit/UIComponents/QMUIVisualEffectView.m deleted file mode 100644 index 6686f736..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIVisualEffectView.m +++ /dev/null @@ -1,74 +0,0 @@ -// -// QMUIVisualEffectView.m -// qmui -// -// Created by ZhoonChen on 14/12/1. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUIVisualEffectView.h" - -@implementation QMUIVisualEffectView -{ - UIVisualEffectView *_effectView_8; // iOS8 及以上 - UIToolbar *_effectView_7; // iOS7 - UIView *_effectView_6; // iOS6 及以下 -} - -- (instancetype)init { - self = [self initWithStyle:QMUIVisualEffectViewStyleLight]; - if (self) { - } - return self; -} - -- (instancetype)initWithStyle:(QMUIVisualEffectViewStyle)style { - self = [super init]; - if (self) { - _style = style; - [self initEffectViewUI]; - } - return self; -} - -- (void)initEffectViewUI { - if ([UIVisualEffectView class]) { - UIBlurEffectStyle effStyle; - switch (_style) { - case QMUIVisualEffectViewStyleExtraLight: - effStyle = UIBlurEffectStyleExtraLight; - break; - case QMUIVisualEffectViewStyleLight: - effStyle = UIBlurEffectStyleLight; - break; - case QMUIVisualEffectViewStyleDark: - effStyle = UIBlurEffectStyleDark; - default: - break; - } - _effectView_8 = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:effStyle]]; - _effectView_8.clipsToBounds = YES; - [self addSubview:_effectView_8]; - } else { - _effectView_7 = [[UIToolbar alloc] init]; - _effectView_7.clipsToBounds = YES; - [self addSubview:_effectView_7]; - } -} - -- (void)setBackgroundColor:(UIColor *)backgroundColor { - _effectView_6.backgroundColor = backgroundColor; - _effectView_7.backgroundColor = backgroundColor; - _effectView_8.backgroundColor = backgroundColor; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - if ([UIVisualEffectView class]) { - _effectView_8.frame = CGRectMake(0, 0, CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds)); - } else { - _effectView_7.frame = CGRectMake(0, 0, CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds)); - } -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIZoomImageView.h b/QMUI/QMUIKit/UIComponents/QMUIZoomImageView.h deleted file mode 100644 index a00cf7eb..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIZoomImageView.h +++ /dev/null @@ -1,142 +0,0 @@ -// -// QMUIZoomImageView.h -// qmui -// -// Created by ZhoonChen on 14-9-14. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import -#import - -@class QMUIZoomImageView; -@class QMUIEmptyView; -@class QMUIButton; -@class QMUISlider; -@class QMUIZoomImageViewVideoToolbar; - -@protocol QMUIZoomImageViewDelegate -@optional -- (void)singleTouchInZoomingImageView:(QMUIZoomImageView *)zoomImageView location:(CGPoint)location; -- (void)doubleTouchInZoomingImageView:(QMUIZoomImageView *)zoomImageView location:(CGPoint)location; -- (void)longPressInZoomingImageView:(QMUIZoomImageView *)zoomImageView; -/** - * 告知 delegate 在视频预览界面里,由于用户点击了空白区域或播放视频等导致了底部的视频工具栏被显示或隐藏 - * @param didHide 如果为 YES 则表示工具栏被隐藏,NO 表示工具栏被显示了出来 - */ -- (void)zoomImageView:(QMUIZoomImageView *)imageView didHideVideoToolbar:(BOOL)didHide; - -/// 是否支持缩放,默认为 YES -- (BOOL)enabledZoomViewInZoomImageView:(QMUIZoomImageView *)zoomImageView; - -// 可通过此方法调整视频播放时底部 toolbar 的视觉位置,默认为 {25, 25, 25, 18} -// 如果同时设置了 QMUIZoomImageViewVideoToolbar 实例的 contentInsets 属性,则这里设置的值将不再生效 -- (UIEdgeInsets)contentInsetsForVideoToolbar:(QMUIZoomImageViewVideoToolbar *)toolbar inZoomingImageView:(QMUIZoomImageView *)zoomImageView; - -@end - -/** - * 支持缩放查看静态图片、live photo、视频的控件 - * 默认显示完整图片或视频,可双击查看原始大小,再次双击查看放大后的大小,第三次双击恢复到初始大小。 - * - * 支持通过修改 contentMode 来控制静态图片和 live photo 默认的显示模式,目前仅支持 UIViewContentModeCenter、UIViewContentModeScaleAspectFill、UIViewContentModeScaleAspectFit,默认为 UIViewContentModeCenter。注意这里的显示模式是基于 viewportRect 而言的而非整个 zoomImageView - * @see viewportRect - * - * QMUIZoomImageView 提供最基础的图片预览和缩放功能以及 loading、错误等状态的展示支持,其他功能请通过继承来实现。 - */ -@interface QMUIZoomImageView : UIView - -@property(nonatomic, weak) id delegate; - -/** - * 比如常见的上传头像预览界面中间有一个用于裁剪的方框,则 viewportRect 必须被设置为这个方框在 zoomImageView 坐标系内的 frame,否则拖拽图片或视频时无法正确限制它们的显示范围 - * @note 图片或视频的初始位置会位于 viewportRect 正中间 - * @note 如果想要图片覆盖整个 viewportRect,将 contentMode 设置为 UIViewContentModeScaleAspectFill 即可 - * 如果设置为 CGRectZero 则表示使用默认值,默认值为和整个 zoomImageView 一样大 - */ -@property(nonatomic, assign) CGRect viewportRect; - -@property(nonatomic, assign) CGFloat maximumZoomScale; - -/// 设置当前要显示的图片,会把 livePhoto/video 相关内容清空,因此注意不要直接通过 imageView.image 来设置图片。 -@property(nonatomic, weak) UIImage *image; - -/// 用于显示图片的 UIImageView,注意不要通过 imageView.image 来设置图片,请使用 image 属性。 -@property(nonatomic, strong, readonly) UIImageView *imageView; - -/// 设置当前要显示的 Live Photo,会把 image/video 相关内容清空,因此注意不要直接通过 livePhotoView.livePhoto 来设置 -@property(nonatomic, weak) PHLivePhoto *livePhoto NS_AVAILABLE_IOS(9_1); - -/// 用于显示 Live Photo 的 view,仅在 iOS 9.1 及以后才有效 -@property(nonatomic, strong, readonly) PHLivePhotoView *livePhotoView NS_AVAILABLE_IOS(9_1); - -/// 设置当前要显示的 video ,会把 image/livePhoto 相关内容清空,因此注意不要直接通过 videoPlayerLayer 来设置 -@property(nonatomic, weak) AVPlayerItem *videoPlayerItem; - -/// 用于显示 video 的 layer -@property(nonatomic, weak, readonly) AVPlayerLayer *videoPlayerLayer; - -// 播放 video 时底部的工具栏,你可通过此属性来拿到并修改上面的播放/暂停按钮、进度条、Label 等的样式 -// @see QMUIZoomImageViewVideoToolbar -@property(nonatomic, strong, readonly) QMUIZoomImageViewVideoToolbar *videoToolbar; - -// 播放 video 时屏幕中央的播放按钮 -@property(nonatomic, strong, readonly) QMUIButton *videoCenteredPlayButton; - -// 可通过此属性修改 video 播放时屏幕中央的播放按钮图片 -@property(nonatomic, strong) UIImage *videoCenteredPlayButtonImage UI_APPEARANCE_SELECTOR; - -/// 暂停视频播放 -- (void)pauseVideo; -/// 停止视频播放,将播放状态重置到初始状态 -- (void)endPlayingVideo; - -/** - * 获取当前正在显示的图片/视频在整个 QMUIZoomImageView 坐标系里的 rect(会按照当前的缩放状态来计算) - */ -- (CGRect)imageViewRectInZoomImageView; - -/** - * 重置图片或视频的大小,使用的场景例如:相册控件里放大当前图片、划到下一张、再回来,当前的图片或视频应该恢复到原来大小。 - * 注意子类重写需要调一下super。 - */ -- (void)revertZooming; - -@property(nonatomic, strong, readonly) QMUIEmptyView *emptyView; - -/** - * 显示一个 loading - * @info 注意 cell 复用可能导致当前页面显示一张错误的旧图片/视频,所以一般情况下需要视情况同时将 image/livePhoto/videoPlayerItem 等属性置为 nil 以清除图片/视频的显示 - */ -- (void)showLoading; - -/** - * 显示一句提示语 - * @info 注意 cell 复用可能导致当前页面显示一张错误的旧图片/视频,所以一般情况下需要视情况同时将 image/livePhoto/videoPlayerItem 等属性置为 nil 以清除图片/视频的显示 - */ -- (void)showEmptyViewWithText:(NSString *)text; - -/** - * 将 emptyView 隐藏 - */ -- (void)hideEmptyView; - -@end - -@interface QMUIZoomImageViewVideoToolbar : UIView - -@property(nonatomic, strong, readonly) QMUIButton *playButton; -@property(nonatomic, strong, readonly) QMUIButton *pauseButton; -@property(nonatomic, strong, readonly) QMUISlider *slider; -@property(nonatomic, strong, readonly) UILabel *sliderLeftLabel; -@property(nonatomic, strong, readonly) UILabel *sliderRightLabel; - -// 可通过调整此属性来调整 toolbar 的视觉位置,默认为 {25, 25, 25, 18} -// 如果同时实现了 QMUIZoomImageViewDelegate 的 contentInsetsForVideoToolbar:inZoomingImageView: 方法,则此处设置的值会覆盖掉 delegate 中返回的值 -@property(nonatomic, assign) UIEdgeInsets contentInsets UI_APPEARANCE_SELECTOR; - -// 可通过这些属性修改 video 播放时屏幕底部工具栏的播放/暂停图标 -@property(nonatomic, strong) UIImage *playButtonImage UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong) UIImage *pauseButtonImage UI_APPEARANCE_SELECTOR; - -@end diff --git a/QMUI/QMUIKit/UIComponents/QMUIZoomImageView.m b/QMUI/QMUIKit/UIComponents/QMUIZoomImageView.m deleted file mode 100644 index 496a5dda..00000000 --- a/QMUI/QMUIKit/UIComponents/QMUIZoomImageView.m +++ /dev/null @@ -1,1023 +0,0 @@ -// -// QMUIZoomImageView.m -// qmui -// -// Created by ZhoonChen on 14-9-14. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUIZoomImageView.h" -#import "QMUICore.h" -#import "QMUIEmptyView.h" -#import "UIImage+QMUI.h" -#import "UIColor+QMUI.h" -#import "UIScrollView+QMUI.h" -#import "QMUIButton.h" -#import "QMUISlider.h" -#import -#import "UIControl+QMUI.h" -#import "UILabel+QMUI.h" - -#define kIconsColor UIColorMakeWithRGBA(255, 255, 255, .75) - -// generate icon images needed by QMUIZoomImageView -// 用于生成 QMUIZoomImageView 所需的一些简单的图标图片 -@interface QMUIZoomImageViewImageGenerator : NSObject - -+ (UIImage *)largePlayImage; -+ (UIImage *)smallPlayImage; -+ (UIImage *)pauseImage; - -@end - -@interface QMUIZoomImageVideoPlayerView : UIView - -@end - -static NSUInteger const kTagForCenteredPlayButton = 1; - -@interface QMUIZoomImageView () - -@property(nonatomic, strong) UIScrollView *scrollView; - -// video play -@property(nonatomic, strong) QMUIZoomImageVideoPlayerView *videoPlayerView; -@property(nonatomic, strong) AVPlayer *videoPlayer; -@property(nonatomic, strong) id videoTimeObserver; -@property(nonatomic, assign) BOOL isSeekingVideo; -@property(nonatomic, assign) CGSize videoSize; - -@end - -@implementation QMUIZoomImageView - -@synthesize imageView = _imageView; -@synthesize livePhotoView = _livePhotoView; -@synthesize videoPlayerLayer = _videoPlayerLayer; -@synthesize videoToolbar = _videoToolbar; -@synthesize videoCenteredPlayButton = _videoCenteredPlayButton; - -- (void)didMoveToWindow { - // 当 self.window 为 nil 时说明此 view 被移出了可视区域(比如所在的 controller 被 pop 了),此时应该停止视频播放 - if (!self.window) { - [self endPlayingVideo]; - } -} - -- (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - QMUIZoomImageView *appearance = [QMUIZoomImageView appearance]; - _videoCenteredPlayButtonImage = appearance.videoCenteredPlayButtonImage; - - self.contentMode = UIViewContentModeCenter; - self.maximumZoomScale = 2.0; - - self.scrollView = [[UIScrollView alloc] init]; - self.scrollView.showsHorizontalScrollIndicator = NO; - self.scrollView.showsVerticalScrollIndicator = NO; - self.scrollView.minimumZoomScale = 0; - self.scrollView.maximumZoomScale = self.maximumZoomScale; - self.scrollView.delegate = self; - [self addSubview:self.scrollView]; - - _emptyView = [[QMUIEmptyView alloc] init]; - ((UIActivityIndicatorView *)self.emptyView.loadingView).color = UIColorWhite; - self.emptyView.hidden = YES; - [self addSubview:self.emptyView]; - - UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTapGestureWithPoint:)]; - singleTapGesture.delegate = self; - singleTapGesture.numberOfTapsRequired = 1; - singleTapGesture.numberOfTouchesRequired = 1; - [self addGestureRecognizer:singleTapGesture]; - - UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTapGestureWithPoint:)]; - doubleTapGesture.numberOfTapsRequired = 2; - doubleTapGesture.numberOfTouchesRequired = 1; - [self addGestureRecognizer:doubleTapGesture]; - - UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; - [self addGestureRecognizer:longPressGesture]; - - // 双击失败后才出发单击 - [singleTapGesture requireGestureRecognizerToFail:doubleTapGesture]; - } - return self; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - - if (CGRectIsEmpty(self.bounds)) { - return; - } - - self.scrollView.frame = self.bounds; - self.emptyView.frame = self.bounds; - - CGRect viewportRect = [self finalViewportRect]; - - if (_videoCenteredPlayButton) { - _videoCenteredPlayButton.center = CGPointMake(CGRectGetMidX(viewportRect), CGRectGetMidY(viewportRect)); - } - - if (_videoToolbar) { - _videoToolbar.frame = ({ - CGFloat height = [_videoToolbar sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)].height; - CGRectFlatMake(0, CGRectGetHeight(self.bounds) - height, CGRectGetWidth(self.bounds), height); - }); - } -} - -- (void)setFrame:(CGRect)frame { - BOOL isBoundsChanged = !CGSizeEqualToSize(frame.size, self.frame.size); - [super setFrame:frame]; - if (isBoundsChanged) { - [self revertZooming]; - } -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -#pragma mark - Normal Image - -- (UIImageView *)imageView { - [self initImageViewIfNeeded]; - return _imageView; -} - -- (void)initImageViewIfNeeded { - if (_imageView) { - return; - } - _imageView = [[UIImageView alloc] init]; - [self.scrollView addSubview:_imageView]; -} - -- (void)setImage:(UIImage *)image { - _image = image; - - // 释放以节省资源 - [_livePhotoView removeFromSuperview]; - _livePhotoView = nil; - [self destroyVideoRelatedObjectsIfNeeded]; - - if (!image) { - _imageView.image = nil; - return; - } - [self initImageViewIfNeeded]; - self.imageView.image = image; - - // 更新 imageView 的大小时,imageView 可能已经被缩放过,所以要应用当前的缩放 - self.imageView.frame = CGRectApplyAffineTransform(CGRectMakeWithSize(image.size), self.imageView.transform); - - [self hideViews]; - self.imageView.hidden = NO; - - [self revertZooming]; -} - -#pragma mark - Live Photo - -- (PHLivePhotoView *)livePhotoView { - [self initLivePhotoViewIfNeeded]; - return _livePhotoView; -} - -- (void)setLivePhoto:(PHLivePhoto *)livePhoto { - _livePhoto = livePhoto; - - [_imageView removeFromSuperview]; - _imageView = nil; - [self destroyVideoRelatedObjectsIfNeeded]; - - if (!livePhoto) { - _livePhotoView.livePhoto = nil; - return; - } - - [self initLivePhotoViewIfNeeded]; - _livePhotoView.livePhoto = livePhoto; - _livePhotoView.hidden = NO; - - // 更新 livePhotoView 的大小时,livePhotoView 可能已经被缩放过,所以要应用当前的缩放 - _livePhotoView.frame = CGRectApplyAffineTransform(CGRectMakeWithSize(livePhoto.size), _livePhotoView.transform); - - [self revertZooming]; -} - -- (void)initLivePhotoViewIfNeeded { - if (_livePhotoView || !NSClassFromString(@"PHLivePhotoView")) { - return; - } - _livePhotoView = [[PHLivePhotoView alloc] init]; - [self.scrollView addSubview:_livePhotoView]; -} - -#pragma mark - Image Scale - -- (void)setContentMode:(UIViewContentMode)contentMode { - BOOL isContentModeChanged = self.contentMode != contentMode; - [super setContentMode:contentMode]; - if (isContentModeChanged) { - [self revertZooming]; - } -} - -- (void)setMaximumZoomScale:(CGFloat)maximumZoomScale { - _maximumZoomScale = maximumZoomScale; - self.scrollView.maximumZoomScale = maximumZoomScale; -} - -- (CGFloat)minimumZoomScale { - if (!self.image && !self.livePhoto && !self.videoPlayerItem) { - return 1; - } - - CGRect viewport = [self finalViewportRect]; - CGSize mediaSize = CGSizeZero; - if (self.image) { - mediaSize = self.image.size; - } else if (self.livePhoto) { - mediaSize = self.livePhoto.size; - } else if (self.videoPlayerItem) { - mediaSize = self.videoSize; - } - - CGFloat minScale = 1; - CGFloat scaleX = CGRectGetWidth(viewport) / mediaSize.width; - CGFloat scaleY = CGRectGetHeight(viewport) / mediaSize.height; - if (self.contentMode == UIViewContentModeScaleAspectFit) { - minScale = fminf(scaleX, scaleY); - } else if (self.contentMode == UIViewContentModeScaleAspectFill) { - minScale = fmaxf(scaleX, scaleY); - } else if (self.contentMode == UIViewContentModeCenter) { - if (scaleX >= 1 && scaleY >= 1) { - minScale = 1; - } else { - minScale = fminf(scaleX, scaleY); - } - } - return minScale; -} - -- (void)revertZooming { - if (CGRectIsEmpty(self.bounds)) { - return; - } - - BOOL enabledZoomImageView = [self enabledZoomImageView]; - CGFloat minimumZoomScale = [self minimumZoomScale]; - CGFloat maximumZoomScale = enabledZoomImageView ? self.maximumZoomScale : minimumZoomScale; - maximumZoomScale = fmaxf(minimumZoomScale, maximumZoomScale);// 可能外部通过 contentMode = UIViewContentModeScaleAspectFit 的方式来让小图片撑满当前的 zoomImageView,所以算出来 minimumZoomScale 会很大(至少比 maximumZoomScale 大),所以这里要做一个保护 - CGFloat zoomScale = minimumZoomScale; - BOOL shouldFireDidZoomingManual = zoomScale == self.scrollView.zoomScale; - self.scrollView.panGestureRecognizer.enabled = enabledZoomImageView; - self.scrollView.pinchGestureRecognizer.enabled = enabledZoomImageView; - self.scrollView.minimumZoomScale = minimumZoomScale; - self.scrollView.maximumZoomScale = maximumZoomScale; - [self setZoomScale:zoomScale animated:NO]; - - // 只有前后的 zoomScale 不相等,才会触发 UIScrollViewDelegate scrollViewDidZoom:,因此对于相等的情况要自己手动触发 - if (shouldFireDidZoomingManual) { - [self handleDidEndZooming]; - } - - // 当内容比 viewport 的区域更大时,要把内容放在 viewport 正中间 - self.scrollView.contentOffset = ({ - CGFloat x = self.scrollView.contentOffset.x; - CGFloat y = self.scrollView.contentOffset.y; - CGRect viewport = [self finalViewportRect]; - if (!CGRectIsEmpty(viewport)) { - UIView *contentView = [self currentContentView]; - if (CGRectGetWidth(viewport) < CGRectGetWidth(contentView.frame)) { - x = (CGRectGetWidth(contentView.frame) / 2 - CGRectGetWidth(viewport) / 2) - CGRectGetMinX(viewport); - } - if (CGRectGetHeight(viewport) < CGRectGetHeight(contentView.frame)) { - y = (CGRectGetHeight(contentView.frame) / 2 - CGRectGetHeight(viewport) / 2) - CGRectGetMinY(viewport); - } - } - CGPointMake(x, y); - }); -} - -- (void)setZoomScale:(CGFloat)zoomScale animated:(BOOL)animated { - if (animated) { - [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - self.scrollView.zoomScale = zoomScale; - } completion:nil]; - } else { - self.scrollView.zoomScale = zoomScale; - } -} - -- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated { - if (animated) { - [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - [self.scrollView zoomToRect:rect animated:NO]; - } completion:nil]; - } else { - [self.scrollView zoomToRect:rect animated:NO]; - } -} - -- (CGRect)imageViewRectInZoomImageView { - UIView *imageView = [self currentContentView]; - return [self convertRect:imageView.frame fromView:imageView.superview]; -} - -- (void)handleDidEndZooming { - CGRect viewport = [self finalViewportRect]; - - UIView *contentView = [self currentContentView]; - // 强制 layout 以确保下面的一堆计算依赖的都是最新的 frame 的值 - [self layoutIfNeeded]; - CGRect contentViewFrame = contentView ? [self convertRect:contentView.frame fromView:contentView.superview] : CGRectZero; - UIEdgeInsets contentInset = UIEdgeInsetsZero; - - contentInset.top = CGRectGetMinY(viewport); - contentInset.left = CGRectGetMinX(viewport); - contentInset.right = CGRectGetWidth(self.bounds) - CGRectGetMaxX(viewport); - contentInset.bottom = CGRectGetHeight(self.bounds) - CGRectGetMaxY(viewport); - - // 图片 height 比选图框(viewport)的 height 小,这时应该把图片纵向摆放在选图框中间,且不允许上下移动 - if (CGRectGetHeight(viewport) > CGRectGetHeight(contentViewFrame)) { - // 用 floor 而不是 flat,是因为 flat 本质上是向上取整,会导致 top + bottom 比实际的大,然后 scrollView 就认为可滚动了 - contentInset.top = floor(CGRectGetMidY(viewport) - CGRectGetHeight(contentViewFrame) / 2.0); - contentInset.bottom = floor(CGRectGetHeight(self.bounds) - CGRectGetMidY(viewport) - CGRectGetHeight(contentViewFrame) / 2.0); - } - - // 图片 width 比选图框的 width 小,这时应该把图片横向摆放在选图框中间,且不允许左右移动 - if (CGRectGetWidth(viewport) > CGRectGetWidth(contentViewFrame)) { - contentInset.left = floor(CGRectGetMidX(viewport) - CGRectGetWidth(contentViewFrame) / 2.0); - contentInset.right = floor(CGRectGetWidth(self.bounds) - CGRectGetMidX(viewport) - CGRectGetWidth(contentViewFrame) / 2.0); - } - - self.scrollView.contentInset = contentInset; - self.scrollView.contentSize = contentView.frame.size; -} - -- (BOOL)enabledZoomImageView { - BOOL enabledZoom = YES; - if ([self.delegate respondsToSelector:@selector(enabledZoomViewInZoomImageView:)]) { - enabledZoom = [self.delegate enabledZoomViewInZoomImageView:self]; - } else if (!self.image && !self.livePhoto && !self.videoPlayerItem) { - enabledZoom = NO; - } - return enabledZoom; -} - -#pragma mark - Video - -- (void)setVideoPlayerItem:(AVPlayerItem *)videoPlayerItem { - _videoPlayerItem = videoPlayerItem; - - [_livePhotoView removeFromSuperview]; - _livePhotoView = nil; - [_imageView removeFromSuperview]; - _imageView = nil; - - if (!videoPlayerItem) { - [self hideViews]; - return; - } - - // 获取视频尺寸 - NSArray *tracksArray = videoPlayerItem.asset.tracks; - self.videoSize = CGSizeZero; - for (AVAssetTrack *track in tracksArray) { - if ([track.mediaType isEqualToString:AVMediaTypeVideo]) { - CGSize size = CGSizeApplyAffineTransform(track.naturalSize, track.preferredTransform); - self.videoSize = CGSizeMake(fabs(size.width), fabs(size.height)); - break; - } - } - - self.videoPlayer = [AVPlayer playerWithPlayerItem:videoPlayerItem]; - [self initVideoRelatedViewsIfNeeded]; - _videoPlayerLayer.player = self.videoPlayer; - // 更新 videoPlayerView 的大小时,videoView 可能已经被缩放过,所以要应用当前的缩放 - self.videoPlayerView.frame = CGRectApplyAffineTransform(CGRectMakeWithSize(self.videoSize), self.videoPlayerView.transform); - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleVideoPlayToEndEvent) name:AVPlayerItemDidPlayToEndTimeNotification object:videoPlayerItem]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; - - [self configVideoProgressSlider]; - - [self hideViews]; - self.videoPlayerLayer.hidden = NO; - self.videoCenteredPlayButton.hidden = NO; - self.videoToolbar.playButton.hidden = NO; - - [self revertZooming]; -} - -- (void)handlePlayButton:(UIButton *)button { - [self addPlayerTimeObserver]; - [self.videoPlayer play]; - self.videoCenteredPlayButton.hidden = YES; - self.videoToolbar.playButton.hidden = YES; - self.videoToolbar.pauseButton.hidden = NO; - if (button.tag == kTagForCenteredPlayButton) { - self.videoToolbar.hidden = YES; - if ([self.delegate respondsToSelector:@selector(zoomImageView:didHideVideoToolbar:)]) { - [self.delegate zoomImageView:self didHideVideoToolbar:YES]; - } - } -} -- (void)handlePauseButton { - [self.videoPlayer pause]; - self.videoToolbar.playButton.hidden = NO; - self.videoToolbar.pauseButton.hidden = YES; -} - -- (void)handleVideoPlayToEndEvent { - [self.videoPlayer seekToTime:CMTimeMake(0, 1)]; - self.videoCenteredPlayButton.hidden = NO; - self.videoToolbar.playButton.hidden = NO; - self.videoToolbar.pauseButton.hidden = YES; -} - -- (void)handleStartDragVideoSlider:(UISlider *)slider { - [self.videoPlayer pause]; - [self removePlayerTimeObserver]; -} - -- (void)handleDraggingVideoSlider:(UISlider *)slider { - if (!self.isSeekingVideo) { - self.isSeekingVideo = YES; - [self updateVideoSliderLeftLabel]; - - CGFloat currentValue = slider.value; - [self.videoPlayer seekToTime:CMTimeMakeWithSeconds(currentValue, NSEC_PER_SEC) completionHandler:^(BOOL finished) { - dispatch_async(dispatch_get_main_queue(), ^{ - self.isSeekingVideo = NO; - }); - }]; - } -} - -- (void)handleFinishDragVideoSlider:(UISlider *)slider { - [self.videoPlayer play]; - self.videoCenteredPlayButton.hidden = YES; - self.videoToolbar.playButton.hidden = YES; - self.videoToolbar.pauseButton.hidden = NO; - - [self addPlayerTimeObserver]; -} - -- (void)syncVideoProgressSlider { - double currentSeconds = CMTimeGetSeconds(self.videoPlayer.currentTime); - [self.videoToolbar.slider setValue:currentSeconds]; - [self updateVideoSliderLeftLabel]; -} - -- (void)configVideoProgressSlider { - self.videoToolbar.sliderLeftLabel.text = [self timeStringFromSeconds:0]; - double duration = CMTimeGetSeconds(self.videoPlayerItem.asset.duration); - self.videoToolbar.sliderRightLabel.text = [self timeStringFromSeconds:duration]; - - self.videoToolbar.slider.minimumValue = 0.0; - self.videoToolbar.slider.maximumValue = duration; - self.videoToolbar.slider.value = 0; - [self.videoToolbar.slider addTarget:self action:@selector(handleStartDragVideoSlider:) forControlEvents:UIControlEventTouchDown]; - [self.videoToolbar.slider addTarget:self action:@selector(handleDraggingVideoSlider:) forControlEvents:UIControlEventValueChanged]; - [self.videoToolbar.slider addTarget:self action:@selector(handleFinishDragVideoSlider:) forControlEvents:UIControlEventTouchUpInside]; - - [self addPlayerTimeObserver]; -} - -- (void)addPlayerTimeObserver { - if (self.videoTimeObserver) { - return; - } - double interval = .1f; - __weak QMUIZoomImageView *weakSelf = self; - self.videoTimeObserver = [self.videoPlayer addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(interval, NSEC_PER_SEC) queue:NULL usingBlock:^(CMTime time) { - [weakSelf syncVideoProgressSlider]; - }]; -} - -- (void)removePlayerTimeObserver { - if (!self.videoTimeObserver) { - return; - } - [self.videoPlayer removeTimeObserver:self.videoTimeObserver]; - self.videoTimeObserver = nil; -} - -- (void)updateVideoSliderLeftLabel { - double currentSeconds = CMTimeGetSeconds(self.videoPlayer.currentTime); - self.videoToolbar.sliderLeftLabel.text = [self timeStringFromSeconds:currentSeconds]; -} - -// convert "100" to "01:40" -- (NSString *)timeStringFromSeconds:(NSUInteger)seconds { - NSUInteger min = floor(seconds / 60); - NSUInteger sec = floor(seconds - min * 60); - return [NSString stringWithFormat:@"%02ld:%02ld", (long)min, (long)sec]; -} - -- (void)pauseVideo { - if (!self.videoPlayer) { - return; - } - [self handlePauseButton]; - [self removePlayerTimeObserver]; -} - -- (void)endPlayingVideo { - if (!self.videoPlayer) { - return; - } - [self.videoPlayer seekToTime:CMTimeMake(0, 1)]; - [self pauseVideo]; - [self syncVideoProgressSlider]; - self.videoToolbar.hidden = YES; - self.videoCenteredPlayButton.hidden = NO; - -} - -- (AVPlayerLayer *)videoPlayerLayer { - [self initVideoPlayerLayerIfNeeded]; - return _videoPlayerLayer; -} - -- (QMUIZoomImageViewVideoToolbar *)videoToolbar { - [self initVideoToolbarIfNeeded]; - return _videoToolbar; -} - -- (QMUIButton *)videoCenteredPlayButton { - [self initVideoCenteredPlayButtonIfNeeded]; - return _videoCenteredPlayButton; -} - -- (void)initVideoPlayerLayerIfNeeded { - if (self.videoPlayerView) { - return; - } - self.videoPlayerView = [[QMUIZoomImageVideoPlayerView alloc] init]; - _videoPlayerLayer = (AVPlayerLayer *)self.videoPlayerView.layer; - self.videoPlayerView.hidden = YES; - [self.scrollView addSubview:self.videoPlayerView]; -} - -- (void)initVideoToolbarIfNeeded { - if (_videoToolbar) { - return; - } - _videoToolbar = ({ - QMUIZoomImageViewVideoToolbar * b = [[QMUIZoomImageViewVideoToolbar alloc] init]; - if ([self.delegate respondsToSelector:@selector(contentInsetsForVideoToolbar:inZoomingImageView:)]) { - b.contentInsets = [self.delegate contentInsetsForVideoToolbar:b inZoomingImageView:self]; - } - [b.playButton addTarget:self action:@selector(handlePlayButton:) forControlEvents:UIControlEventTouchUpInside]; - [b.pauseButton addTarget:self action:@selector(handlePauseButton) forControlEvents:UIControlEventTouchUpInside]; - [self insertSubview:b belowSubview:self.emptyView]; - b.hidden = YES; - b; - }); -} - -- (void)initVideoCenteredPlayButtonIfNeeded { - if (_videoCenteredPlayButton) { - return; - } - - _videoCenteredPlayButton = ({ - QMUIButton *b = [[QMUIButton alloc] init]; - b.qmui_outsideEdge = UIEdgeInsetsMake(-60, -60, -60, -60); - b.tag = kTagForCenteredPlayButton; - [b setImage:self.videoCenteredPlayButtonImage forState:UIControlStateNormal]; - [b sizeToFit]; - [b addTarget:self action:@selector(handlePlayButton:) forControlEvents:UIControlEventTouchUpInside]; - b.hidden = YES; - [self insertSubview:b belowSubview:self.emptyView]; - b; - }); -} - -- (void)initVideoRelatedViewsIfNeeded { - [self initVideoPlayerLayerIfNeeded]; - [self initVideoToolbarIfNeeded]; - [self initVideoCenteredPlayButtonIfNeeded]; - [self setNeedsLayout]; -} - -- (void)destroyVideoRelatedObjectsIfNeeded { - [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; - [self removePlayerTimeObserver]; - - [self.videoPlayerView removeFromSuperview]; - self.videoPlayerView = nil; - - [self.videoToolbar removeFromSuperview]; - _videoToolbar = nil; - - [self.videoCenteredPlayButton removeFromSuperview]; - _videoCenteredPlayButton = nil; - - self.videoPlayer = nil; -} - -- (void)setVideoCenteredPlayButtonImage:(UIImage *)videoCenteredPlayButtonImage { - _videoCenteredPlayButtonImage = videoCenteredPlayButtonImage; - if (!self.videoCenteredPlayButton) { - return; - } - [self.videoCenteredPlayButton setImage:videoCenteredPlayButtonImage forState:UIControlStateNormal]; - [self setNeedsLayout]; -} - -- (void)applicationDidEnterBackground { - [self pauseVideo]; -} - -#pragma mark - GestureRecognizers - -- (void)handleSingleTapGestureWithPoint:(UITapGestureRecognizer *)gestureRecognizer { - CGPoint gesturePoint = [gestureRecognizer locationInView:gestureRecognizer.view]; - if ([self.delegate respondsToSelector:@selector(singleTouchInZoomingImageView:location:)]) { - [self.delegate singleTouchInZoomingImageView:self location:gesturePoint]; - } - if (self.videoPlayerItem) { - self.videoToolbar.hidden = !self.videoToolbar.hidden; - if ([self.delegate respondsToSelector:@selector(zoomImageView:didHideVideoToolbar:)]) { - [self.delegate zoomImageView:self didHideVideoToolbar:self.videoToolbar.hidden]; - } - } -} - -- (void)handleDoubleTapGestureWithPoint:(UITapGestureRecognizer *)gestureRecognizer { - CGPoint gesturePoint = [gestureRecognizer locationInView:gestureRecognizer.view]; - if ([self.delegate respondsToSelector:@selector(doubleTouchInZoomingImageView:location:)]) { - [self.delegate doubleTouchInZoomingImageView:self location:gesturePoint]; - } - - if ([self enabledZoomImageView]) { - // 如果图片被压缩了,则第一次放大到原图大小,第二次放大到最大倍数 - if (self.scrollView.zoomScale >= self.scrollView.maximumZoomScale) { - [self setZoomScale:self.scrollView.minimumZoomScale animated:YES]; - } else { - CGFloat newZoomScale = 0; - if (self.scrollView.zoomScale < 1) { - // 如果目前显示的大小比原图小,则放大到原图 - newZoomScale = 1; - } else { - // 如果当前显示原图,则放大到最大的大小 - newZoomScale = self.scrollView.maximumZoomScale; - } - - CGRect zoomRect = CGRectZero; - CGPoint tapPoint = [[self currentContentView] convertPoint:gesturePoint fromView:gestureRecognizer.view]; - zoomRect.size.width = CGRectGetWidth(self.bounds) / newZoomScale; - zoomRect.size.height = CGRectGetHeight(self.bounds) / newZoomScale; - zoomRect.origin.x = tapPoint.x - CGRectGetWidth(zoomRect) / 2; - zoomRect.origin.y = tapPoint.y - CGRectGetHeight(zoomRect) / 2; - [self zoomToRect:zoomRect animated:YES]; - } - } -} - -- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)longPressGestureRecognizer { - if ([self enabledZoomImageView] && longPressGestureRecognizer.state == UIGestureRecognizerStateBegan) { - if ([self.delegate respondsToSelector:@selector(longPressInZoomingImageView:)]) { - [self.delegate longPressInZoomingImageView:self]; - } - } -} - -- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { - if ([touch.view isKindOfClass:[UISlider class]]) { - return NO; - } - return YES; -} - -#pragma mark - EmptyView - -- (void)showLoading { - // 挪到最前面 - [self insertSubview:self.emptyView atIndex:(self.subviews.count - 1)]; - [self.emptyView setLoadingViewHidden:NO]; - [self.emptyView setTextLabelText:nil]; - [self.emptyView setDetailTextLabelText:nil]; - [self.emptyView setActionButtonTitle:nil]; - self.emptyView.hidden = NO; -} - -- (void)showEmptyViewWithText:(NSString *)text { - [self insertSubview:self.emptyView atIndex:(self.subviews.count - 1)]; - [self.emptyView setLoadingViewHidden:YES]; - [self.emptyView setTextLabelText:text]; - [self.emptyView setDetailTextLabelText:nil]; - [self.emptyView setActionButtonTitle:nil]; - self.emptyView.hidden = NO; -} - -- (void)hideEmptyView { - self.emptyView.hidden = YES; -} - -#pragma mark - - -- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { - return [self currentContentView]; -} - -- (void)scrollViewDidZoom:(UIScrollView *)scrollView { - [self handleDidEndZooming]; -} - -#pragma mark - 工具方法 - -- (CGRect)finalViewportRect { - CGRect rect = self.viewportRect; - if (CGRectIsEmpty(rect) && !CGRectIsEmpty(self.bounds)) { - // 有可能此时还没有走到过 layoutSubviews 因此拿不到正确的 scrollView 的 size,因此这里要强制 layout 一下 - if (!CGSizeEqualToSize(self.scrollView.bounds.size, self.bounds.size)) { - [self setNeedsLayout]; - [self layoutIfNeeded]; - } - rect = CGRectMakeWithSize(self.scrollView.bounds.size); - } - return rect; -} - -- (void)hideViews { - _livePhotoView.hidden = YES; - _imageView.hidden = YES; - _videoCenteredPlayButton.hidden = YES; - _videoPlayerLayer.hidden = YES; - _videoToolbar.hidden = YES; - _videoToolbar.pauseButton.hidden = YES; - _videoToolbar.playButton.hidden = YES; - _videoCenteredPlayButton.hidden = YES; -} - - -- (UIView *)currentContentView { - if (_imageView) { - return _imageView; - } - if (_livePhotoView) { - return _livePhotoView; - } - if (self.videoPlayerView) { - return self.videoPlayerView; - } - return nil; -} - -@end - -@interface QMUIZoomImageView (UIAppearance) - -@end - -@implementation QMUIZoomImageView (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearance]; - }); -} - -+ (void)setDefaultAppearance { - QMUIZoomImageView *appearance = [QMUIZoomImageView appearance]; - appearance.videoCenteredPlayButtonImage = [QMUIZoomImageViewImageGenerator largePlayImage]; -} - -@end - -@implementation QMUIZoomImageVideoPlayerView - -+ (Class)layerClass { - return [AVPlayerLayer class]; -} - -@end - -@implementation QMUIZoomImageViewImageGenerator - -+ (UIImage *)largePlayImage { - CGFloat width = 60; - - UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, width), NO, 0); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - - UIColor *color = kIconsColor; - CGContextSetStrokeColorWithColor(context, color.CGColor); - - // circle outside - CGContextSetFillColorWithColor(context, UIColorMakeWithRGBA(0, 0, 0, .25).CGColor); - CGFloat circleLineWidth = 1; - // consider line width to avoid edge clip - UIBezierPath *circle = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(circleLineWidth / 2, circleLineWidth / 2, width - circleLineWidth, width - circleLineWidth)]; - [circle setLineWidth:circleLineWidth]; - [circle stroke]; - [circle fill]; - - // triangle inside - CGContextSetFillColorWithColor(context, color.CGColor); - CGFloat triangleLength = width / 2.5; - UIBezierPath *triangle = [self trianglePathWithLength:triangleLength]; - UIOffset offset = UIOffsetMake(width / 2 - triangleLength * tan(M_PI / 6) / 2, width / 2 - triangleLength / 2); - [triangle applyTransform:CGAffineTransformMakeTranslation(offset.horizontal, offset.vertical)]; - [triangle fill]; - - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return image; -} - -+ (UIImage *)smallPlayImage { - // width and height are equal - CGFloat width = 17; - - UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, width), NO, 0); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - - UIColor *color = kIconsColor; - CGContextSetFillColorWithColor(context, color.CGColor); - UIBezierPath *path = [self trianglePathWithLength:width]; - [path fill]; - - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return image; -} - -+ (UIImage *)pauseImage { - CGSize size = CGSizeMake(12, 18); - - UIGraphicsBeginImageContextWithOptions(size, NO, 0); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - - UIColor *color = kIconsColor; - CGContextSetStrokeColorWithColor(context, color.CGColor); - CGFloat lineWidth = 2; - UIBezierPath *path = [UIBezierPath bezierPath]; - [path moveToPoint:CGPointMake(lineWidth / 2, 0)]; - [path addLineToPoint:CGPointMake(lineWidth / 2, size.height)]; - [path moveToPoint:CGPointMake(size.width - lineWidth / 2, 0)]; - [path addLineToPoint:CGPointMake(size.width - lineWidth / 2, size.height)]; - [path setLineWidth:lineWidth]; - [path stroke]; - - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return image; -} - -// @param length of the triangle side -+ (UIBezierPath *)trianglePathWithLength:(CGFloat)length { - UIBezierPath *path = [UIBezierPath bezierPath]; - [path moveToPoint:CGPointZero]; - [path addLineToPoint:CGPointMake(length * cos(M_PI / 6), length / 2)]; - [path addLineToPoint:CGPointMake(0, length)]; - [path closePath]; - return path; -} - -@end - -@implementation QMUIZoomImageViewVideoToolbar - -- (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - QMUIZoomImageViewVideoToolbar *appearance = [QMUIZoomImageViewVideoToolbar appearance]; - _contentInsets = appearance.contentInsets; - _playButtonImage = appearance.playButtonImage; - _pauseButtonImage = appearance.pauseButtonImage; - - self.backgroundColor = UIColorMakeWithRGBA(.5, 255, 0, 0); - - _playButton = [[QMUIButton alloc] init]; - self.playButton.qmui_outsideEdge = UIEdgeInsetsMake(-10, -10, -10, -10); - [self.playButton setImage:self.playButtonImage forState:UIControlStateNormal]; - [self addSubview:self.playButton]; - - _pauseButton = [[QMUIButton alloc] init]; - self.pauseButton.qmui_outsideEdge = UIEdgeInsetsMake(-10, -10, -10, -10); - [self.pauseButton setImage:self.pauseButtonImage forState:UIControlStateNormal]; - [self addSubview:self.pauseButton]; - - _slider = [[QMUISlider 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 addSubview:self.slider]; - - _sliderLeftLabel = [[UILabel alloc] initWithFont:UIFontMake(12) textColor:UIColorWhite]; - self.sliderLeftLabel.textAlignment = NSTextAlignmentCenter; - [self addSubview:self.sliderLeftLabel]; - - _sliderRightLabel = [[UILabel alloc] init]; - [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; - } - return self; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - - CGFloat contentHeight = [self maxHeightAmongViews:@[self.playButton, self.pauseButton, self.sliderLeftLabel, self.sliderRightLabel, self.slider]]; - - self.playButton.frame = ({ - CGSize size = [self.playButton sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]; - CGRectFlatMake(self.contentInsets.left, contentHeight / 2 - size.height / 2 + self.contentInsets.top, size.width, size.height); - }); - - self.pauseButton.frame = ({ - CGSize size = [self.pauseButton sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]; - CGRectFlatMake(CGRectGetMidX(self.playButton.frame) - size.width / 2, CGRectGetMidY(self.playButton.frame) - size.height / 2, size.width, size.height); - }); - - CGFloat timeLabelWidth = 55; - self.sliderLeftLabel.frame = ({ - CGFloat marginLeft = 19; - CGRectFlatMake(CGRectGetMaxX(self.playButton.frame) + marginLeft, self.contentInsets.top, timeLabelWidth, contentHeight); - }); - self.sliderRightLabel.frame = ({ - CGRectFlatMake(CGRectGetWidth(self.bounds) - self.contentInsets.right - timeLabelWidth, self.contentInsets.top, timeLabelWidth, contentHeight); - }); - self.slider.frame = ({ - CGFloat marginToLabel = 4; - CGFloat x = CGRectGetMaxX(self.sliderLeftLabel.frame) + marginToLabel; - CGRectFlatMake(x, self.contentInsets.top, CGRectGetMinX(self.sliderRightLabel.frame) - marginToLabel - x, contentHeight); - }); -} - -- (CGSize)sizeThatFits:(CGSize)size { - CGFloat contentHeight = [self maxHeightAmongViews:@[self.playButton, self.pauseButton, self.sliderLeftLabel, self.sliderRightLabel, self.slider]]; - size.height = contentHeight + UIEdgeInsetsGetVerticalValue(self.contentInsets); - return size; -} - -- (void)setContentInsets:(UIEdgeInsets)contentInsets { - _contentInsets = contentInsets; - [self setNeedsLayout]; -} - -- (void)setPlayButtonImage:(UIImage *)playButtonImage { - _playButtonImage = playButtonImage; - [self.playButton setImage:playButtonImage forState:UIControlStateNormal]; - [self setNeedsLayout]; -} - -- (void)setPauseButtonImage:(UIImage *)pauseButtonImage { - _pauseButtonImage = pauseButtonImage; - [self.pauseButton setImage:pauseButtonImage forState:UIControlStateNormal]; - [self setNeedsLayout]; -} - -// 返回一堆 view 中高度最大的那个的高度 -- (CGFloat)maxHeightAmongViews:(NSArray *)views { - __block CGFloat maxValue = 0; - [views enumerateObjectsUsingBlock:^(UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - CGFloat height = [obj sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)].height; - maxValue = MAX(height, maxValue); - }]; - return maxValue; -} - -@end - -@interface QMUIZoomImageViewVideoToolbar (UIAppearance) - -@end - -@implementation QMUIZoomImageViewVideoToolbar (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self setDefaultAppearance]; - }); -} - -+ (void)setDefaultAppearance { - QMUIZoomImageViewVideoToolbar *appearance = [QMUIZoomImageViewVideoToolbar appearance]; - appearance.contentInsets = UIEdgeInsetsMake(25, 25, 25, 18); - appearance.playButtonImage = [QMUIZoomImageViewImageGenerator smallPlayImage]; - appearance.pauseButtonImage = [QMUIZoomImageViewImageGenerator pauseImage]; -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellData.h b/QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellData.h deleted file mode 100644 index d250defd..00000000 --- a/QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellData.h +++ /dev/null @@ -1,98 +0,0 @@ -// -// QMUIStaticTableViewCellData.h -// qmui -// -// Created by MoLice on 15/5/3. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import -#import - -@class QMUITableViewCell; - -typedef NS_ENUM(NSInteger, QMUIStaticTableViewCellAccessoryType) { - QMUIStaticTableViewCellAccessoryTypeNone, - QMUIStaticTableViewCellAccessoryTypeDisclosureIndicator, - QMUIStaticTableViewCellAccessoryTypeDetailDisclosureButton, - QMUIStaticTableViewCellAccessoryTypeCheckmark, - QMUIStaticTableViewCellAccessoryTypeDetailButton, - QMUIStaticTableViewCellAccessoryTypeSwitch, -}; - -/** - * 一个 cellData 对象用于存储 static tableView(例如设置界面那种列表) 列表里的一行 cell 的基本信息,包括这个 cell 的 class、text、detailText、accessoryView 等。 - * @see QMUIStaticTableViewCellDataSource - */ -@interface QMUIStaticTableViewCellData : NSObject - -/// 当前 cellData 的标志,一般同个 tableView 里的每个 cellData 都会拥有不相同的 identifier -@property(nonatomic, assign) NSInteger identifier; - -/// 当前 cellData 所对应的 indexPath -@property(nonatomic, strong, readonly) NSIndexPath *indexPath; - -/// cell 要使用的 class,默认为 QMUITableViewCell,若要改为自定义 class,必须是 QMUITableViewCell 的子类 -@property(nonatomic, assign) Class cellClass; - -/// init cell 时要使用的 style -@property(nonatomic, assign) UITableViewCellStyle style; - -/// cell 的高度,默认为 TableViewCellNormalHeight -@property(nonatomic, assign) CGFloat height; - -/// cell 左边要显示的图片,将会被设置到 cell.imageView.image -@property(nonatomic, strong) UIImage *image; - -/// cell 的文字,将会被设置到 cell.textLabel.text -@property(nonatomic, copy) NSString *text; - -/// cell 的详细文字,将会被设置到 cell.detailTextLabel.text,所以要求 cellData.style 的值必须是带 detailTextLabel 类型的 style -@property(nonatomic, copy) NSString *detailText; - -/// 当 cell 的点击事件被触发时,要由哪个对象来接收 -@property(nonatomic, assign) id didSelectTarget; - -/// 当 cell 的点击事件被触发时,要向 didSelectTarget 指针发送什么消息以响应事件 -/// @warning 这个 selector 接收一个参数,这个参数也即当前的 QMUIStaticTableViewCellData 对象 -@property(nonatomic, assign) 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; - -/// 当 accessoryType 是某些带 UIControl 的控件时,可通过这两个属性来为 accessoryView 添加操作事件。 -/// 目前支持的类型包括:QMUIStaticTableViewCellAccessoryTypeDetailDisclosureButton、QMUIStaticTableViewCellAccessoryTypeDetailButton、QMUIStaticTableViewCellAccessoryTypeSwitch -/// @warning 这个 selector 接收一个参数,与 didSelectAction 一样,这个参数一般情况下也是当前的 QMUIStaticTableViewCellData 对象,仅在 Switch 时会传 UISwitch 控件的实例 -@property(nonatomic, assign) id accessoryTarget; -@property(nonatomic, assign) SEL accessoryAction; - - - -+ (instancetype)staticTableViewCellDataWithIdentifier:(NSInteger)identifier - image:(UIImage *)image - text:(NSString *)text - detailText:(NSString *)detailText - didSelectTarget:(id)didSelectTarget - didSelectAction:(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 - accessoryType:(QMUIStaticTableViewCellAccessoryType)accessoryType - accessoryValueObject:(NSObject *)accessoryValueObject - accessoryTarget:(id)accessoryTarget - accessoryAction:(SEL)accessoryAction; - -+ (UITableViewCellAccessoryType)tableViewCellAccessoryTypeWithStaticAccessoryType:(QMUIStaticTableViewCellAccessoryType)type; -@end diff --git a/QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellData.m b/QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellData.m deleted file mode 100644 index 0b6e08c9..00000000 --- a/QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellData.m +++ /dev/null @@ -1,100 +0,0 @@ -// -// QMUIStaticTableViewCellData.m -// qmui -// -// Created by MoLice on 15/5/3. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIStaticTableViewCellData.h" -#import "QMUICore.h" -#import "QMUITableViewCell.h" - -@implementation QMUIStaticTableViewCellData - -- (void)setIndexPath:(NSIndexPath *)indexPath { - _indexPath = indexPath; -} - -+ (instancetype)staticTableViewCellDataWithIdentifier:(NSInteger)identifier - image:(UIImage *)image - text:(NSString *)text - detailText:(NSString *)detailText - 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]; -} - -+ (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 - accessoryType:(QMUIStaticTableViewCellAccessoryType)accessoryType - accessoryValueObject:(NSObject *)accessoryValueObject - accessoryTarget:(id)accessoryTarget - accessoryAction:(SEL)accessoryAction { - QMUIStaticTableViewCellData *data = [[QMUIStaticTableViewCellData alloc] init]; - data.identifier = identifier; - data.cellClass = cellClass; - data.style = style; - data.height = height; - data.image = image; - data.text = text; - data.detailText = detailText; - data.didSelectTarget = didSelectTarget; - data.didSelectAction = didSelectAction; - data.accessoryType = accessoryType; - data.accessoryValueObject = accessoryValueObject; - data.accessoryTarget = accessoryTarget; - data.accessoryAction = accessoryAction; - return data; -} - -- (instancetype)init { - if (self = [super init]) { - self.cellClass = [QMUITableViewCell class]; - self.height = TableViewCellNormalHeight; - } - return self; -} - -- (void)setCellClass:(Class)cellClass { - NSAssert([cellClass isSubclassOfClass:[QMUITableViewCell class]], @"%@.cellClass 必须为 QMUITableViewCell 的子类", NSStringFromClass(self.class)); - _cellClass = cellClass; -} - -+ (UITableViewCellAccessoryType)tableViewCellAccessoryTypeWithStaticAccessoryType:(QMUIStaticTableViewCellAccessoryType)type { - switch (type) { - case QMUIStaticTableViewCellAccessoryTypeDisclosureIndicator: - return UITableViewCellAccessoryDisclosureIndicator; - case QMUIStaticTableViewCellAccessoryTypeDetailDisclosureButton: - return UITableViewCellAccessoryDetailDisclosureButton; - case QMUIStaticTableViewCellAccessoryTypeCheckmark: - return UITableViewCellAccessoryCheckmark; - case QMUIStaticTableViewCellAccessoryTypeDetailButton: - return UITableViewCellAccessoryDetailButton; - case QMUIStaticTableViewCellAccessoryTypeSwitch: - default: - return UITableViewCellAccessoryNone; - } -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m b/QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m deleted file mode 100644 index 75204791..00000000 --- a/QMUI/QMUIKit/UIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m +++ /dev/null @@ -1,164 +0,0 @@ -// -// QMUIStaticTableViewCellDataSource.m -// qmui -// -// Created by MoLice on 2017/6/20. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import "QMUIStaticTableViewCellDataSource.h" -#import "QMUICore.h" -#import "QMUIStaticTableViewCellData.h" -#import "QMUITableViewCell.h" -#import "UITableView+QMUIStaticCell.h" -#import "NSObject+QMUI.h" -#import - -@interface QMUIStaticTableViewCellDataSource () -@end - -@implementation QMUIStaticTableViewCellDataSource - -- (instancetype)init { - if (self = [super init]) { - } - return self; -} - -- (instancetype)initWithCellDataSections:(NSArray *> *)cellDataSections { - if (self = [super init]) { - self.cellDataSections = cellDataSections; - } - return self; -} - -- (void)setCellDataSections:(NSArray *> *)cellDataSections { - _cellDataSections = cellDataSections; - [self.tableView reloadData]; -} - -// 在 UITableView (QMUI_StaticCell) 那边会把 tableView 的 property 改为 readwrite,所以这里补上 setter -- (void)setTableView:(UITableView *)tableView { - _tableView = tableView; - // 触发 UITableView (QMUI_StaticCell) 里重写的 setter 里的逻辑 - tableView.delegate = tableView.delegate; - tableView.dataSource = tableView.dataSource; -} - -@end - -@interface QMUIStaticTableViewCellData (Manual) - -@property(nonatomic, strong, readwrite) NSIndexPath *indexPath; -@end - -@implementation QMUIStaticTableViewCellDataSource (Manual) - -- (QMUIStaticTableViewCellData *)cellDataAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section >= self.cellDataSections.count) { - NSLog(@"cellDataWithIndexPath:%@, data not exist in section!", indexPath); - return nil; - } - - NSArray *rowDatas = [self.cellDataSections objectAtIndex:indexPath.section]; - if (indexPath.row >= rowDatas.count) { - NSLog(@"cellDataWithIndexPath:%@, data not exist in row!", indexPath); - return nil; - } - - QMUIStaticTableViewCellData *cellData = [rowDatas objectAtIndex:indexPath.row]; - [cellData setIndexPath:indexPath];// 在这里才为 cellData.indexPath 赋值 - return cellData; -} - -- (NSString *)reuseIdentifierForCellAtIndexPath:(NSIndexPath *)indexPath { - QMUIStaticTableViewCellData *data = [self cellDataAtIndexPath:indexPath]; - return [NSString stringWithFormat:@"cell_%@", @(data.identifier)]; -} - -- (QMUITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath { - - QMUIStaticTableViewCellData *data = [self cellDataAtIndexPath:indexPath]; - if (!data) { - return nil; - } - - NSString *identifier = [self reuseIdentifierForCellAtIndexPath:indexPath]; - - QMUITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:identifier]; - if (!cell) { - cell = [[data.cellClass alloc] initForTableView:self.tableView withStyle:data.style reuseIdentifier:identifier]; - } - cell.imageView.image = data.image; - cell.textLabel.text = data.text; - cell.detailTextLabel.text = data.detailText; - cell.accessoryType = [QMUIStaticTableViewCellData tableViewCellAccessoryTypeWithStaticAccessoryType:data.accessoryType]; - - // 为某些控件类型的accessory添加控件及相应的事件绑定 - if (data.accessoryType == QMUIStaticTableViewCellAccessoryTypeSwitch) { - UISwitch *switcher; - BOOL switcherOn = NO; - if ([cell.accessoryView isKindOfClass:[UISwitch class]]) { - switcher = (UISwitch *)cell.accessoryView; - } else { - switcher = [[UISwitch alloc] init]; - } - if ([data.accessoryValueObject isKindOfClass:[NSNumber class]]) { - switcherOn = [((NSNumber *)data.accessoryValueObject) boolValue]; - } - switcher.on = switcherOn; - [switcher removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents]; - [switcher addTarget:data.accessoryTarget action:data.accessoryAction forControlEvents:UIControlEventValueChanged]; - cell.accessoryView = switcher; - } - - // 统一设置selectionStyle - if (data.accessoryType == QMUIStaticTableViewCellAccessoryTypeSwitch || (!data.didSelectTarget || !data.didSelectAction)) { - cell.selectionStyle = UITableViewCellSelectionStyleNone; - } else { - cell.selectionStyle = UITableViewCellSelectionStyleBlue; - } - - [cell updateCellAppearanceWithIndexPath:indexPath]; - - return cell; -} - -- (CGFloat)heightForRowAtIndexPath:(NSIndexPath *)indexPath { - QMUIStaticTableViewCellData *cellData = [self cellDataAtIndexPath:indexPath]; - return cellData.height; -} - -- (void)didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - QMUIStaticTableViewCellData *cellData = [self cellDataAtIndexPath:indexPath]; - if (!cellData || !cellData.didSelectTarget || !cellData.didSelectAction) { - QMUITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; - if (cell.selectionStyle != UITableViewCellSelectionStyleNone) { - [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; - } - return; - } - - // 1、分发选中事件(UISwitch 类型不支持 didSelect) - if ([cellData.didSelectTarget respondsToSelector:cellData.didSelectAction] && cellData.accessoryType != QMUIStaticTableViewCellAccessoryTypeSwitch) { - BeginIgnorePerformSelectorLeaksWarning - [cellData.didSelectTarget performSelector:cellData.didSelectAction withObject:cellData]; - EndIgnorePerformSelectorLeaksWarning - } - - // 2、处理点击状态(对checkmark类型的cell,选中后自动反选) - if (cellData.accessoryType == QMUIStaticTableViewCellAccessoryTypeCheckmark) { - [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; - } -} - -- (void)accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath { - QMUIStaticTableViewCellData *cellData = [self cellDataAtIndexPath:indexPath]; - if ([cellData.accessoryTarget respondsToSelector:cellData.accessoryAction]) { - BeginIgnorePerformSelectorLeaksWarning - [cellData.accessoryTarget performSelector:cellData.accessoryAction withObject:cellData]; - EndIgnorePerformSelectorLeaksWarning - } -} - -@end diff --git a/QMUI/QMUIKit/UIComponents/StaticTableView/UITableView+QMUIStaticCell.h b/QMUI/QMUIKit/UIComponents/StaticTableView/UITableView+QMUIStaticCell.h deleted file mode 100644 index 6e2ee44d..00000000 --- a/QMUI/QMUIKit/UIComponents/StaticTableView/UITableView+QMUIStaticCell.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// UITableView+QMUIStaticCell.h -// qmui -// -// Created by MoLice on 2017/6/20. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import -#import - -@class QMUIStaticTableViewCellDataSource; - -/** - * 配合 QMUIStaticTableViewCellDataSource 使用,主要负责: - * 1. 提供 property 去绑定一个 static dataSource - * 2. 重写 setDataSource:、setDelegate: 方法,自动实现 UITableViewDataSource、UITableViewDelegate 里一些必要的方法 - * - * 使用方式:初始化一个 QMUIStaticTableViewCellDataSource 并将其赋值给 qmui_staticCellDataSource 属性即可。 - * - * @warning 当要动态更新 dataSource 时,可直接修改 self.qmui_staticCellDataSource.cellDataSections 数组,或者创建一个新的 QMUIStaticTableViewCellDataSource。不管用哪种方法,都不需要手动调用 reloadData,tableView 会自动刷新的。 - */ -@interface UITableView (QMUI_StaticCell) - -@property(nonatomic, strong) QMUIStaticTableViewCellDataSource *qmui_staticCellDataSource; -@end diff --git a/QMUI/QMUIKit/UIComponents/StaticTableView/UITableView+QMUIStaticCell.m b/QMUI/QMUIKit/UIComponents/StaticTableView/UITableView+QMUIStaticCell.m deleted file mode 100644 index 15885d34..00000000 --- a/QMUI/QMUIKit/UIComponents/StaticTableView/UITableView+QMUIStaticCell.m +++ /dev/null @@ -1,96 +0,0 @@ -// -// UITableView+QMUIStaticCell.m -// qmui -// -// Created by MoLice on 2017/6/20. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import "UITableView+QMUIStaticCell.h" -#import "QMUICore.h" -#import "QMUIStaticTableViewCellDataSource.h" -#import - -@interface QMUIStaticTableViewCellDataSource () - -@property(nonatomic, weak, readwrite) UITableView *tableView; -@end - -@implementation UITableView (QMUI_StaticCell) - -+ (void)load { - ReplaceMethod([UITableView class], @selector(setDataSource:), @selector(staticCell_setDataSource:)); - ReplaceMethod([UITableView class], @selector(setDelegate:), @selector(staticCell_setDelegate:)); -} - -static char kAssociatedObjectKey_staticCellDataSource; -- (void)setQmui_staticCellDataSource:(QMUIStaticTableViewCellDataSource *)qmui_staticCellDataSource { - objc_setAssociatedObject(self, &kAssociatedObjectKey_staticCellDataSource, qmui_staticCellDataSource, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - qmui_staticCellDataSource.tableView = self; - [self reloadData]; -} - -- (QMUIStaticTableViewCellDataSource *)qmui_staticCellDataSource { - return (QMUIStaticTableViewCellDataSource *)objc_getAssociatedObject(self, &kAssociatedObjectKey_staticCellDataSource); -} - -- (void)addSelector:(SEL)selector withImplementation:(IMP)implementation types:(const char *)types forObject:(NSObject *)object { - if (!class_addMethod(object.class, selector, implementation, types)) { - QMUILog(@"%@, 尝试为 %@ 添加方法 %@ 失败,可能该类里已经实现了这个方法", NSStringFromClass(self.class), NSStringFromClass(object.class), NSStringFromSelector(selector)); - } -} - -#pragma mark - DataSource - -NSInteger staticCell_numberOfSections (id current_self, SEL current_cmd, UITableView *tableView) { - return tableView.qmui_staticCellDataSource.cellDataSections.count; -} - -NSInteger staticCell_numberOfRows (id current_self, SEL current_cmd, UITableView *tableView, NSInteger section) { - return tableView.qmui_staticCellDataSource.cellDataSections[section].count; -} - -id staticCell_cellForRow (id current_self, SEL current_cmd, UITableView *tableView, NSIndexPath *indexPath) { - QMUITableViewCell *cell = [tableView.qmui_staticCellDataSource cellForRowAtIndexPath:indexPath]; - return cell; -} - -- (void)staticCell_setDataSource:(id)dataSource { - if (dataSource && self.qmui_staticCellDataSource) { - // 这些 addMethod 的操作必须要在系统的 setDataSource 执行前就执行,否则 tableView 可能会认为不存在这些 method - // 并且 addMethod 操作执行一次之后,直到 App 进程被杀死前都会生效,所以多次进入这段代码可能就会提示添加方法失败,请不用在意 - [self addSelector:@selector(numberOfSectionsInTableView:) withImplementation:(IMP)staticCell_numberOfSections types:"l@:@" forObject:dataSource]; - [self addSelector:@selector(tableView:numberOfRowsInSection:) withImplementation:(IMP)staticCell_numberOfRows types:"l@:@l" forObject:dataSource]; - [self addSelector:@selector(tableView:cellForRowAtIndexPath:) withImplementation:(IMP)staticCell_cellForRow types:"@@:@@" forObject:dataSource]; - } - - [self staticCell_setDataSource:dataSource]; -} - -#pragma mark - Delegate - -CGFloat staticCell_heightForRow (id current_self, SEL current_cmd, UITableView *tableView, NSIndexPath *indexPath) { - return [tableView.qmui_staticCellDataSource heightForRowAtIndexPath:indexPath]; -} - -void staticCell_didSelectRow (id current_self, SEL current_cmd, UITableView *tableView, NSIndexPath *indexPath) { - [tableView.qmui_staticCellDataSource didSelectRowAtIndexPath:indexPath]; -} - -void staticCell_accessoryButtonTapped (id current_self, SEL current_cmd, UITableView *tableView, NSIndexPath *indexPath) { - [tableView.qmui_staticCellDataSource accessoryButtonTappedForRowWithIndexPath:indexPath]; -} - -- (void)staticCell_setDelegate:(id)delegate { - if (delegate && self.qmui_staticCellDataSource) { - // 这些 addMethod 的操作必须要在系统的 setDataSource 执行前就执行,否则 tableView 可能会认为不存在这些 method - // 并且 addMethod 操作执行一次之后,直到 App 进程被杀死前都会生效,所以多次进入这段代码可能就会提示添加方法失败,请不用在意 - [self addSelector:@selector(tableView:heightForRowAtIndexPath:) withImplementation:(IMP)staticCell_heightForRow types:"d@:@@" forObject:delegate]; - [self addSelector:@selector(tableView:didSelectRowAtIndexPath:) withImplementation:(IMP)staticCell_didSelectRow types:"v@:@@" forObject:delegate]; - [self addSelector:@selector(tableView:accessoryButtonTappedForRowWithIndexPath:) withImplementation:(IMP)staticCell_accessoryButtonTapped types:"v@:@@" forObject:delegate]; - } - - [self staticCell_setDelegate:delegate]; -} - -@end diff --git a/QMUI/QMUIKit/UICore/QMUICommonDefines.h b/QMUI/QMUIKit/UICore/QMUICommonDefines.h deleted file mode 100644 index 23a11b52..00000000 --- a/QMUI/QMUIKit/UICore/QMUICommonDefines.h +++ /dev/null @@ -1,563 +0,0 @@ -// -// QMUICommonDefines.h -// qmui -// -// Created by QQMail on 14-6-23. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import -#import -#import "QMUIHelper.h" -#import "UIFont+QMUI.h" - -#pragma mark - 变量-编译相关 - -// 判断当前是否debug编译模式 -#ifdef DEBUG -#define IS_DEBUG YES -#else -#define IS_DEBUG NO -#endif - -/// 使用iOS7 API时要加`ifdef IOS7_SDK_ALLOWED`的判断 - -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 -#define IOS7_SDK_ALLOWED YES -#endif - - -/// 使用iOS8 API时要加`ifdef IOS8_SDK_ALLOWED`的判断 - -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 -#define IOS8_SDK_ALLOWED YES -#endif - - -/// 使用iOS9 API时要加`ifdef IOS9_SDK_ALLOWED`的判断 - -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 -#define IOS9_SDK_ALLOWED YES -#endif - - -/// 使用iOS10 API时要加`ifdef IOS10_SDK_ALLOWED`的判断 - -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 -#define IOS10_SDK_ALLOWED YES -#endif - - -/// 使用iOS11 API时要加`ifdef IOS11_SDK_ALLOWED`的判断 - -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 -#define IOS11_SDK_ALLOWED YES -#endif - -#pragma mark - Clang - -#define ArgumentToString(macro) #macro -#define ClangWarningConcat(warning_name) ArgumentToString(clang diagnostic ignored warning_name) - -// 参数可直接传入 clang 的 warning 名,warning 列表参考:http://fuckingclangwarnings.com/ -#define BeginIgnoreClangWarning(warningName) _Pragma("clang diagnostic push") _Pragma(ClangWarningConcat(#warningName)) -#define EndIgnoreClangWarning _Pragma("clang diagnostic pop") - -#define BeginIgnorePerformSelectorLeaksWarning BeginIgnoreClangWarning(-Warc-performSelector-leaks) -#define EndIgnorePerformSelectorLeaksWarning EndIgnoreClangWarning - -#define BeginIgnoreAvailabilityWarning BeginIgnoreClangWarning(-Wpartial-availability) -#define EndIgnoreAvailabilityWarning EndIgnoreClangWarning - -#define BeginIgnoreDeprecatedWarning BeginIgnoreClangWarning(-Wdeprecated-declarations) -#define EndIgnoreDeprecatedWarning EndIgnoreClangWarning - - -#pragma mark - 变量-设备相关 - -// 设备类型 -#define IS_IPAD [QMUIHelper isIPad] -#define IS_IPAD_PRO [QMUIHelper isIPadPro] -#define IS_IPOD [QMUIHelper isIPod] -#define IS_IPHONE [QMUIHelper isIPhone] -#define IS_SIMULATOR [QMUIHelper isSimulator] - -// 操作系统版本号 -#define IOS_VERSION ([[[UIDevice currentDevice] systemVersion] floatValue]) - -// 是否横竖屏 -// 用户界面横屏了才会返回YES -#define IS_LANDSCAPE UIInterfaceOrientationIsLandscape([[UIApplication sharedApplication] statusBarOrientation]) -// 无论支不支持横屏,只要设备横屏了,就会返回YES -#define IS_DEVICE_LANDSCAPE UIDeviceOrientationIsLandscape([[UIDevice currentDevice] orientation]) - - -// 屏幕宽度,会根据横竖屏的变化而变化 -#define SCREEN_WIDTH (IOS_VERSION >= 8.0 ? [[UIScreen mainScreen] bounds].size.width : (IS_LANDSCAPE ? [[UIScreen mainScreen] bounds].size.height : [[UIScreen mainScreen] bounds].size.width)) - -// 屏幕宽度,跟横竖屏无关 -#define DEVICE_WIDTH (IOS_VERSION >= 8.0 ? (IS_LANDSCAPE ? [[UIScreen mainScreen] bounds].size.height : [[UIScreen mainScreen] bounds].size.width) : [[UIScreen mainScreen] bounds].size.width) - -// 屏幕高度,会根据横竖屏的变化而变化 -#define SCREEN_HEIGHT (IOS_VERSION >= 8.0 ? [[UIScreen mainScreen] bounds].size.height : (IS_LANDSCAPE ? [[UIScreen mainScreen] bounds].size.width : [[UIScreen mainScreen] bounds].size.height)) - -// 屏幕高度,跟横竖屏无关 -#define DEVICE_HEIGHT (IOS_VERSION >= 8.0 ? (IS_LANDSCAPE ? [[UIScreen mainScreen] bounds].size.width : [[UIScreen mainScreen] bounds].size.height) : [[UIScreen mainScreen] bounds].size.height) - -// 设备屏幕尺寸 -#define IS_55INCH_SCREEN [QMUIHelper is55InchScreen] -#define IS_47INCH_SCREEN [QMUIHelper is47InchScreen] -#define IS_40INCH_SCREEN [QMUIHelper is40InchScreen] -#define IS_35INCH_SCREEN [QMUIHelper is35InchScreen] - -// 是否Retina -#define IS_RETINASCREEN ([[UIScreen mainScreen] scale] >= 2.0) - - -#pragma mark - 变量-布局相关 - -// bounds && nativeBounds / scale && nativeScale -#define ScreenBoundsSize ([[UIScreen mainScreen] bounds].size) -#define ScreenNativeBoundsSize (IOS_VERSION >= 8.0 ? ([[UIScreen mainScreen] nativeBounds].size) : ScreenBoundsSize) -#define ScreenScale ([[UIScreen mainScreen] scale]) -#define ScreenNativeScale (IOS_VERSION >= 8.0 ? ([[UIScreen mainScreen] nativeScale]) : ScreenScale) -// 区分设备是否处于放大模式(iPhone 6及以上的设备支持放大模式) -#define ScreenInDisplayZoomMode (ScreenNativeScale > ScreenScale) - -// 状态栏高度(来电等情况下,状态栏高度会发生变化,所以应该实时计算) -#define StatusBarHeight (IOS_VERSION >= 8.0 ? ([[UIApplication sharedApplication] statusBarFrame].size.height) : (IS_LANDSCAPE ? ([[UIApplication sharedApplication] statusBarFrame].size.width) : ([[UIApplication sharedApplication] statusBarFrame].size.height))) - -// navigationBar相关frame -#define NavigationBarHeight (IS_LANDSCAPE ? PreferredVarForDevices(44, 32, 32, 32) : 44) - -// toolBar的相关frame -#define ToolBarHeight (IS_LANDSCAPE ? PreferredVarForDevices(44, 32, 32, 32) : 44) - -#define TabBarHeight 49 - -// 除去navigationBar和toolbar后的中间内容区域 -#define NavigationContentHeight(viewController) (CGRectGetHeight(viewController.view.frame) - NavigationBarHeight - StatusBarHeight - (viewController.navigationController.toolbarHidden ? 0 : CGRectGetHeight(viewController.navigationController.toolbar.frame))) - -// 兼容controller.view的subView的top值在不同iOS版本下的差异 -#define NavigationContentTop (StatusBarHeight + NavigationBarHeight)// 这是动态获取的 -#define NavigationContentStaticTop (20 + NavigationBarHeight) // 不动态从状态栏获取高度,避免来电模式下多算了20pt(来电模式下系统会把UIViewController.view的frame往下移动20pt) -#define NavigationContentOriginY(y) (NavigationContentTop + y) - -// 获取一个像素 -#define PixelOne [QMUIHelper pixelOne] - -// 获取最合适的适配值,默认以varFor55Inch为准,也即偏向大屏 -#define PreferredVarForDevices(varFor55Inch, varFor47Inch, varFor40Inch, varFor35Inch) (IS_35INCH_SCREEN ? varFor35Inch : (IS_40INCH_SCREEN ? varFor40Inch : (IS_47INCH_SCREEN ? varFor47Inch : varFor55Inch))) - -// 同上,加多一个iPad的参数 -#define PreferredVarForUniversalDevices(varForPad, varFor55Inch, varFor47Inch, varFor40Inch, varFor35Inch) (IS_IPAD ? varForPad :(IS_55INCH_SCREEN ? varFor55Inch : (IS_47INCH_SCREEN ? varFor47Inch : (IS_40INCH_SCREEN ? varFor40Inch : varFor35Inch)))) - - -#pragma mark - 方法-创建器 - -// 使用文件名(不带后缀名)创建一个UIImage对象,会被系统缓存,适用于大量复用的小资源图 -#define UIImageMake(img) \ -BeginIgnoreAvailabilityWarning \ -(IOS_VERSION >= 8.0 ? [UIImage imageNamed:img inBundle:nil compatibleWithTraitCollection:nil] : [UIImage imageNamed:img]) \ -EndIgnoreAvailabilityWarning - -// 使用文件名(不带后缀名,仅限png)创建一个UIImage对象,不会被系统缓存,用于不被复用的图片,特别是大图 -#define UIImageMakeWithFile(name) UIImageMakeWithFileAndSuffix(name, @"png") -#define UIImageMakeWithFileAndSuffix(name, suffix) [UIImage imageWithContentsOfFile:[NSString stringWithFormat:@"%@/%@.%@", [[NSBundle mainBundle] resourcePath], name, suffix]] - -// 字体相关创建器,包括动态字体的支持 -#define UIFontMake(size) [UIFont systemFontOfSize:size] -#define UIFontItalicMake(size) [UIFont italicSystemFontOfSize:size] // 斜体只对数字和字母有效,中文无效 -#define UIFontBoldMake(size) [UIFont boldSystemFontOfSize:size] -#define UIFontBoldWithFont(_font) [UIFont boldSystemFontOfSize:_font.pointSize] -#define UIFontLightMake(size) [UIFont qmui_lightSystemFontOfSize:size] -#define UIFontLightWithFont(_font) [UIFont qmui_lightSystemFontOfSize:_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] - -// UIColor相关创建器 -#define UIColorMake(r, g, b) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:1] -#define UIColorMakeWithRGBA(r, g, b, a) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:a/1.0] -#define UIColorMakeWithHex(hex) [UIColor qmui_colorWithHexString:hex] - - -#pragma mark - 数学计算 - -#define AngleWithDegrees(deg) (M_PI * (deg) / 180.0) - - -#pragma mark - 动画 - -#define QMUIViewAnimationOptionsCurveOut (7<<16) -#define QMUIViewAnimationOptionsCurveIn (8<<16) - - -#pragma mark - 其他 - -#define StringFromBOOL(_flag) (_flag ? @"YES" : @"NO") - -#define QMUILog(...) [[QMUIHelper sharedInstance] printLogWithCalledFunction:__FUNCTION__ log:__VA_ARGS__] - -#pragma mark - 方法-C对象、结构操作 - -CG_INLINE void -ReplaceMethod(Class _class, SEL _originSelector, SEL _newSelector) { - Method oriMethod = class_getInstanceMethod(_class, _originSelector); - Method newMethod = class_getInstanceMethod(_class, _newSelector); - BOOL isAddedMethod = class_addMethod(_class, _originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)); - if (isAddedMethod) { - class_replaceMethod(_class, _newSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); - } else { - method_exchangeImplementations(oriMethod, newMethod); - } -} - -#pragma mark - CGFloat - -/** - * 基于指定的倍数,对传进来的 floatValue 进行像素取整。若指定倍数为0,则表示以当前设备的屏幕倍数为准。 - * - * 例如传进来 “2.1”,在 2x 倍数下会返回 2.5(0.5pt 对应 1px),在 3x 倍数下会返回 2.333(0.333pt 对应 1px)。 - */ -CG_INLINE CGFloat -flatSpecificScale(CGFloat floatValue, CGFloat scale) { - scale = scale == 0 ? ScreenScale : scale; - CGFloat flattedValue = ceil(floatValue * scale) / scale; - return flattedValue; -} - -/** - * 基于当前设备的屏幕倍数,对传进来的 floatValue 进行像素取整。 - * - * 注意如果在 Core Graphic 绘图里使用时,要注意当前画布的倍数是否和设备屏幕倍数一致,若不一致,不可使用 flat() 函数,而应该用 flatSpecificScale - */ -CG_INLINE CGFloat -flat(CGFloat floatValue) { - return flatSpecificScale(floatValue, 0); -} - -/** - * 类似flat(),只不过 flat 是向上取整,而 floorInPixel 是向下取整 - */ -CG_INLINE CGFloat -floorInPixel(CGFloat floatValue) { - CGFloat resultValue = floor(floatValue * ScreenScale) / ScreenScale; - return resultValue; -} - -CG_INLINE BOOL -between(CGFloat minimumValue, CGFloat value, CGFloat maximumValue) { - return minimumValue < value && value < maximumValue; -} - -CG_INLINE BOOL -betweenOrEqual(CGFloat minimumValue, CGFloat value, CGFloat maximumValue) { - return minimumValue <= value && value <= maximumValue; -} - -/** - * 调整给定的某个 CGFloat 值的小数点精度,超过精度的部分按四舍五入处理。 - * - * 例如 CGFloatToFixed(0.3333, 2) 会返回 0.33,而 CGFloatToFixed(0.6666, 2) 会返回 0.67 - * - * @warning 参数类型为 CGFloat,也即意味着不管传进来的是 float 还是 double 最终都会被强制转换成 CGFloat 再做计算 - */ -CG_INLINE CGFloat -CGFloatToFixed(CGFloat value, NSUInteger precision) { - NSString *formatString = [NSString stringWithFormat:@"%%.%@f", @(precision)]; - NSString *toString = [NSString stringWithFormat:formatString, value]; - #if defined(__LP64__) && __LP64__ - CGFloat result = [toString doubleValue]; - #else - CGFloat result = [toString floatValue]; - #endif - return result; -} - -/// 用于居中运算 -CG_INLINE CGFloat -CGFloatGetCenter(CGFloat parent, CGFloat child) { - return flat((parent - child) / 2.0); -} - -#pragma mark - CGPoint - -/// 两个point相加 -CG_INLINE CGPoint -CGPointUnion(CGPoint point1, CGPoint point2) { - return CGPointMake(flat(point1.x + point2.x), flat(point1.y + point2.y)); -} - -/// 获取rect的center,包括rect本身的x/y偏移 -CG_INLINE CGPoint -CGPointGetCenterWithRect(CGRect rect) { - return CGPointMake(flat(CGRectGetMidX(rect)), flat(CGRectGetMidY(rect))); -} - -CG_INLINE CGPoint -CGPointGetCenterWithSize(CGSize size) { - return CGPointMake(flat(size.width / 2.0), flat(size.height / 2.0)); -} - -CG_INLINE CGPoint -CGPointToFixed(CGPoint point, NSUInteger precision) { - CGPoint result = CGPointMake(CGFloatToFixed(point.x, precision), CGFloatToFixed(point.y, precision)); - return result; -} - -#pragma mark - UIEdgeInsets - -/// 获取UIEdgeInsets在水平方向上的值 -CG_INLINE CGFloat -UIEdgeInsetsGetHorizontalValue(UIEdgeInsets insets) { - return insets.left + insets.right; -} - -/// 获取UIEdgeInsets在垂直方向上的值 -CG_INLINE CGFloat -UIEdgeInsetsGetVerticalValue(UIEdgeInsets insets) { - return insets.top + insets.bottom; -} - -/// 将两个UIEdgeInsets合并为一个 -CG_INLINE UIEdgeInsets -UIEdgeInsetsConcat(UIEdgeInsets insets1, UIEdgeInsets insets2) { - insets1.top += insets2.top; - insets1.left += insets2.left; - insets1.bottom += insets2.bottom; - insets1.right += insets2.right; - return insets1; -} - -CG_INLINE UIEdgeInsets -UIEdgeInsetsSetTop(UIEdgeInsets insets, CGFloat top) { - insets.top = flat(top); - return insets; -} - -CG_INLINE UIEdgeInsets -UIEdgeInsetsSetLeft(UIEdgeInsets insets, CGFloat left) { - insets.left = flat(left); - return insets; -} -CG_INLINE UIEdgeInsets -UIEdgeInsetsSetBottom(UIEdgeInsets insets, CGFloat bottom) { - insets.bottom = flat(bottom); - return insets; -} - -CG_INLINE UIEdgeInsets -UIEdgeInsetsSetRight(UIEdgeInsets insets, CGFloat right) { - insets.right = flat(right); - return insets; -} - -CG_INLINE UIEdgeInsets -UIEdgeInsetsToFixed(UIEdgeInsets insets, NSUInteger precision) { - UIEdgeInsets result = UIEdgeInsetsMake(CGFloatToFixed(insets.top, precision), CGFloatToFixed(insets.left, precision), CGFloatToFixed(insets.bottom, precision), CGFloatToFixed(insets.right, precision)); - return result; -} - -#pragma mark - CGSize - -/// 判断一个size是否为空(宽或高为0) -CG_INLINE BOOL -CGSizeIsEmpty(CGSize size) { - return size.width <= 0 || size.height <= 0; -} - -/// 将一个CGSize像素对齐 -CG_INLINE CGSize -CGSizeFlatted(CGSize size) { - return CGSizeMake(flat(size.width), flat(size.height)); -} - -/// 将一个 CGSize 以 pt 为单位向上取整 -CG_INLINE CGSize -CGSizeCeil(CGSize size) { - return CGSizeMake(ceil(size.width), ceil(size.height)); -} - -/// 将一个 CGSize 以 pt 为单位向下取整 -CG_INLINE CGSize -CGSizeFloor(CGSize size) { - return CGSizeMake(floor(size.width), floor(size.height)); -} - -CG_INLINE CGSize -CGSizeToFixed(CGSize size, NSUInteger precision) { - CGSize result = CGSizeMake(CGFloatToFixed(size.width, precision), CGFloatToFixed(size.height, precision)); - return result; -} - -#pragma mark - CGRect - -/// 判断一个 CGRect 是否存在NaN -CG_INLINE BOOL -CGRectIsNaN(CGRect rect) { - return isnan(rect.origin.x) || isnan(rect.origin.y) || isnan(rect.size.width) || isnan(rect.size.height); -} - -/// 判断一个 CGRect 是否合法(例如不带无穷大的值、不带非法数字、不为空) -CG_INLINE BOOL -CGRectIsValidated(CGRect rect) { - return !CGRectIsNull(rect) && !CGRectIsEmpty(rect) && !CGRectIsInfinite(rect) && !CGRectIsNaN(rect); -} - -/// 创建一个像素对齐的CGRect -CG_INLINE CGRect -CGRectFlatMake(CGFloat x, CGFloat y, CGFloat width, CGFloat height) { - return CGRectMake(flat(x), flat(y), flat(width), flat(height)); -} - -/// 对CGRect的x/y、width/height都调用一次flat,以保证像素对齐 -CG_INLINE CGRect -CGRectFlatted(CGRect rect) { - return CGRectMake(flat(rect.origin.x), flat(rect.origin.y), flat(rect.size.width), flat(rect.size.height)); -} - -/// 为一个CGRect叠加scale计算 -CG_INLINE CGRect -CGRectApplyScale(CGRect rect, CGFloat scale) { - return CGRectFlatted(CGRectMake(CGRectGetMinX(rect) * scale, CGRectGetMinY(rect) * scale, CGRectGetWidth(rect) * scale, CGRectGetHeight(rect) * scale)); -} - -/// 计算view的水平居中,传入父view和子view的frame,返回子view在水平居中时的x值 -CG_INLINE CGFloat -CGRectGetMinXHorizontallyCenterInParentRect(CGRect parentRect, CGRect childRect) { - return flat((CGRectGetWidth(parentRect) - CGRectGetWidth(childRect)) / 2.0); -} - -/// 计算view的垂直居中,传入父view和子view的frame,返回子view在垂直居中时的y值 -CG_INLINE CGFloat -CGRectGetMinYVerticallyCenterInParentRect(CGRect parentRect, CGRect childRect) { - return flat((CGRectGetHeight(parentRect) - CGRectGetHeight(childRect)) / 2.0); -} - -/// 返回值:同一个坐标系内,想要layoutingRect和已布局完成的referenceRect保持垂直居中时,layoutingRect的originY -CG_INLINE CGFloat -CGRectGetMinYVerticallyCenter(CGRect referenceRect, CGRect layoutingRect) { - return CGRectGetMinY(referenceRect) + CGRectGetMinYVerticallyCenterInParentRect(referenceRect, layoutingRect); -} - -/// 返回值:同一个坐标系内,想要layoutingRect和已布局完成的referenceRect保持水平居中时,layoutingRect的originX -CG_INLINE CGFloat -CGRectGetMinXHorizontallyCenter(CGRect referenceRect, CGRect layoutingRect) { - return CGRectGetMinX(referenceRect) + CGRectGetMinXHorizontallyCenterInParentRect(referenceRect, layoutingRect); -} - -/// 为给定的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; -} - -/// 传入size,返回一个x/y为0的CGRect -CG_INLINE CGRect -CGRectMakeWithSize(CGSize size) { - return CGRectMake(0, 0, size.width, size.height); -} - -CG_INLINE CGRect -CGRectFloatTop(CGRect rect, CGFloat top) { - rect.origin.y = top; - return rect; -} - -CG_INLINE CGRect -CGRectFloatBottom(CGRect rect, CGFloat bottom) { - rect.origin.y = bottom - CGRectGetHeight(rect); - return rect; -} - -CG_INLINE CGRect -CGRectFloatRight(CGRect rect, CGFloat right) { - rect.origin.x = right - CGRectGetWidth(rect); - return rect; -} - -CG_INLINE CGRect -CGRectFloatLeft(CGRect rect, CGFloat left) { - rect.origin.x = left; - return rect; -} - -/// 保持rect的左边缘不变,改变其宽度,使右边缘靠在right上 -CG_INLINE CGRect -CGRectLimitRight(CGRect rect, CGFloat rightLimit) { - rect.size.width = rightLimit - rect.origin.x; - return rect; -} - -/// 保持rect右边缘不变,改变其宽度和origin.x,使其左边缘靠在left上。只适合那种右边缘不动的view -/// 先改变origin.x,让其靠在offset上 -/// 再改变size.width,减少同样的宽度,以抵消改变origin.x带来的view移动,从而保证view的右边缘是不动的 -CG_INLINE CGRect -CGRectLimitLeft(CGRect rect, CGFloat leftLimit) { - CGFloat subOffset = leftLimit - rect.origin.x; - rect.origin.x = leftLimit; - rect.size.width = rect.size.width - subOffset; - return rect; -} - -/// 限制rect的宽度,超过最大宽度则截断,否则保持rect的宽度不变 -CG_INLINE CGRect -CGRectLimitMaxWidth(CGRect rect, CGFloat maxWidth) { - CGFloat width = CGRectGetWidth(rect); - rect.size.width = width > maxWidth ? maxWidth : width; - return rect; -} - -CG_INLINE CGRect -CGRectSetX(CGRect rect, CGFloat x) { - rect.origin.x = flat(x); - return rect; -} - -CG_INLINE CGRect -CGRectSetY(CGRect rect, CGFloat y) { - rect.origin.y = flat(y); - return rect; -} - -CG_INLINE CGRect -CGRectSetXY(CGRect rect, CGFloat x, CGFloat y) { - rect.origin.x = flat(x); - rect.origin.y = flat(y); - return rect; -} - -CG_INLINE CGRect -CGRectSetWidth(CGRect rect, CGFloat width) { - rect.size.width = flat(width); - return rect; -} - -CG_INLINE CGRect -CGRectSetHeight(CGRect rect, CGFloat height) { - rect.size.height = flat(height); - return rect; -} - -CG_INLINE CGRect -CGRectSetSize(CGRect rect, CGSize size) { - rect.size = CGSizeFlatted(size); - return rect; -} - -CG_INLINE CGRect -CGRectToFixed(CGRect rect, NSUInteger precision) { - CGRect result = CGRectMake(CGFloatToFixed(CGRectGetMinX(rect), precision), - CGFloatToFixed(CGRectGetMinY(rect), precision), - CGFloatToFixed(CGRectGetWidth(rect), precision), - CGFloatToFixed(CGRectGetHeight(rect), precision)); - return result; -} diff --git a/QMUI/QMUIKit/UICore/QMUIConfiguration.h b/QMUI/QMUIKit/UICore/QMUIConfiguration.h deleted file mode 100644 index e56d0955..00000000 --- a/QMUI/QMUIKit/UICore/QMUIConfiguration.h +++ /dev/null @@ -1,189 +0,0 @@ -// -// QMUIConfiguration.h -// qmui -// -// Created by QQMail on 15/3/29. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import -#import -#import "QMUINavigationController.h" - -/** - * 维护项目全局 UI 配置的单例,通过业务项目自己的 QMUIConfigurationTemplate 来为这个单例赋值,而业务代码里则通过 QMUIConfigurationMacros.h 文件里的宏来使用这些值。 - */ -@interface QMUIConfiguration : NSObject - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - Global Color - -@property(nonatomic, strong) UIColor *clearColor; -@property(nonatomic, strong) UIColor *whiteColor; -@property(nonatomic, strong) UIColor *blackColor; -@property(nonatomic, strong) UIColor *grayColor; -@property(nonatomic, strong) UIColor *grayDarkenColor; -@property(nonatomic, strong) UIColor *grayLightenColor; -@property(nonatomic, strong) UIColor *redColor; -@property(nonatomic, strong) UIColor *greenColor; -@property(nonatomic, strong) UIColor *blueColor; -@property(nonatomic, strong) UIColor *yellowColor; - -@property(nonatomic, strong) UIColor *linkColor; -@property(nonatomic, strong) UIColor *disabledColor; -@property(nonatomic, strong, nullable) UIColor *backgroundColor; -@property(nonatomic, strong) UIColor *maskDarkColor; -@property(nonatomic, strong) UIColor *maskLightColor; -@property(nonatomic, strong) UIColor *separatorColor; -@property(nonatomic, strong) UIColor *separatorDashedColor; -@property(nonatomic, strong) UIColor *placeholderColor; - -@property(nonatomic, strong) UIColor *testColorRed; -@property(nonatomic, strong) UIColor *testColorGreen; -@property(nonatomic, strong) UIColor *testColorBlue; - -#pragma mark - UIControl - -@property(nonatomic, assign) CGFloat controlHighlightedAlpha; -@property(nonatomic, assign) CGFloat controlDisabledAlpha; - -#pragma mark - UIButton - -@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 *textFieldTintColor; -@property(nonatomic, assign) UIEdgeInsets textFieldTextInsets; - -#pragma mark - NavigationBar - -@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, strong, nullable) UIImage *navBarShadowImage; -@property(nonatomic, strong, nullable) UIColor *navBarBarTintColor; -@property(nonatomic, strong, nullable) UIColor *navBarTintColor; -@property(nonatomic, strong, nullable) UIColor *navBarTitleColor; -@property(nonatomic, strong, nullable) UIFont *navBarTitleFont; -@property(nonatomic, assign) UIOffset navBarBackButtonTitlePositionAdjustment; -@property(nonatomic, strong, nullable) UIImage *navBarBackIndicatorImage; -@property(nonatomic, strong) UIImage *navBarCloseButtonImage; - -@property(nonatomic, assign) CGFloat navBarLoadingMarginRight; -@property(nonatomic, assign) CGFloat navBarAccessoryViewMarginLeft; -@property(nonatomic, assign) UIActivityIndicatorViewStyle navBarActivityIndicatorViewStyle; -@property(nonatomic, strong) UIImage *navBarAccessoryViewTypeDisclosureIndicatorImage; - -#pragma mark - TabBar - -@property(nonatomic, strong, nullable) UIImage *tabBarBackgroundImage; -@property(nonatomic, strong, nullable) UIColor *tabBarBarTintColor; -@property(nonatomic, strong, nullable) UIColor *tabBarShadowImageColor; -@property(nonatomic, strong, nullable) UIColor *tabBarTintColor; -@property(nonatomic, strong, nullable) UIColor *tabBarItemTitleColor; -@property(nonatomic, strong, nullable) UIColor *tabBarItemTitleColorSelected; -@property(nonatomic, strong, nullable) UIFont *tabBarItemTitleFont; - -#pragma mark - Toolbar - -@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, strong, nullable) UIColor *toolBarBarTintColor; -@property(nonatomic, strong, nullable) UIColor *toolBarShadowImageColor; -@property(nonatomic, strong, nullable) UIFont *toolBarButtonFont; - -#pragma mark - SearchBar - -@property(nonatomic, strong, nullable) UIColor *searchBarTextFieldBackground; -@property(nonatomic, strong, nullable) UIColor *searchBarTextFieldBorderColor; -@property(nonatomic, strong, nullable) UIColor *searchBarBottomBorderColor; -@property(nonatomic, strong, nullable) UIColor *searchBarBarTintColor; -@property(nonatomic, strong, nullable) UIColor *searchBarTintColor; -@property(nonatomic, strong, nullable) UIColor *searchBarTextColor; -@property(nonatomic, strong, nullable) UIColor *searchBarPlaceholderColor; -@property(nonatomic, strong, nullable) UIFont *searchBarFont; -/// 搜索框放大镜icon的图片,大小必须为13x13pt,否则会失真(系统的限制) -@property(nonatomic, strong, nullable) UIImage *searchBarSearchIconImage; -@property(nonatomic, strong, nullable) UIImage *searchBarClearIconImage; -@property(nonatomic, assign) CGFloat searchBarTextFieldCornerRadius; - -#pragma mark - TableView / TableViewCell - -@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; -@property(nonatomic, strong, nullable) UIColor *tableViewSeparatorColor; - -@property(nonatomic, assign) CGFloat tableViewCellNormalHeight; -@property(nonatomic, strong, nullable) UIColor *tableViewCellTitleLabelColor; -@property(nonatomic, strong, nullable) UIColor *tableViewCellDetailLabelColor; -@property(nonatomic, strong, nullable) UIColor *tableViewCellBackgroundColor; -@property(nonatomic, strong, nullable) UIColor *tableViewCellSelectedBackgroundColor; -@property(nonatomic, strong, nullable) UIColor *tableViewCellWarningBackgroundColor; -@property(nonatomic, strong, nullable) UIImage *tableViewCellDisclosureIndicatorImage; -@property(nonatomic, strong, nullable) UIImage *tableViewCellCheckmarkImage; -@property(nonatomic, strong, nullable) UIImage *tableViewCellDetailButtonImage; -@property(nonatomic, assign) CGFloat tableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator; - -@property(nonatomic, strong, nullable) UIColor *tableViewSectionHeaderBackgroundColor; -@property(nonatomic, strong, nullable) UIColor *tableViewSectionFooterBackgroundColor; -@property(nonatomic, strong, nullable) UIFont *tableViewSectionHeaderFont; -@property(nonatomic, strong, nullable) UIFont *tableViewSectionFooterFont; -@property(nonatomic, strong, nullable) UIColor *tableViewSectionHeaderTextColor; -@property(nonatomic, strong, nullable) UIColor *tableViewSectionFooterTextColor; -@property(nonatomic, assign) CGFloat tableViewSectionHeaderHeight; -@property(nonatomic, assign) CGFloat tableViewSectionFooterHeight; -@property(nonatomic, assign) UIEdgeInsets tableViewSectionHeaderContentInset; -@property(nonatomic, assign) UIEdgeInsets tableViewSectionFooterContentInset; - -@property(nonatomic, strong, nullable) UIFont *tableViewGroupedSectionHeaderFont; -@property(nonatomic, strong, nullable) UIFont *tableViewGroupedSectionFooterFont; -@property(nonatomic, strong, nullable) UIColor *tableViewGroupedSectionHeaderTextColor; -@property(nonatomic, strong, nullable) UIColor *tableViewGroupedSectionFooterTextColor; -@property(nonatomic, assign) CGFloat tableViewGroupedSectionHeaderHeight; -@property(nonatomic, assign) CGFloat tableViewGroupedSectionFooterHeight; -@property(nonatomic, assign) UIEdgeInsets tableViewGroupedSectionHeaderContentInset; -@property(nonatomic, assign) UIEdgeInsets tableViewGroupedSectionFooterContentInset; - -#pragma mark - UIWindowLevel - -@property(nonatomic, assign) CGFloat windowLevelQMUIAlertView; -@property(nonatomic, assign) CGFloat windowLevelQMUIImagePreviewView; - -#pragma mark - Others - -@property(nonatomic, assign) UIInterfaceOrientationMask supportedOrientationMask; -@property(nonatomic, assign) BOOL automaticallyRotateDeviceOrientation; -@property(nonatomic, assign) BOOL statusbarStyleLightInitially; -@property(nonatomic, assign) BOOL needsBackBarButtonItemTitle; -@property(nonatomic, assign) BOOL hidesBottomBarWhenPushedInitially; -@property(nonatomic, assign) BOOL navigationBarHiddenInitially; - -NS_ASSUME_NONNULL_END - -/// 单例对象 -+ (instancetype _Nullable )sharedInstance; - -@end diff --git a/QMUI/QMUIKit/UICore/QMUIConfiguration.m b/QMUI/QMUIKit/UICore/QMUIConfiguration.m deleted file mode 100644 index 91f226de..00000000 --- a/QMUI/QMUIKit/UICore/QMUIConfiguration.m +++ /dev/null @@ -1,395 +0,0 @@ -// -// QMUIConfiguration.m -// qmui -// -// Created by QQMail on 15/3/29. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIConfiguration.h" -#import "QMUICore.h" -#import "UIImage+QMUI.h" -#import "QMUIButton.h" -#import "NSString+QMUI.h" -#import "QMUITabBarViewController.h" -#import "QMUINavigationController.h" - -@implementation QMUIConfiguration - -+ (instancetype)sharedInstance { - static dispatch_once_t pred; - static QMUIConfiguration *sharedInstance = nil; - - // 检查是否有在某些类的 +load 方法里调用 QMUICMI,因为在 [QMUIConfiguration init] 方法里会操作到 UI 的东西,例如 [UINavigationBar appearance] xxx 等,这些操作不能太早(+load 里就太早了)执行,否则会 crash,所以加这个检测 -//#ifdef DEBUG -// BOOL shouldCheckCallStack = NO; -// if (shouldCheckCallStack) { -// for (NSString *symbol in [NSThread callStackSymbols]) { -// if ([symbol qmui_includesString:@" load]"]) { -// NSAssert(NO, @"不应该在 + load 方法里调用 %s", __func__); -// return nil; -// } -// } -// } -//#endif - - dispatch_once(&pred, ^{ - sharedInstance = [[QMUIConfiguration alloc] init]; - }); - return sharedInstance; -} - -- (instancetype)init { - self = [super init]; - if (self) { - [self initDefaultConfiguration]; - } - return self; -} - -#pragma mark - 初始化默认值 - -- (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.grayColor = UIColorMake(179, 179, 179); - self.grayDarkenColor = UIColorMake(163, 163, 163); - self.grayLightenColor = UIColorMake(198, 198, 198); - self.redColor = UIColorMake(250, 58, 58); - self.greenColor = UIColorMake(159, 214, 97); - self.blueColor = UIColorMake(49, 189, 243); - self.yellowColor = UIColorMake(255, 207, 71); - - 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); - self.separatorDashedColor = UIColorMake(17, 17, 17); - self.placeholderColor = UIColorMake(196, 200, 208); - - self.testColorRed = UIColorMakeWithRGBA(255, 0, 0, .3); - self.testColorGreen = UIColorMakeWithRGBA(0, 255, 0, .3); - self.testColorBlue = UIColorMakeWithRGBA(0, 0, 255, .3); - - #pragma mark - UIControl - - self.controlHighlightedAlpha = 0.5f; - self.controlDisabledAlpha = 0.5f; - - #pragma mark - UIButton - - self.buttonHighlightedAlpha = self.controlHighlightedAlpha; - 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 = self.blackColor; - self.navBarTitleFont = nil; - self.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; - 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 - - 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.tableViewBackgroundColor = nil; - self.tableViewGroupedBackgroundColor = nil; - self.tableSectionIndexColor = nil; - self.tableSectionIndexBackgroundColor = nil; - self.tableSectionIndexTrackingBackgroundColor = nil; - self.tableViewSeparatorColor = self.separatorColor; - - self.tableViewCellNormalHeight = 44; - 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); - self.tableViewSectionFooterBackgroundColor = UIColorMake(244, 244, 244); - self.tableViewSectionHeaderFont = UIFontBoldMake(12); - self.tableViewSectionFooterFont = UIFontBoldMake(12); - self.tableViewSectionHeaderTextColor = self.grayDarkenColor; - self.tableViewSectionFooterTextColor = self.grayColor; - self.tableViewSectionHeaderHeight = 20; - self.tableViewSectionFooterHeight = 0; - self.tableViewSectionHeaderContentInset = UIEdgeInsetsMake(4, 15, 4, 15); - self.tableViewSectionFooterContentInset = UIEdgeInsetsMake(4, 15, 4, 15); - - self.tableViewGroupedSectionHeaderFont = UIFontMake(12); - self.tableViewGroupedSectionFooterFont = UIFontMake(12); - self.tableViewGroupedSectionHeaderTextColor = self.grayDarkenColor; - self.tableViewGroupedSectionFooterTextColor = self.grayColor; - self.tableViewGroupedSectionHeaderHeight = 15; - self.tableViewGroupedSectionFooterHeight = 1; - self.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, 15, 8, 15); - self.tableViewGroupedSectionFooterContentInset = UIEdgeInsetsMake(8, 15, 2, 15); - - #pragma mark - UIWindowLevel - self.windowLevelQMUIAlertView = UIWindowLevelAlert - 4.0; - self.windowLevelQMUIImagePreviewView = UIWindowLevelStatusBar + 1; - - #pragma mark - Others - - self.supportedOrientationMask = UIInterfaceOrientationMaskPortrait; - self.automaticallyRotateDeviceOrientation = NO; - self.statusbarStyleLightInitially = NO; - self.needsBackBarButtonItemTitle = NO; - self.hidesBottomBarWhenPushedInitially = NO; - self.navigationBarHiddenInitially = NO; -} - -- (void)setNavBarTintColor:(UIColor *)navBarTintColor { - _navBarTintColor = navBarTintColor; - [QMUIHelper visibleViewController].navigationController.navigationBar.tintColor = _navBarTintColor; -} - -- (void)setNavBarBarTintColor:(UIColor *)navBarBarTintColor { - _navBarBarTintColor = navBarBarTintColor; - [UINavigationBar appearance].barTintColor = _navBarBarTintColor; - [QMUIHelper visibleViewController].navigationController.navigationBar.barTintColor = _navBarBarTintColor; -} - -- (void)setNavBarShadowImage:(UIImage *)navBarShadowImage { - _navBarShadowImage = navBarShadowImage; - [UINavigationBar appearance].shadowImage = _navBarShadowImage; - [QMUIHelper visibleViewController].navigationController.navigationBar.shadowImage = _navBarShadowImage; -} - -- (void)setNavBarBackgroundImage:(UIImage *)navBarBackgroundImage { - _navBarBackgroundImage = navBarBackgroundImage; - [[UINavigationBar appearance] setBackgroundImage:_navBarBackgroundImage forBarMetrics:UIBarMetricsDefault]; - [[QMUIHelper visibleViewController].navigationController.navigationBar setBackgroundImage:_navBarBackgroundImage forBarMetrics:UIBarMetricsDefault]; -} - -- (void)setNavBarTitleFont:(UIFont *)navBarTitleFont { - _navBarTitleFont = navBarTitleFont; - if (self.navBarTitleFont || self.navBarTitleColor) { - NSMutableDictionary *titleTextAttributes = [[NSMutableDictionary alloc] init]; - if (self.navBarTitleFont) { - [titleTextAttributes setValue:self.navBarTitleFont forKey:NSFontAttributeName]; - } - if (self.navBarTitleColor) { - [titleTextAttributes setValue:self.navBarTitleColor forKey:NSForegroundColorAttributeName]; - } - [UINavigationBar appearance].titleTextAttributes = titleTextAttributes; - [QMUIHelper visibleViewController].navigationController.navigationBar.titleTextAttributes = titleTextAttributes; - } -} - -- (void)setNavBarTitleColor:(UIColor *)navBarTitleColor { - _navBarTitleColor = navBarTitleColor; - if (self.navBarTitleFont || self.navBarTitleColor) { - NSMutableDictionary *titleTextAttributes = [[NSMutableDictionary alloc] init]; - if (self.navBarTitleFont) { - [titleTextAttributes setValue:self.navBarTitleFont forKey:NSFontAttributeName]; - } - if (self.navBarTitleColor) { - [titleTextAttributes setValue:self.navBarTitleColor forKey:NSForegroundColorAttributeName]; - } - [UINavigationBar appearance].titleTextAttributes = titleTextAttributes; - [QMUIHelper visibleViewController].navigationController.navigationBar.titleTextAttributes = titleTextAttributes; - } -} - -- (void)setNavBarBackIndicatorImage:(UIImage *)navBarBackIndicatorImage { - _navBarBackIndicatorImage = navBarBackIndicatorImage; - - if (_navBarBackIndicatorImage) { - UINavigationBar *navBarAppearance = [UINavigationBar appearance]; - UINavigationBar *navigationBar = [QMUIHelper visibleViewController].navigationController.navigationBar; - - // 返回按钮的图片frame是和系统默认的返回图片的大小一致的(13, 21),所以用自定义返回箭头时要保证图片大小与系统的箭头大小一样,否则无法对齐 - CGSize systemBackIndicatorImageSize = CGSizeMake(13, 21); // 在iOS 8-11 上实际测量得到 - CGSize customBackIndicatorImageSize = _navBarBackIndicatorImage.size; - if (!CGSizeEqualToSize(customBackIndicatorImageSize, systemBackIndicatorImageSize)) { - CGFloat imageExtensionVerticalFloat = CGFloatGetCenter(systemBackIndicatorImageSize.height, customBackIndicatorImageSize.height); - _navBarBackIndicatorImage = [_navBarBackIndicatorImage qmui_imageWithSpacingExtensionInsets:UIEdgeInsetsMake(imageExtensionVerticalFloat, - 0, - imageExtensionVerticalFloat, - systemBackIndicatorImageSize.width - customBackIndicatorImageSize.width)]; - } - - navBarAppearance.backIndicatorImage = _navBarBackIndicatorImage; - navBarAppearance.backIndicatorTransitionMaskImage = navBarAppearance.backIndicatorImage; - navigationBar.backIndicatorImage = _navBarBackIndicatorImage; - navigationBar.backIndicatorTransitionMaskImage = navigationBar.backIndicatorImage; - } -} - -- (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]; - } -} - -- (void)setToolBarTintColor:(UIColor *)toolBarTintColor { - _toolBarTintColor = toolBarTintColor; - [QMUIHelper visibleViewController].navigationController.toolbar.tintColor = _toolBarTintColor; -} - -- (void)setToolBarBarTintColor:(UIColor *)toolBarBarTintColor { - _toolBarBarTintColor = toolBarBarTintColor; - [UIToolbar appearance].barTintColor = _toolBarBarTintColor; - [QMUIHelper visibleViewController].navigationController.toolbar.barTintColor = _toolBarBarTintColor; -} - -- (void)setToolBarBackgroundImage:(UIImage *)toolBarBackgroundImage { - _toolBarBackgroundImage = toolBarBackgroundImage; - [[UIToolbar appearance] setBackgroundImage:_toolBarBackgroundImage forToolbarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; - [[QMUIHelper visibleViewController].navigationController.toolbar setBackgroundImage:_toolBarBackgroundImage forToolbarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; -} - -- (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]; - } -} - -- (void)setTabBarTintColor:(UIColor *)tabBarTintColor { - _tabBarTintColor = tabBarTintColor; - [QMUIHelper visibleViewController].tabBarController.tabBar.tintColor = _tabBarTintColor; -} - -- (void)setTabBarBarTintColor:(UIColor *)tabBarBarTintColor { - _tabBarBarTintColor = tabBarBarTintColor; - [UITabBar appearance].barTintColor = _tabBarBarTintColor; - [QMUIHelper visibleViewController].tabBarController.tabBar.barTintColor = _tabBarBarTintColor; -} - -- (void)setTabBarBackgroundImage:(UIImage *)tabBarBackgroundImage { - _tabBarBackgroundImage = tabBarBackgroundImage; - [UITabBar appearance].backgroundImage = _tabBarBackgroundImage; - [QMUIHelper visibleViewController].tabBarController.tabBar.backgroundImage = _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; - } -} - -- (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 { - _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)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]; - }]; - } -} - -@end diff --git a/QMUI/QMUIKit/UICore/QMUIConfigurationMacros.h b/QMUI/QMUIKit/UICore/QMUIConfigurationMacros.h deleted file mode 100644 index b1ffe55e..00000000 --- a/QMUI/QMUIKit/UICore/QMUIConfigurationMacros.h +++ /dev/null @@ -1,197 +0,0 @@ -// -// QMUIConfigurationMacros.h -// qmui -// -// Created by QQMail on 14-7-2. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUIConfiguration.h" - - -/** - * 提供一系列方便书写的宏,以便在代码里读取配置表的各种属性。 - * @warning 请不要在 + load 方法里调用 QMUIConfigurationTemplate 或 QMUIConfigurationMacros 提供的宏,那个时机太早,可能导致 crash - * @waining 维护时,如果需要增加一个宏,则需要定义一个新的 QMUIConfiguration 属性。 - */ - - -// 单例的宏 - -#define QMUICMI [QMUIConfiguration sharedInstance] - - -#pragma mark - Global Color - -// 基础颜色 -#define UIColorClear [QMUICMI clearColor] -#define UIColorWhite [QMUICMI whiteColor] -#define UIColorBlack [QMUICMI blackColor] -#define UIColorGray [QMUICMI grayColor] -#define UIColorGrayDarken [QMUICMI grayDarkenColor] -#define UIColorGrayLighten [QMUICMI grayLightenColor] -#define UIColorRed [QMUICMI redColor] -#define UIColorGreen [QMUICMI greenColor] -#define UIColorBlue [QMUICMI blueColor] -#define UIColorYellow [QMUICMI yellowColor] - -// 功能颜色 -#define UIColorLink [QMUICMI linkColor] // 全局统一文字链接颜色 -#define UIColorDisabled [QMUICMI disabledColor] // 全局统一文字disabled颜色 -#define UIColorForBackground [QMUICMI backgroundColor] // 全局统一的背景色 -#define UIColorMask [QMUICMI maskDarkColor] // 全局统一的mask背景色 -#define UIColorMaskWhite [QMUICMI maskLightColor] // 全局统一的mask背景色,白色 -#define UIColorSeparator [QMUICMI separatorColor] // 全局分隔线颜色 -#define UIColorSeparatorDashed [QMUICMI separatorDashedColor] // 全局分隔线颜色(虚线) -#define UIColorPlaceholder [QMUICMI placeholderColor] // 全局的输入框的placeholder颜色 - -// 测试用的颜色 -#define UIColorTestRed [QMUICMI testColorRed] -#define UIColorTestGreen [QMUICMI testColorGreen] -#define UIColorTestBlue [QMUICMI testColorBlue] - -// 可操作的控件 -#pragma mark - UIControl - -#define UIControlHighlightedAlpha [QMUICMI controlHighlightedAlpha] // 一般control的Highlighted透明值 -#define UIControlDisabledAlpha [QMUICMI controlDisabledAlpha] // 一般control的Disable透明值 - -// 按钮 -#pragma mark - UIButton -#define ButtonHighlightedAlpha [QMUICMI buttonHighlightedAlpha] // 按钮Highlighted状态的透明度 -#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 - NavigationBar - -#define NavBarHighlightedAlpha [QMUICMI navBarHighlightedAlpha] -#define NavBarDisabledAlpha [QMUICMI navBarDisabledAlpha] -#define NavBarButtonFont [QMUICMI navBarButtonFont] -#define NavBarButtonFontBold [QMUICMI navBarButtonFontBold] -#define NavBarBackgroundImage [QMUICMI navBarBackgroundImage] -#define NavBarShadowImage [QMUICMI navBarShadowImage] -#define NavBarBarTintColor [QMUICMI navBarBarTintColor] -#define NavBarTintColor [QMUICMI navBarTintColor] -#define NavBarTitleColor [QMUICMI navBarTitleColor] -#define NavBarTitleFont [QMUICMI navBarTitleFont] -#define NavBarBarBackButtonTitlePositionAdjustment [QMUICMI navBarBackButtonTitlePositionAdjustment] -#define NavBarBackIndicatorImage [QMUICMI navBarBackIndicatorImage] // 自定义的返回按钮,尺寸建议与系统的返回按钮尺寸一致(iOS8下实测系统大小是(13, 21)),可提高性能 -#define NavBarCloseButtonImage [QMUICMI navBarCloseButtonImage] - -#define NavBarLoadingMarginRight [QMUICMI navBarLoadingMarginRight] // titleView里左边的loading的右边距 -#define NavBarAccessoryViewMarginLeft [QMUICMI navBarAccessoryViewMarginLeft] // titleView里的accessoryView的左边距 -#define NavBarActivityIndicatorViewStyle [QMUICMI navBarActivityIndicatorViewStyle] // titleView loading 的style -#define NavBarAccessoryViewTypeDisclosureIndicatorImage [QMUICMI navBarAccessoryViewTypeDisclosureIndicatorImage] // titleView上倒三角的默认图片 - - -#pragma mark - TabBar - -#define TabBarBackgroundImage [QMUICMI tabBarBackgroundImage] -#define TabBarBarTintColor [QMUICMI tabBarBarTintColor] -#define TabBarShadowImageColor [QMUICMI tabBarShadowImageColor] -#define TabBarTintColor [QMUICMI tabBarTintColor] -#define TabBarItemTitleColor [QMUICMI tabBarItemTitleColor] -#define TabBarItemTitleColorSelected [QMUICMI tabBarItemTitleColorSelected] -#define TabBarItemTitleFont [QMUICMI tabBarItemTitleFont] - - -#pragma mark - Toolbar - -#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 ToolBarBarTintColor [QMUICMI toolBarBarTintColor] -#define ToolBarShadowImageColor [QMUICMI toolBarShadowImageColor] -#define ToolBarButtonFont [QMUICMI toolBarButtonFont] - - -#pragma mark - SearchBar - -#define SearchBarTextFieldBackground [QMUICMI searchBarTextFieldBackground] -#define SearchBarTextFieldBorderColor [QMUICMI searchBarTextFieldBorderColor] -#define SearchBarBottomBorderColor [QMUICMI searchBarBottomBorderColor] -#define SearchBarBarTintColor [QMUICMI searchBarBarTintColor] -#define SearchBarTintColor [QMUICMI searchBarTintColor] -#define SearchBarTextColor [QMUICMI searchBarTextColor] -#define SearchBarPlaceholderColor [QMUICMI searchBarPlaceholderColor] -#define SearchBarFont [QMUICMI searchBarFont] -#define SearchBarSearchIconImage [QMUICMI searchBarSearchIconImage] -#define SearchBarClearIconImage [QMUICMI searchBarClearIconImage] -#define SearchBarTextFieldCornerRadius [QMUICMI searchBarTextFieldCornerRadius] - - -#pragma mark - TableView / TableViewCell - -#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 TableViewSeparatorColor [QMUICMI tableViewSeparatorColor] // 列表分隔线颜色 -#define TableViewCellBackgroundColor [QMUICMI tableViewCellBackgroundColor] // 列表 cell 的背景色 -#define TableViewCellSelectedBackgroundColor [QMUICMI tableViewCellSelectedBackgroundColor] // 列表 cell 按下时的背景色 -#define TableViewCellWarningBackgroundColor [QMUICMI tableViewCellWarningBackgroundColor] // 列表 cell 在未读状态下的背景色 -#define TableViewCellNormalHeight [QMUICMI tableViewCellNormalHeight] // 默认 cell 的高度 - -#define TableViewCellDisclosureIndicatorImage [QMUICMI tableViewCellDisclosureIndicatorImage] // 列表 cell 右边的箭头图片 -#define TableViewCellCheckmarkImage [QMUICMI tableViewCellCheckmarkImage] // 列表 cell 右边的打钩checkmark -#define TableViewCellDetailButtonImage [QMUICMI tableViewCellDetailButtonImage] // 列表 cell 右边的 i 按钮 -#define TableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator [QMUICMI tableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator] // 列表 cell 右边的 i 按钮和向右箭头之间的间距(仅当两者都使用了自定义图片并且同时显示时才生效) - -#define TableViewSectionHeaderBackgroundColor [QMUICMI tableViewSectionHeaderBackgroundColor] -#define TableViewSectionFooterBackgroundColor [QMUICMI tableViewSectionFooterBackgroundColor] -#define TableViewSectionHeaderFont [QMUICMI tableViewSectionHeaderFont] -#define TableViewSectionFooterFont [QMUICMI tableViewSectionFooterFont] -#define TableViewSectionHeaderTextColor [QMUICMI tableViewSectionHeaderTextColor] -#define TableViewSectionFooterTextColor [QMUICMI tableViewSectionFooterTextColor] -#define TableViewSectionHeaderHeight [QMUICMI tableViewSectionHeaderHeight] // 列表sectionheader的高度 -#define TableViewSectionFooterHeight [QMUICMI tableViewSectionFooterHeight] // 列表sectionheader的高度 -#define TableViewSectionHeaderContentInset [QMUICMI tableViewSectionHeaderContentInset] -#define TableViewSectionFooterContentInset [QMUICMI tableViewSectionFooterContentInset] - -#define TableViewGroupedSectionHeaderFont [QMUICMI tableViewGroupedSectionHeaderFont] -#define TableViewGroupedSectionFooterFont [QMUICMI tableViewGroupedSectionFooterFont] -#define TableViewGroupedSectionHeaderTextColor [QMUICMI tableViewGroupedSectionHeaderTextColor] -#define TableViewGroupedSectionFooterTextColor [QMUICMI tableViewGroupedSectionFooterTextColor] -#define TableViewGroupedSectionHeaderHeight [QMUICMI tableViewGroupedSectionHeaderHeight] -#define TableViewGroupedSectionFooterHeight [QMUICMI tableViewGroupedSectionFooterHeight] -#define TableViewGroupedSectionHeaderContentInset [QMUICMI tableViewGroupedSectionHeaderContentInset] -#define TableViewGroupedSectionFooterContentInset [QMUICMI tableViewGroupedSectionFooterContentInset] - -#define TableViewCellTitleLabelColor [QMUICMI tableViewCellTitleLabelColor] //cell的title颜色 -#define TableViewCellDetailLabelColor [QMUICMI tableViewCellDetailLabelColor] //cell的detailTitle颜色 - -#pragma mark - UIWindowLevel -#define UIWindowLevelQMUIAlertView [QMUICMI windowLevelQMUIAlertView] -#define UIWindowLevelQMUIImagePreviewView [QMUICMI windowLevelQMUIImagePreviewView] - -#pragma mark - Others - -#define SupportedOrientationMask [QMUICMI supportedOrientationMask] // 默认支持的横竖屏方向 -#define AutomaticallyRotateDeviceOrientation [QMUICMI automaticallyRotateDeviceOrientation] // 是否在界面切换或 viewController.supportedOrientationMask 发生变化时自动旋转屏幕,默认为 NO -#define StatusbarStyleLightInitially [QMUICMI statusbarStyleLightInitially] // 默认的状态栏内容是否使用白色,默认为NO,也即黑色 -#define NeedsBackBarButtonItemTitle [QMUICMI needsBackBarButtonItemTitle] // 全局是否需要返回按钮的title,不需要则只显示一个返回image -#define HidesBottomBarWhenPushedInitially [QMUICMI hidesBottomBarWhenPushedInitially] // QMUICommonViewController.hidesBottomBarWhenPushed 的初始值,默认为 NO,以保持与系统默认值一致,但通常建议改为 YES,因为一般只有 tabBar 首页那几个界面要求为 NO -#define NavigationBarHiddenInitially [QMUICMI navigationBarHiddenInitially] // NavigationBarHiddenInitially : preferredNavigationBarHidden 的初始值,默认为NO - diff --git a/QMUI/QMUIKit/UICore/QMUICore.h b/QMUI/QMUIKit/UICore/QMUICore.h deleted file mode 100644 index 1188db3b..00000000 --- a/QMUI/QMUIKit/UICore/QMUICore.h +++ /dev/null @@ -1,12 +0,0 @@ -// -// QMUICore.h -// qmui -// -// Created by MoLice on 2017/5/17. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#import "QMUIHelper.h" -#import "QMUICommonDefines.h" -#import "QMUIConfiguration.h" -#import "QMUIConfigurationMacros.h" diff --git a/QMUI/QMUIKit/UICore/QMUIHelper.h b/QMUI/QMUIKit/UICore/QMUIHelper.h deleted file mode 100644 index 8b2d7c85..00000000 --- a/QMUI/QMUIKit/UICore/QMUIHelper.h +++ /dev/null @@ -1,222 +0,0 @@ -// -// QMUIHelper.h -// qmui -// -// Created by QQMail on 14/10/25. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import -#import - -@protocol QMUIHelperDelegate - -@required -- (void)QMUIHelperPrintLog:(nonnull NSString *)log; - -@end - -@interface QMUIHelper : NSObject - -+ (instancetype _Nonnull)sharedInstance; - -@property(nullable, nonatomic, weak) id helperDelegate; - -@end - - -extern NSString *const _Nonnull QMUIResourcesMainBundleName; -extern NSString *const _Nonnull QMUIResourcesQQEmotionBundleName; - -@interface QMUIHelper (Bundle) - -// QMUI专属 -+ (nullable NSBundle *)resourcesBundle; -+ (nullable UIImage *)imageWithName:(nullable NSString *)name; - -+ (nullable NSBundle *)resourcesBundleWithName:(nullable NSString *)bundleName; -+ (nullable UIImage *)imageInBundle:(nullable NSBundle *)bundle withName:(nullable NSString *)name; -@end - - -@interface QMUIHelper (DynamicType) - -/// 返回当前contentSize的level,这个值可以在设置里面的“字体大小”查看,辅助功能里面有个“更大字体”可以设置更大的字体,不过这里我们这个接口将更大字体都做了统一,都返回“字体大小”里面最大值。 -+ (nonnull NSNumber *)preferredContentSizeLevel; - -/// 设置当前cell的高度,heights是有七个数值的数组,对于不支持的iOS版本,则选择中间的值返回。 -+ (CGFloat)heightForDynamicTypeCell:(nonnull NSArray *)heights; -@end - - -@interface QMUIHelper (Keyboard) - -/** - * 判断当前App里的键盘是否升起,默认为NO - */ -+ (BOOL)isKeyboardVisible; - -/** - * 记录上一次键盘显示时的高度(基于整个 App 所在的 window 的坐标系),注意使用前用 `isKeyboardVisible` 判断键盘是否显示,因为即便是键盘被隐藏的情况下,调用 `lastKeyboardHeightInApplicationWindowWhenVisible` 也会得到高度值。 - */ -+ (CGFloat)lastKeyboardHeightInApplicationWindowWhenVisible; - -/** - * 获取当前键盘frame相关 - * @warning 注意iOS8以下的系统在横屏时得到的rect,宽度和高度相反了,所以不建议直接通过这个方法获取高度,而是使用keyboardHeightWithNotification:inView:,因为在后者的实现里会将键盘的rect转换坐标系,转换过程就会处理横竖屏旋转问题。 - */ -+ (CGRect)keyboardRectWithNotification:(nullable NSNotification *)notification; - -/// 获取当前键盘的高度,注意高度可能为0(例如第三方键盘会发出两次notification,其中第一次的高度就为0) -+ (CGFloat)keyboardHeightWithNotification:(nullable NSNotification *)notification; - -/** - * 获取当前键盘在屏幕上的可见高度,注意外接键盘(iPad那种)时,[QMUIHelper keyboardRectWithNotification]得到的键盘rect里有一部分是超出屏幕,不可见的,如果直接拿rect的高度来计算就会与意图相悖。 - * @param notification 接收到的键盘事件的UINotification对象 - * @param view 要得到的键盘高度是相对于哪个View的键盘高度,若为nil,则等同于调用[QMUIHelper keyboardHeightWithNotification:] - * @warning 如果view.window为空(当前View尚不可见),则会使用App默认的UIWindow来做坐标转换,可能会导致一些计算错误 - * @return 键盘在view里的可视高度 - */ -+ (CGFloat)keyboardHeightWithNotification:(nullable NSNotification *)notification inView:(nullable UIView *)view; - -/// 获取键盘显示/隐藏的动画时长,注意返回值可能为0 -+ (NSTimeInterval)keyboardAnimationDurationWithNotification:(nullable NSNotification *)notification; - -/// 获取键盘显示/隐藏的动画时间函数 -+ (UIViewAnimationCurve)keyboardAnimationCurveWithNotification:(nullable NSNotification *)notification; - -/// 获取键盘显示/隐藏的动画时间函数 -+ (UIViewAnimationOptions)keyboardAnimationOptionsWithNotification:(nullable NSNotification *)notification; -@end - - -@interface QMUIHelper (AudioSession) - -/** - * 听筒和扬声器的切换 - * - * @param speaker 是否转为扬声器,NO则听筒 - * @param temporary 决定使用kAudioSessionProperty_OverrideAudioRoute还是kAudioSessionProperty_OverrideCategoryDefaultToSpeaker,两者的区别请查看本组的博客文章:http://km.oa.com/group/gyui/articles/show/235957 - */ -+ (void)redirectAudioRouteWithSpeaker:(BOOL)speaker temporary:(BOOL)temporary; - -/** - * 设置category - * - * @param category 使用iOS7的category,iOS6的会自动适配 - */ -+ (void)setAudioSessionCategory:(nullable NSString *)category; -@end - -@interface QMUIHelper (UIGraphic) - -/// 获取一像素的大小 -+ (CGFloat)pixelOne; - -/// 判断size是否超出范围 -+ (void)inspectContextSize:(CGSize)size; - -/// context是否合法 -+ (void)inspectContextIfInvalidatedInDebugMode:(CGContextRef _Nonnull)context; -+ (BOOL)inspectContextIfInvalidatedInReleaseMode:(CGContextRef _Nonnull)context; -@end - - -@interface QMUIHelper (Device) - -+ (BOOL)isIPad; -+ (BOOL)isIPadPro; -+ (BOOL)isIPod; -+ (BOOL)isIPhone; -+ (BOOL)isSimulator; - -+ (BOOL)is55InchScreen; -+ (BOOL)is47InchScreen; -+ (BOOL)is40InchScreen; -+ (BOOL)is35InchScreen; - -+ (CGSize)screenSizeFor55Inch; -+ (CGSize)screenSizeFor47Inch; -+ (CGSize)screenSizeFor40Inch; -+ (CGSize)screenSizeFor35Inch; - -/// 判断当前设备是否高性能设备,只会判断一次,以后都直接读取结果,所以没有性能问题 -+ (BOOL)isHighPerformanceDevice; -@end - -@interface QMUIHelper (Orientation) - -/** - * 旋转当前设备的方向到指定方向,一般用于 [UIViewController supportedInterfaceOrientations] 发生变化时主动触发界面方向的刷新 - * @return 是否真正旋转了方向,YES 表示参数的方向和目前设备方向不一致,NO 表示一致也即不会旋转 - * @see [QMUIConfiguration automaticallyRotateDeviceOrientation] - */ -+ (BOOL)rotateToDeviceOrientation:(UIDeviceOrientation)orientation; - -/** - * 记录手动旋转方向前的设备方向,当值不为 UIDeviceOrientationUnknown 时表示设备方向有经过了手动调整。默认值为 UIDeviceOrientationUnknown。 - * @see [QMUIHelper rotateToDeviceOrientation] - */ -@property(nonatomic, assign) UIDeviceOrientation orientationBeforeChangingByHelper; - -/// 根据指定的旋转方向计算出对应的旋转角度 -+ (CGFloat)angleForTransformWithInterfaceOrientation:(UIInterfaceOrientation)orientation; - -/// 根据当前设备的旋转方向计算出对应的CGAffineTransform -+ (CGAffineTransform)transformForCurrentInterfaceOrientation; - -/// 根据指定的旋转方向计算出对应的CGAffineTransform -+ (CGAffineTransform)transformWithInterfaceOrientation:(UIInterfaceOrientation)orientation; -@end - -@interface QMUIHelper (ViewController) - -/** - * 获取当前应用里最顶层的可见viewController - * @warning 注意返回值可能为nil,要做好保护 - */ -+ (nullable UIViewController *)visibleViewController; - -@end - -@interface QMUIHelper (UIApplication) - -/** - * 更改状态栏内容颜色为深色 - * - * @warning 需在Info.plist文件内设置字段“View controller-based status bar appearance”的值为“NO”才能生效 - */ -+ (void)renderStatusBarStyleDark; - -/** - * 更改状态栏内容颜色为浅色 - * - * @warning 需在Info.plist文件内设置字段“View controller-based status bar appearance”的值为“NO”才能生效 - */ -+ (void)renderStatusBarStyleLight; - -/** - * 把App的主要window置灰,用于浮层弹出时,请注意要在适当时机调用`resetDimmedApplicationWindow`恢复到正常状态 - */ -+ (void)dimmedApplicationWindow; - -/** - * 恢复对App的主要window的置灰操作,与`dimmedApplicationWindow`成对调用 - */ -+ (void)resetDimmedApplicationWindow; - -@end - -extern NSString * __nonnull const QMUISpringAnimationKey; - -@interface QMUIHelper (Animation) - -+ (void)actionSpringAnimationForView:(nonnull UIView *)view; - -@end - -@interface QMUIHelper (Log) - -- (void)printLogWithCalledFunction:(nonnull const char *)func log:(nonnull NSString *)log, ... NS_FORMAT_FUNCTION(2,3); - -@end diff --git a/QMUI/QMUIKit/UICore/QMUIHelper.m b/QMUI/QMUIKit/UICore/QMUIHelper.m deleted file mode 100644 index 93c511ec..00000000 --- a/QMUI/QMUIKit/UICore/QMUIHelper.m +++ /dev/null @@ -1,533 +0,0 @@ -// -// QMUIHelper.m -// qmui -// -// Created by QQMail on 14/10/25. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUIHelper.h" -#import "QMUICore.h" -#import "UIViewController+QMUI.h" -#import -#import "NSString+QMUI.h" - - -NSString *const QMUIResourcesMainBundleName = @"QMUIResources.bundle"; -NSString *const QMUIResourcesQQEmotionBundleName = @"QMUI_QQEmotion.bundle"; - -@implementation QMUIHelper (Bundle) - -+ (NSBundle *)resourcesBundle { - return [QMUIHelper resourcesBundleWithName:QMUIResourcesMainBundleName]; -} - -+ (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; -} - -+ (UIImage *)imageWithName:(NSString *)name { - NSBundle *bundle = [QMUIHelper resourcesBundle]; - return [QMUIHelper imageInBundle:bundle withName:name]; -} - -+ (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; -} - -+ (NSDictionary *)parseBundleName:(NSString *)bundleName { - NSArray *bundleData = [bundleName componentsSeparatedByString:@"."]; - if (bundleData.count == 2) { - return @{@"name":bundleData[0], @"type":bundleData[1]}; - } - return nil; -} - -@end - - -@implementation QMUIHelper (DynamicType) - -+ (NSNumber *)preferredContentSizeLevel { - NSNumber *index = nil; - if ([UIApplication instancesRespondToSelector:@selector(preferredContentSizeCategory)]) { - NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; - if ([contentSizeCategory isEqualToString:UIContentSizeCategoryExtraSmall]) { - index = [NSNumber numberWithInt:0]; - } else if ([contentSizeCategory isEqualToString:UIContentSizeCategorySmall]) { - index = [NSNumber numberWithInt:1]; - } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryMedium]) { - index = [NSNumber numberWithInt:2]; - } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryLarge]) { - index = [NSNumber numberWithInt:3]; - } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryExtraLarge]) { - index = [NSNumber numberWithInt:4]; - } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryExtraExtraLarge]) { - index = [NSNumber numberWithInt:5]; - } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) { - index = [NSNumber numberWithInt:6]; - } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityMedium]) { - index = [NSNumber numberWithInt:6]; - } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityLarge]) { - index = [NSNumber numberWithInt:6]; - } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) { - index = [NSNumber numberWithInt:6]; - } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) { - index = [NSNumber numberWithInt:6]; - } else if ([contentSizeCategory isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) { - index = [NSNumber numberWithInt:6]; - } else{ - index = [NSNumber numberWithInt:6]; - } - } else { - index = [NSNumber numberWithInt:3]; - } - - return index; -} - -+ (CGFloat)heightForDynamicTypeCell:(NSArray *)heights { - NSNumber *index = [QMUIHelper preferredContentSizeLevel]; - return [((NSNumber *)[heights objectAtIndex:[index intValue]]) floatValue]; -} -@end - -@implementation QMUIHelper (Keyboard) - -- (void)handleKeyboardWillShow:(NSNotification *)notification { - self.keyboardVisible = YES; - self.lastKeyboardHeight = [QMUIHelper keyboardHeightWithNotification:notification]; -} - -- (void)handleKeyboardWillHide:(NSNotification *)notification { - self.keyboardVisible = NO; -} - -static char kAssociatedObjectKey_KeyboardVisible; -- (void)setKeyboardVisible:(BOOL)argv { - objc_setAssociatedObject(self, &kAssociatedObjectKey_KeyboardVisible, @(argv), OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (BOOL)isKeyboardVisible { - return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_KeyboardVisible)) boolValue]; -} - -+ (BOOL)isKeyboardVisible { - BOOL visible = [[QMUIHelper sharedInstance] isKeyboardVisible]; - return visible; -} - -static char kAssociatedObjectKey_LastKeyboardHeight; -- (void)setLastKeyboardHeight:(CGFloat)argv { - objc_setAssociatedObject(self, &kAssociatedObjectKey_LastKeyboardHeight, @(argv), OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (CGFloat)lastKeyboardHeight { - return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_LastKeyboardHeight)) floatValue]; -} - -+ (CGFloat)lastKeyboardHeightInApplicationWindowWhenVisible { - return [[QMUIHelper sharedInstance] lastKeyboardHeight]; -} - -+ (CGRect)keyboardRectWithNotification:(NSNotification *)notification { - NSDictionary *userInfo = [notification userInfo]; - CGRect keyboardRect = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; - // 注意iOS8以下的系统在横屏时得到的rect,宽度和高度相反了,所以不建议直接通过这个方法获取高度,而是使用keyboardHeightWithNotification:inView:,因为在后者的实现里会将键盘的rect转换坐标系,转换过程就会处理横竖屏旋转问题。 - return keyboardRect; -} - -+ (CGFloat)keyboardHeightWithNotification:(NSNotification *)notification { - return [QMUIHelper keyboardHeightWithNotification:notification inView:nil]; -} - -+ (CGFloat)keyboardHeightWithNotification:(nullable NSNotification *)notification inView:(nullable UIView *)view { - CGRect keyboardRect = [self keyboardRectWithNotification:notification]; - if (!view) { - return CGRectGetHeight(keyboardRect); - } - CGRect keyboardRectInView = [view convertRect:keyboardRect fromView:view.window]; - CGRect keyboardVisibleRectInView = CGRectIntersection(view.bounds, keyboardRectInView); - CGFloat resultHeight = CGRectIsNull(keyboardVisibleRectInView) ? 0.0f : CGRectGetHeight(keyboardVisibleRectInView); - return resultHeight; -} - -+ (NSTimeInterval)keyboardAnimationDurationWithNotification:(NSNotification *)notification { - NSDictionary *userInfo = [notification userInfo]; - NSTimeInterval animationDuration = [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; - return animationDuration; -} - -+ (UIViewAnimationCurve)keyboardAnimationCurveWithNotification:(NSNotification *)notification { - NSDictionary *userInfo = [notification userInfo]; - UIViewAnimationCurve curve = (UIViewAnimationCurve)[[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue]; - return curve; -} - -+ (UIViewAnimationOptions)keyboardAnimationOptionsWithNotification:(NSNotification *)notification { - UIViewAnimationOptions options = [QMUIHelper keyboardAnimationCurveWithNotification:notification]<<16; - return options; -} - -@end - - -@implementation QMUIHelper (AudioSession) - -+ (void)redirectAudioRouteWithSpeaker:(BOOL)speaker temporary:(BOOL)temporary { - if (![[AVAudioSession sharedInstance].category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) { - return; - } - if (temporary) { - [[AVAudioSession sharedInstance] overrideOutputAudioPort:speaker ? AVAudioSessionPortOverrideSpeaker : AVAudioSessionPortOverrideNone error:nil]; - } else { - [[AVAudioSession sharedInstance] setCategory:[AVAudioSession sharedInstance].category withOptions:speaker ? AVAudioSessionCategoryOptionDefaultToSpeaker : 0 error:nil]; - } -} - -+ (void)setAudioSessionCategory:(nullable NSString *)category { - - // 如果不属于系统category,返回 - if (category != AVAudioSessionCategoryAmbient && - category != AVAudioSessionCategorySoloAmbient && - category != AVAudioSessionCategoryPlayback && - category != AVAudioSessionCategoryRecord && - category != AVAudioSessionCategoryPlayAndRecord && - category != AVAudioSessionCategoryAudioProcessing) - { - return; - } - - [[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 - - -@implementation QMUIHelper (UIGraphic) - -static CGFloat pixelOne = -1.0f; -+ (CGFloat)pixelOne { - if (pixelOne < 0) { - pixelOne = 1 / [[UIScreen mainScreen] scale]; - } - return pixelOne; -} - -+ (void)inspectContextSize:(CGSize)size { - if (size.width < 0 || size.height < 0) { - NSAssert(NO, @"QMUI CGPostError, %@:%d %s, 非法的size:%@\n%@", [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __PRETTY_FUNCTION__, NSStringFromCGSize(size), [NSThread callStackSymbols]); - } -} - -+ (void)inspectContextIfInvalidatedInDebugMode:(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; - } - return NO; -} - -@end - -@implementation QMUIHelper (Device) - -static NSInteger isIPad = -1; -+ (BOOL)isIPad { - if (isIPad < 0) { - // [[[UIDevice currentDevice] model] isEqualToString:@"iPad"] 无法判断模拟器,改为以下方式 - isIPad = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? 1 : 0; - } - return isIPad > 0; -} - -static NSInteger isIPadPro = -1; -+ (BOOL)isIPadPro { - if (isIPadPro < 0) { - isIPadPro = [QMUIHelper isIPad] ? (DEVICE_WIDTH == 1024 && DEVICE_HEIGHT == 1366 ? 1 : 0) : 0; - } - return isIPadPro > 0; -} - -static NSInteger isIPod = -1; -+ (BOOL)isIPod { - if (isIPod < 0) { - isIPod = [[[UIDevice currentDevice] model] qmui_includesString:@"iPod touch"] ? 1 : 0; - } - return isIPod > 0; -} - -static NSInteger isIPhone = -1; -+ (BOOL)isIPhone { - if (isIPhone < 0) { - isIPhone = [[[UIDevice currentDevice] model] qmui_includesString:@"iPhone"] ? 1 : 0; - } - return isIPhone > 0; -} - -static NSInteger isSimulator = -1; -+ (BOOL)isSimulator { - if (isSimulator < 0) { -#if TARGET_OS_SIMULATOR - isSimulator = 1; -#else - isSimulator = 0; -#endif - } - return isSimulator > 0; -} - -static NSInteger is55InchScreen = -1; -+ (BOOL)is55InchScreen { - if (is55InchScreen < 0) { - is55InchScreen = (DEVICE_WIDTH == self.screenSizeFor55Inch.width && DEVICE_HEIGHT == self.screenSizeFor55Inch.height) ? 1 : 0; - } - return is55InchScreen > 0; -} - -static NSInteger is47InchScreen = -1; -+ (BOOL)is47InchScreen { - if (is47InchScreen < 0) { - is47InchScreen = (DEVICE_WIDTH == self.screenSizeFor47Inch.width && DEVICE_HEIGHT == self.screenSizeFor47Inch.height) ? 1 : 0; - } - return is47InchScreen > 0; -} - -static NSInteger is40InchScreen = -1; -+ (BOOL)is40InchScreen { - if (is40InchScreen < 0) { - is40InchScreen = (DEVICE_WIDTH == self.screenSizeFor40Inch.width && DEVICE_HEIGHT == self.screenSizeFor40Inch.height) ? 1 : 0; - } - return is40InchScreen > 0; -} - -static NSInteger is35InchScreen = -1; -+ (BOOL)is35InchScreen { - if (is35InchScreen < 0) { - is35InchScreen = (DEVICE_WIDTH == self.screenSizeFor35Inch.width && DEVICE_HEIGHT == self.screenSizeFor35Inch.height) ? 1 : 0; - } - return is35InchScreen > 0; -} - -+ (CGSize)screenSizeFor55Inch { - return CGSizeMake(414, 736); -} - -+ (CGSize)screenSizeFor47Inch { - return CGSizeMake(375, 667); -} - -+ (CGSize)screenSizeFor40Inch { - return CGSizeMake(320, 568); -} - -+ (CGSize)screenSizeFor35Inch { - return CGSizeMake(320, 480); -} - -static NSInteger isHighPerformanceDevice = -1; -+ (BOOL)isHighPerformanceDevice { - if (isHighPerformanceDevice < 0) { - isHighPerformanceDevice = (IOS_VERSION >= 8.0 && PreferredVarForUniversalDevices(YES, YES, YES, NO, NO)) ? 1 : 0; - } - return isHighPerformanceDevice > 0; -} - -@end - -@implementation QMUIHelper (Orientation) - -- (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; - } - - [[UIDevice currentDevice] setValue:@(orientation) forKey:@"orientation"]; - return YES; -} - -static char kAssociatedObjectKey_orientationBeforeChangedByHelper; -- (void)setOrientationBeforeChangingByHelper:(UIDeviceOrientation)orientationBeforeChangedByHelper { - objc_setAssociatedObject(self, &kAssociatedObjectKey_orientationBeforeChangedByHelper, @(orientationBeforeChangedByHelper), OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (UIDeviceOrientation)orientationBeforeChangingByHelper { - return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_orientationBeforeChangedByHelper)) integerValue]; -} - -+ (CGFloat)angleForTransformWithInterfaceOrientation:(UIInterfaceOrientation)orientation { - CGFloat angle; - switch (orientation) - { - case UIInterfaceOrientationPortraitUpsideDown: - angle = M_PI; - break; - case UIInterfaceOrientationLandscapeLeft: - angle = -M_PI_2; - break; - case UIInterfaceOrientationLandscapeRight: - angle = M_PI_2; - break; - default: - angle = 0.0; - break; - } - return angle; -} - -+ (CGAffineTransform)transformForCurrentInterfaceOrientation { - return [QMUIHelper transformWithInterfaceOrientation:[[UIApplication sharedApplication] statusBarOrientation]]; -} - -+ (CGAffineTransform)transformWithInterfaceOrientation:(UIInterfaceOrientation)orientation { - CGFloat angle = [QMUIHelper angleForTransformWithInterfaceOrientation:orientation]; - return CGAffineTransformMakeRotation(angle); -} - -@end - -@implementation QMUIHelper (ViewController) - -+ (nullable UIViewController *)visibleViewController { - UIViewController *rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController; - UIViewController *visibleViewController = [rootViewController qmui_visibleViewControllerIfExist]; - return visibleViewController; -} - -@end - -@implementation QMUIHelper (UIApplication) - -+ (void)renderStatusBarStyleDark { - [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault]; -} - -+ (void)renderStatusBarStyleLight { - [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent]; -} - -+ (void)dimmedApplicationWindow { - UIWindow *window = [[[UIApplication sharedApplication] delegate] window]; - window.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed; - [window tintColorDidChange]; -} - -+ (void)resetDimmedApplicationWindow { - UIWindow *window = [[[UIApplication sharedApplication] delegate] window]; - window.tintAdjustmentMode = UIViewTintAdjustmentModeNormal; - [window tintColorDidChange]; -} - -@end - -NSString *const QMUISpringAnimationKey = @"QMUISpringAnimationKey"; - -@implementation QMUIHelper (Animation) - -+ (void)actionSpringAnimationForView:(UIView *)view { - NSTimeInterval duration = 0.6; - CAKeyframeAnimation *springAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; - springAnimation.values = @[@.85, @1.15, @.9, @1.0,]; - springAnimation.keyTimes = @[@(0.0 / duration), @(0.15 / duration) , @(0.3 / duration), @(0.45 / duration),]; - springAnimation.duration = duration; - [view.layer addAnimation:springAnimation forKey:QMUISpringAnimationKey]; -} - -@end - -@implementation QMUIHelper (Log) - -- (void)printLogWithCalledFunction:(nonnull const char *)func log:(nonnull NSString *)log, ... { - va_list args; - va_start(args, log); - NSString *logString = [[NSString alloc] initWithFormat:log arguments:args]; - if ([self.helperDelegate respondsToSelector:@selector(QMUIHelperPrintLog:)]) { - [self.helperDelegate QMUIHelperPrintLog:[NSString stringWithFormat:@"QMUI - %@. Called By %s", logString, func]]; - } else { - NSLog(@"QMUI - %@. Called By %s", logString, func); - } - va_end(args); -} - -@end - -@implementation QMUIHelper - -+ (instancetype)sharedInstance { - static dispatch_once_t onceToken; - static QMUIHelper *instance = nil; - dispatch_once(&onceToken,^{ - instance = [[super allocWithZone:NULL] init]; - // 先设置默认值,不然可能变量的指针地址错误 - instance.keyboardVisible = NO; - instance.lastKeyboardHeight = 0; - instance.orientationBeforeChangingByHelper = 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(handleDeviceOrientationNotification:) name:UIDeviceOrientationDidChangeNotification object:nil]; - }); - return instance; -} - -+ (id)allocWithZone:(struct _NSZone *)zone{ - return [self sharedInstance]; -} - -- (void)dealloc { - // QMUIHelper 若干个分类里有用到消息监听,所以在 dealloc 的时候注销一下 - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.h index c5649dfb..6c3b8603 100644 --- a/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.h @@ -1,27 +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. + */ + // // CALayer+QMUI.h // qmui // -// Created by MoLice on 16/8/12. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/8/12. // +#import #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 有圆角,则请使用 QMUIBorder,否则系统的 border 会被 clip 掉 + * @warning 使用 qmui 方法,则超出 layer 范围内的内容都会被 clip 掉,系统的则不会 + * @warning 如果使用这个接口设置圆角,那么需要获取圆角的值需要用 qmui_originCornerRadius,否则 iOS 11 以下获取到的都是 0 + */ +@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 - * @warning 要被移动的sublayer必须已经添加到当前layer上 + * 把某个 sublayer 移动到当前所有 sublayers 的最后面 + * @param sublayer 要被移动的 layer + * @warning 要被移动的 sublayer 必须已经添加到当前 layer 上 */ - (void)qmui_sendSublayerToBack:(CALayer *)sublayer; /** - * 把某个sublayer移动到当前所有sublayers的最前面 - * @param sublayer 要被移动的layer - * @warning 要被移动的sublayer必须已经添加到当前layer上 + * 把某个 sublayer 移动到当前所有 sublayers 的最前面 + * @param sublayer 要被移动的layer + * @warning 要被移动的 sublayer 必须已经添加到当前 layer 上 */ - (void)qmui_bringSublayerToFront:(CALayer *)sublayer; @@ -30,6 +86,39 @@ */ - (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 每一段的线宽 + * @param lineSpacing 线之间的间隔 + * @param lineWidth 线的宽度 + * @param lineColor 线的颜色 + * @param isHorizontal 是否横向,因为画虚线的缘故,需要指定横向或纵向,横向是 YES,纵向是 NO。 + * 注意:暂不支持 dashPhase 和 dashPattens 数组设置,因为这些都定制性太强,如果用到则自己调用系统方法即可。 + */ ++ (CAShapeLayer *)qmui_separatorDashLayerWithLineLength:(NSInteger)lineLength + lineSpacing:(NSInteger)lineSpacing + lineWidth:(CGFloat)lineWidth + lineColor:(CGColorRef)lineColor + isHorizontal:(BOOL)isHorizontal; + +/** + + * 产生一个通用分隔虚线的 layer,高度为 PixelOne,线宽为 2,线距为 2,默认会移除动画,并且背景色用 UIColorSeparator,注意返回的是 CAShapeLayer。 + + * 其中,InHorizon 是横向;InVertical 是纵向。 + + */ ++ (CAShapeLayer *)qmui_separatorDashLayerInHorizontal; + ++ (CAShapeLayer *)qmui_separatorDashLayerInVertical; + /** * 产生一个适用于做通用分隔线的 layer,高度为 PixelOne,默认会移除动画,并且背景色用 UIColorSeparator */ @@ -39,4 +128,14 @@ * 产生一个适用于做列表分隔线的 layer,高度为 PixelOne,默认会移除动画,并且背景色用 TableViewSeparatorColor */ + (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 90985316..8eff35fb 100644 --- a/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/CALayer+QMUI.m @@ -1,28 +1,244 @@ +/** + * 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+QMUI.m // qmui // -// Created by MoLice on 16/8/12. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/8/12. // #import "CALayer+QMUI.h" +#import "UIView+QMUI.h" #import "QMUICore.h" +#import "QMUILog.h" +#import "UIColor+QMUI.h" + +@interface CALayer () + +@property(nonatomic, assign) float qmui_speedBeforePause; + +@end @implementation CALayer (QMUI) -- (void)qmui_sendSublayerToBack:(CALayer *)sublayer { - if (sublayer.superlayer == self) { - [sublayer removeFromSuperlayer]; - [self insertSublayer:sublayer atIndex:0]; +QMUISynthesizeFloatProperty(qmui_speedBeforePause, setQmui_speedBeforePause) +QMUISynthesizeCGFloatProperty(qmui_originCornerRadius, setQmui_originCornerRadius) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // 由于其他方法需要通过调用 qmuilayer_setCornerRadius: 来执行 swizzle 前的实现,所以这里暂时用 ExchangeImplementations + ExchangeImplementations([CALayer class], @selector(setCornerRadius:), @selector(qmuilayer_setCornerRadius:)); + + ExtendImplementationOfNonVoidMethodWithoutArguments([CALayer class], @selector(init), CALayer *, ^CALayer *(CALayer *selfObject, CALayer *originReturnValue) { + selfObject.qmui_speedBeforePause = selfObject.speed; + 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) { + + // 对非法的 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); + } + } + + // call super + void (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, bounds); + }; + }); + + OverrideImplementation([CALayer class], @selector(setPosition:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(CALayer *selfObject, CGPoint position) { + + // 对非法的 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)); + } + } + + // call super + void (*originSelectorIMP)(id, SEL, CGPoint); + originSelectorIMP = (void (*)(id, SEL, CGPoint))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, position); + }; + }); + }); +} + +- (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; + [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.qmui_borderLayer setNeedsLayout];// 直接调用 layer 的 setNeedsLayout,没有线程限制,如果通过 view 调用则需要在主线程才行 + } + } } } -- (void)qmui_bringSublayerToFront:(CALayer *)sublayer { - if (sublayer.superlayer == self) { - [sublayer removeFromSuperlayer]; - [self insertSublayer:sublayer atIndex:(unsigned)self.sublayers.count]; +static char kAssociatedObjectKey_pause; +- (void)setQmui_pause:(BOOL)qmui_pause { + if (qmui_pause == self.qmui_pause) { + return; + } + if (qmui_pause) { + self.qmui_speedBeforePause = self.speed; + CFTimeInterval pausedTime = [self convertTime:CACurrentMediaTime() fromLayer:nil]; + self.speed = 0; + self.timeOffset = pausedTime; + } else { + CFTimeInterval pausedTime = self.timeOffset; + self.speed = self.qmui_speedBeforePause; + self.timeOffset = 0; + self.beginTime = 0; + CFTimeInterval timeSincePause = [self convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime; + self.beginTime = timeSincePause; + } + objc_setAssociatedObject(self, &kAssociatedObjectKey_pause, @(qmui_pause), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (BOOL)qmui_pause { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_pause)) boolValue]; +} + +static char kAssociatedObjectKey_maskedCorners; +- (void)setQmui_maskedCorners:(QMUICornerMask)qmui_maskedCorners { + BOOL maskedCornersChanged = qmui_maskedCorners != self.qmui_maskedCorners; + objc_setAssociatedObject(self, &kAssociatedObjectKey_maskedCorners, @(qmui_maskedCorners), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + 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.qmui_borderLayer setNeedsLayout];// 直接调用 layer 的 setNeedsLayout,没有线程限制,如果通过 view 调用则需要在主线程才行 + } + } + } +} + +- (QMUICornerMask)qmui_maskedCorners { + return [objc_getAssociatedObject(self, &kAssociatedObjectKey_maskedCorners) unsignedIntegerValue]; +} + +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); +} + +- (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 { @@ -57,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], @@ -80,6 +298,50 @@ - (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 + lineColor:(CGColorRef)lineColor + isHorizontal:(BOOL)isHorizontal { + CAShapeLayer *layer = [CAShapeLayer layer]; + layer.fillColor = UIColorClear.CGColor; + layer.strokeColor = lineColor; + layer.lineWidth = lineWidth; + layer.lineDashPattern = [NSArray arrayWithObjects:[NSNumber numberWithInteger:lineLength], [NSNumber numberWithInteger:lineSpacing], nil]; + layer.masksToBounds = YES; + + CGMutablePathRef path = CGPathCreateMutable(); + if (isHorizontal) { + CGPathMoveToPoint(path, NULL, 0, lineWidth / 2); + CGPathAddLineToPoint(path, NULL, SCREEN_WIDTH, lineWidth / 2); + } else { + CGPathMoveToPoint(path, NULL, lineWidth / 2, 0); + CGPathAddLineToPoint(path, NULL, lineWidth / 2, SCREEN_HEIGHT); + } + layer.path = path; + CGPathRelease(path); + + return layer; +} + ++ (CAShapeLayer *)qmui_separatorDashLayerInHorizontal { + CAShapeLayer *layer = [CAShapeLayer qmui_separatorDashLayerWithLineLength:2 lineSpacing:2 lineWidth:PixelOne lineColor:UIColorSeparatorDashed.CGColor isHorizontal:YES]; + return layer; +} + ++ (CAShapeLayer *)qmui_separatorDashLayerInVertical { + CAShapeLayer *layer = [CAShapeLayer qmui_separatorDashLayerWithLineLength:2 lineSpacing:2 lineWidth:PixelOne lineColor:UIColorSeparatorDashed.CGColor isHorizontal:NO]; + return layer; +} + + (CALayer *)qmui_separatorLayer { CALayer *layer = [CALayer layer]; [layer qmui_removeDefaultAnimations]; @@ -95,3 +357,119 @@ + (CALayer *)qmui_separatorLayerForTableView { } @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 CAGradientLayer (QMUI_DynamicColor) + +@property(nonatomic, strong) NSArray * qcl_originalColors; + +@end + +@implementation CAGradientLayer (QMUI_DynamicColor) + +QMUISynthesizeIdStrongProperty(qcl_originalColors, setQcl_originalColors) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + 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 { + [originalColors addObject:[UIColor colorWithCGColor:(__bridge CGColorRef _Nonnull)(color)]]; + } + }]; + + if (hasDynamicColor) { + selfObject.qcl_originalColors = originalColors; + } else { + selfObject.qcl_originalColors = nil; + } + + }; + }); + }); +} + +- (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 new file mode 100644 index 00000000..8aaa2f1d --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSArray+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. + */ + +// +// NSArray+QMUI.h +// QMUIKit +// +// Created by QMUI Team on 2017/11/14. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSArray (QMUI) + +/** + 将多个对象合并成一个数组,如果参数类型是数组则会将数组内的元素拆解出来加到 return 内(只会拆解一层,所以多维数组不处理) + + @param object 要合并的多个数组 + @return 合并完的结果 + */ ++ (instancetype)qmui_arrayWithObjects:(ObjectType)object, ...; + +/** + * 将多维数组打平成一维数组再遍历所有子元素 + */ +- (void)qmui_enumerateNestedArrayWithBlock:(void (NS_NOESCAPE^)(id obj, BOOL *stop))block; + +/** + * 将多维数组递归转换成 mutable 多维数组 + */ +- (NSMutableArray *)qmui_mutableCopyNestedArray; + +/** + * 过滤数组元素,将 block 返回 YES 的 item 重新组装成一个数组返回 + */ +- (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 new file mode 100644 index 00000000..c6d824a9 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSArray+QMUI.m @@ -0,0 +1,125 @@ +/** + * 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. + */ + +// +// NSArray+QMUI.m +// QMUIKit +// +// Created by QMUI Team on 2017/11/14. +// + +#import "NSArray+QMUI.h" + +@implementation NSArray (QMUI) + ++ (instancetype)qmui_arrayWithObjects:(id)object, ... { + void (^addObjectToArrayBlock)(NSMutableArray *array, id obj) = ^void(NSMutableArray *array, id obj) { + if ([obj isKindOfClass:[NSArray class]]) { + [array addObjectsFromArray:obj]; + } else { + [array addObject:obj]; + } + }; + + NSMutableArray *result = [[NSMutableArray alloc] init]; + addObjectToArrayBlock(result, object); + + va_list argumentList; + va_start(argumentList, object); + id argument; + while ((argument = va_arg(argumentList, id))) { + addObjectToArrayBlock(result, argument); + } + va_end(argumentList); + if ([self isKindOfClass:[NSMutableArray class]]) { + return result; + } + return result.copy; +} + +- (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]; + if ([object isKindOfClass:[NSArray class]]) { + [((NSArray *)object) qmui_enumerateNestedArrayWithBlock:block]; + } else { + block(object, &stop); + } + if (stop) { + return; + } + } +} + +- (NSMutableArray *)qmui_mutableCopyNestedArray { + NSMutableArray *mutableResult = [self mutableCopy]; + for (NSInteger i = 0; i < self.count; i++) { + id object = self[i]; + if ([object isKindOfClass:[NSArray class]]) { + NSMutableArray *mutableItem = [((NSArray *)object) qmui_mutableCopyNestedArray]; + [mutableResult replaceObjectAtIndex:i withObject:mutableItem]; + } + } + return mutableResult; +} + +- (NSArray *)qmui_filterWithBlock:(BOOL (NS_NOESCAPE^)(id _Nonnull))block { + if (!block) { + return self; + } + + NSMutableArray *result = [[NSMutableArray alloc] init]; + for (NSInteger i = 0; i < self.count; i++) { + id item = self[i]; + if (block(item)) { + [result addObject:item]; + } + } + 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 9c9e4dad..8d45c49b 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.h @@ -1,22 +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. + */ + // // NSAttributedString+QMUI.h // qmui // -// Created by MoLice on 16/9/23. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/9/23. // #import +#import +#import "QMUIHelper.h" +#import "NSString+QMUI.h" + +NS_ASSUME_NONNULL_BEGIN + +/// 如果某个 NSAttributedString 是通过 +[NSAttributedString qmui_attributedStringWithImage:margins:] 创建的,则该 string 会被添加以这个 name 为 key 的 attribute,值为 NSValue 包裹的 UIEdgeInsets。 +UIKIT_EXTERN NSAttributedStringKey const QMUIImageMarginsAttributeName; + +@interface NSAttributedString (QMUI) -@interface NSAttributedString (QMUI) +/** + * @brief 将指定 image 作为 NSTextAttachment 用以生成一段 NSAttributedString。 + * @note 如果该 image 是由 [UIImage qmui_imageWithAttributedString:] 生成的,则会利用 image 内部关联的 attributes 来试图调整 image 的 y 轴偏移值,以使其与其他文本垂直对齐。 + * @param image 要用的图片 + */ ++ (instancetype)qmui_attributedStringWithImage:(UIImage *)image; + +/** + * @brief 将指定 image 作为 NSTextAttachment 用以生成一段 NSAttributedString,并利用给定的一整段文字的 attributes 来自动居中 image + * @note 一般情况下我们会将某个 image 作为一串富文本里的某一个部分拼接在一起,为了保证 image、string 垂直对齐,需要根据 font、lineHeight 等信息做一些垂直方向的调整,此时你可以将整段文字的 attributes 传进来,内部根据一定规则帮你计算。 + * @param image 要用的图片 + * @param attributes 最终一整段文字的 attributes + */ ++ (instancetype)qmui_attributedStringWithImage:(UIImage *)image alignByAttributes:(NSDictionary *)attributes; /** - * 按照中文 2 个字符、英文 1 个字符的方式来计算文本长度 + * @brief 创建一个包含图片的 attributedString + * @param image 要用的图片 + * @param offset 图片相对基线的垂直偏移(当 offset > 0 时,图片会向上偏移) + * @param leftMargin 图片距离左侧内容的间距 + * @param rightMargin 图片距离右侧内容的间距 + * @note leftMargin 和 rightMargin 必须大于或等于 0 */ -- (NSUInteger)qmui_lengthWhenCountingNonASCIICharacterAsTwo; ++ (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 + * @param width 空白占位符的宽度 + */ ++ (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 dbabb9bb..96a09334 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.m @@ -1,61 +1,219 @@ +/** + * 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. + */ + // // NSAttributedString+QMUI.m // qmui // -// Created by MoLice on 16/9/23. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/9/23. // #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 - ReplaceMethod([[[NSAttributedString alloc] initWithString:@""] class], @selector(initWithString:), @selector(qmui_initWithString:)); - ReplaceMethod([[[NSAttributedString alloc] initWithString:@"" attributes:nil] class], @selector(initWithString:attributes:), @selector(qmui_initWithString:attributes:)); - }); ++ (instancetype)qmui_attributedStringWithImage:(UIImage *)image { + return [self qmui_attributedStringWithImage:image alignByAttributes:image.qmui_stringAttributes]; +} + ++ (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 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 margins:(UIEdgeInsets)margins { + if (!image) { + return nil; + } + NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; + attachment.image = image; + attachment.bounds = CGRectMake(0, -margins.top, image.size.width, image.size.height); + NSMutableAttributedString *string = [[NSAttributedString attributedStringWithAttachment:attachment] mutableCopy]; + 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 (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; } -- (instancetype)qmui_initWithString:(NSString *)str { - str = str ?: @""; - return [self qmui_initWithString:str]; ++ (instancetype)qmui_attributedStringWithFixedSpace:(CGFloat)width { + UIGraphicsBeginImageContext(CGSizeMake(width, 1)); + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return [self qmui_attributedStringWithImage:image]; } -- (instancetype)qmui_initWithString:(NSString *)str attributes:(NSDictionary *)attrs { - str = str ?: @""; - return [self qmui_initWithString:str attributes:attrs]; +- (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 - ReplaceMethod([[[NSMutableAttributedString alloc] initWithString:@""] class], @selector(initWithString:), @selector(qmui_initWithString:)); - ReplaceMethod([[[NSMutableAttributedString alloc] initWithString:@"" attributes:nil] class], @selector(initWithString:attributes:), @selector(qmui_initWithString:attributes:)); + OverrideImplementation([UIImage class], @selector(copyWithZone:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^id (UIImage *selfObject, NSZone *firstArgv) { + + // call super + id (*originSelectorIMP)(id, SEL, NSZone *); + originSelectorIMP = (id (*)(id, SEL, NSZone *))originalIMPProvider(); + id result = originSelectorIMP(selfObject, originCMD, firstArgv); + + if ([result isKindOfClass:UIImage.class]) { + id obj = [result qmui_getBoundObjectForKey:kQMUIImageOriginalAttributedStringKey]; + if (obj) { + [result qmui_bindObjectWeakly:obj forKey:kQMUIImageOriginalAttributedStringKey]; + } + } + return result; + }; + }); }); } -- (instancetype)qmui_initWithString:(NSString *)str { - str = str ?: @""; - return [self qmui_initWithString:str]; ++ (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]; } -- (instancetype)qmui_initWithString:(NSString *)str attributes:(NSDictionary *)attrs { - str = str ?: @""; - return [self qmui_initWithString:str attributes:attrs]; +- (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 new file mode 100644 index 00000000..78229312 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+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. + */ + +// +// NSCharacterSet+QMUI.h +// QMUIKit +// +// Created by QMUI Team on 2018/9/17. +// + +#import + +@interface NSCharacterSet (QMUI) + +/** + 也即在系统的 URLQueryAllowedCharacterSet 基础上去掉“#&=”这3个字符,专用于 URL query 里来源于用户输入的 value,避免服务器解析出现异常。 + */ +@property (class, readonly, copy) NSCharacterSet *qmui_URLUserInputQueryAllowedCharacterSet; + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.m new file mode 100644 index 00000000..0a2ea0ff --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.m @@ -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. + */ + +// +// NSCharacterSet+QMUI.m +// QMUIKit +// +// Created by QMUI Team on 2018/9/17. +// + +#import "NSCharacterSet+QMUI.h" + +@implementation NSCharacterSet (QMUI) + ++ (NSCharacterSet *)qmui_URLUserInputQueryAllowedCharacterSet { + NSMutableCharacterSet *set = [NSCharacterSet URLQueryAllowedCharacterSet].mutableCopy; + [set removeCharactersInString:@"#&="]; + return set.copy; +} + +@end 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 new file mode 100644 index 00000000..8f0d779c --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.h @@ -0,0 +1,37 @@ +/** + * 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. +// + +#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/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.m new file mode 100644 index 00000000..7b654935 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSMethodSignature+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. + */ +// +// NSMethodSignature+QMUI.m +// QMUIKit +// +// Created by MoLice on 2019/A/28. +// + +#import "NSMethodSignature+QMUI.h" +#import "NSObject+QMUI.h" +#import "QMUICore.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 { + BeginIgnorePerformSelectorLeaksWarning + NSString *typeString = [self performSelector:NSSelectorFromString([NSString stringWithFormat:@"_%@String", @"type"])]; + EndIgnorePerformSelectorLeaksWarning + return typeString; +} + +- (const char *)qmui_typeEncoding { + return self.qmui_typeString.UTF8String; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.h new file mode 100644 index 00000000..10bb929d --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.h @@ -0,0 +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. + */ + +// +// NSNumber+QMUI.h +// QMUIKit +// +// Created by QMUI Team on 2018/1/16. +// + +#import +#import + +@interface NSNumber (QMUI) + +@property(nonatomic, assign, readonly) CGFloat qmui_CGFloatValue; +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.m new file mode 100644 index 00000000..23838cb9 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSNumber+QMUI.m @@ -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. + */ + +// +// NSNumber+QMUI.m +// QMUIKit +// +// Created by QMUI Team on 2018/1/16. +// + +#import "NSNumber+QMUI.h" + +@implementation NSNumber (QMUI) + +- (CGFloat)qmui_CGFloatValue { +#if CGFLOAT_IS_DOUBLE + return self.doubleValue; +#else + return self.floatValue; +#endif +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.h index dfb71bde..22334fa1 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.h @@ -1,18 +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. + */ + // // NSObject+QMUI.h // qmui // -// Created by MoLice on 2016/11/1. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2016/11/1. // #import +#import + +NS_ASSUME_NONNULL_BEGIN @interface NSObject (QMUI) /** 判断当前类是否有重写某个父类的指定方法 - + @param selector 要判断的方法 @param superclass 要比较的父类,必须是当前类的某个 superclass @return YES 表示子类有重写了父类方法,NO 表示没有重写(异常情况也返回 NO,例如当前类与指定的类并非父子关系、父类本身也无法响应指定的方法) @@ -20,41 +30,289 @@ - (BOOL)qmui_hasOverrideMethod:(SEL)selector ofSuperclass:(Class)superclass; /** - 对 super 发送消息 + 判断指定的类是否有重写某个父类的指定方法 + + @param selector 要判断的方法 + @param superclass 要比较的父类,必须是当前类的某个 superclass + @return YES 表示子类有重写了父类方法,NO 表示没有重写(异常情况也返回 NO,例如当前类与指定的类并非父子关系、父类本身也无法响应指定的方法) + */ ++ (BOOL)qmui_hasOverrideMethod:(SEL)selector forClass:(Class)aClass ofSuperclass:(Class)superclass; +/** + 对 super 发送消息 + @param aSelector 要发送的消息 @return 消息执行后的结果 - @link http://stackoverflow.com/questions/14635024/using-objc-msgsendsuper-to-invoke-a-class-method + @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 发送消息 - + @param aSelector 要发送的消息 @param object 作为参数传过去 @return 消息执行后的结果 - @link http://stackoverflow.com/questions/14635024/using-objc-msgsendsuper-to-invoke-a-class-method + @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; /** - 使用 block 遍历当前实例的所有方法,父类的方法不包含在内 + * 调用一个无参数、返回值类型为非对象的 selector。如果返回值类型为对象,请直接使用系统的 performSelector: 方法。 + * @param selector 要被调用的方法名 + * @param returnValue selector 的返回值的指针地址,请先定义一个变量再将其指针地址传进来,例如 &result + * + * @code + * CGFloat alpha; + * [view qmui_performSelector:@selector(alpha) withPrimitiveReturnValue:&alpha]; + * @endcode */ -- (void)qmui_enumrateInstanceMethodsUsingBlock:(void (^)(SEL selector))block; +- (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(nullable void *)returnValue; /** - 使用 block 遍历指定的某个类的实例方法,该类的父类方法不包含在内 - * @param aClass 要遍历的某个类 - * @param block 遍历时使用的 block,参数为某一个方法 + * 调用一个带参数的 selector,参数类型支持对象和非对象,也没有数量限制。返回值为对象或者 void。 + * @param selector 要被调用的方法名 + * @param firstArgument 参数列表,请传参数的指针地址,支持多个参数 + * @return 方法的返回值,如果该方法返回类型为 void,则会返回 nil,如果返回类型为对象,则返回该对象。 + * + * @code + * id target = xxx; + * SEL action = xxx; + * UIControlEvents events = xxx; + * [control qmui_performSelector:@selector(addTarget:action:forControlEvents:) withArguments:&target, &action, &events, nil]; + * @endcode */ -+ (void)qmui_enumrateInstanceMethodsOfClass:(Class)aClass usingBlock:(void (^)(SEL selector))block; +- (nullable id)qmui_performSelector:(SEL)selector withArguments:(nullable void *)firstArgument, ...; /** - 遍历某个 protocol 里的所有方法 + * 调用一个返回值类型为非对象且带参数的 selector,参数类型支持对象和非对象,也没有数量限制。 + * + * @param selector 要被调用的方法名 + * @param returnValue selector 的返回值的指针地址 + * @param firstArgument 参数列表,请传参数的指针地址,支持多个参数 + * + * @code + * CGPoint point = xxx; + * UIEvent *event = xxx; + * BOOL isInside; + * [view qmui_performSelector:@selector(pointInside:withEvent:) withPrimitiveReturnValue:&isInside arguments:&point, &event, nil]; + * @endcode + */ +- (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(nullable void *)returnValue arguments:(nullable void *)firstArgument, ...; + +/** + 使用 block 遍历指定 class 的所有成员变量(也即 _xxx 那种),不包含 property 对应的 _property 成员变量,也不包含 superclasses 里定义的变量 + + @param block 用于遍历的 block + */ +- (void)qmui_enumrateIvarsUsingBlock:(void (^)(Ivar ivar, NSString *ivarDescription))block; + +/** + 使用 block 遍历指定 class 的所有成员变量(也即 _xxx 那种),不包含 property 对应的 _property 成员变量 + + @param aClass 指定的 class + @param includingInherited 是否要包含由继承链带过来的 ivars + @param block 用于遍历的 block + */ ++ (void)qmui_enumrateIvarsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Ivar ivar, NSString *ivarDescription))block; + +/** + 使用 block 遍历指定 class 的所有属性,不包含 superclasses 里定义的 property + + @param block 用于遍历的 block,如果要获取 property 的信息,推荐用 QMUIPropertyDescriptor。 + */ +- (void)qmui_enumratePropertiesUsingBlock:(void (^)(objc_property_t property, NSString *propertyName))block; + +/** + 使用 block 遍历指定 class 的所有属性 + + @param aClass 指定的 class + @param includingInherited 是否要包含由继承链带过来的 property + @param block 用于遍历的 block,如果要获取 property 的信息,推荐用 QMUIPropertyDescriptor。 + @see https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW1 + */ ++ (void)qmui_enumratePropertiesOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(objc_property_t property, NSString *propertyName))block; + +/** + 使用 block 遍历当前实例的所有方法,不包含 superclasses 里定义的 method + */ +- (void)qmui_enumrateInstanceMethodsUsingBlock:(void (^)(Method method, SEL selector))block; + +/** + 使用 block 遍历指定的某个类的实例方法 + @param aClass 指定的 class + @param includingInherited 是否要包含由继承链带过来的 method + @param block 用于遍历的 block + */ ++ (void)qmui_enumrateInstanceMethodsOfClass:(Class)aClass includingInherited:(BOOL)includingInherited usingBlock:(void (^)(Method method, SEL selector))block; + +/** + 遍历某个 protocol 里的所有方法 + @param protocol 要遍历的 protocol,例如 \@protocol(xxx) @param block 遍历过程中调用的 block */ + (void)qmui_enumerateProtocolMethods:(Protocol *)protocol usingBlock:(void (^)(SEL selector))block; + +@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) + +/** + 给对象绑定上另一个对象以供后续取出使用,如果 object 传入 nil 则会清除该 key 之前绑定的对象 + + @attention 被绑定的对象会被 strong 强引用 + @note 内部是使用 objc_setAssociatedObject / objc_getAssociatedObject 来实现 + + @code + - (UITableViewCell *)cellForIndexPath:(NSIndexPath *)indexPath { + // 1)在这里给 button 绑定上 indexPath 对象 + [cell qmui_bindObject:indexPath forKey:@"indexPath"]; + } + + - (void)didTapButton:(UIButton *)button { + // 2)在这里取出被点击的 button 的 indexPath 对象 + NSIndexPath *indexPathTapped = [button qmui_getBoundObjectForKey:@"indexPath"]; + } + @endcode + */ +- (void)qmui_bindObject:(nullable id)object forKey:(NSString *)key; + +/** + 给对象绑定上另一个对象以供后续取出使用,但相比于 qmui_bindObject:forKey:,该方法不会 strong 强引用传入的 object + */ +- (void)qmui_bindObjectWeakly:(nullable id)object forKey:(NSString *)key; + +/** + 取出之前使用 bind 方法绑定的对象 + */ +- (nullable id)qmui_getBoundObjectForKey:(NSString *)key; + +/** + 给对象绑定上一个 double 值以供后续取出使用 + */ +- (void)qmui_bindDouble:(double)doubleValue forKey:(NSString *)key; + +/** + 取出之前用 bindDouble:forKey: 绑定的值 + */ +- (double)qmui_getBoundDoubleForKey:(NSString *)key; + +/** + 给对象绑定上一个 BOOL 值以供后续取出使用 + */ +- (void)qmui_bindBOOL:(BOOL)boolValue forKey:(NSString *)key; + +/** + 取出之前用 bindBOOL:forKey: 绑定的值 + */ +- (BOOL)qmui_getBoundBOOLForKey:(NSString *)key; + +/** + 给对象绑定上一个 long 值以供后续取出使用 + */ +- (void)qmui_bindLong:(long)longValue forKey:(NSString *)key; + +/** + 取出之前用 bindLong:forKey: 绑定的值 + */ +- (long)qmui_getBoundLongForKey:(NSString *)key; + +/** + 移除之前使用 bind 方法绑定的对象 + */ +- (void)qmui_clearBindingForKey:(NSString *)key; + +/** + 移除之前使用 bind 方法绑定的所有对象 + */ +- (void)qmui_clearAllBinding; + +/** + 返回当前有绑定对象存在的所有的 key 的数组,如果不存在任何 key,则返回一个空数组 + @note 数组中元素的顺序是随机的 + */ +- (NSArray *)qmui_allBindingKeys; + +/** + 返回是否设置了某个 key + */ +- (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 9853ebbb..9fd21b90 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSObject+QMUI.m @@ -1,30 +1,43 @@ +/** + * 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+QMUI.m // qmui // -// Created by MoLice on 2016/11/1. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2016/11/1. // #import "NSObject+QMUI.h" +#import "QMUIWeakObjectContainer.h" +#import "QMUICore.h" +#import "NSString+QMUI.h" #import -#import + @implementation NSObject (QMUI) + - (BOOL)qmui_hasOverrideMethod:(SEL)selector ofSuperclass:(Class)superclass { - if (![[self class] isSubclassOfClass:superclass]) { -// NSLog(@"%s, %@ 并非 %@ 的父类", __func__, NSStringFromClass(superclass), NSStringFromClass([self class])); + return [NSObject qmui_hasOverrideMethod:selector forClass:self.class ofSuperclass:superclass]; +} + ++ (BOOL)qmui_hasOverrideMethod:(SEL)selector forClass:(Class)aClass ofSuperclass:(Class)superclass { + if (![aClass isSubclassOfClass:superclass]) { return NO; } if (![superclass instancesRespondToSelector:selector]) { -// NSLog(@"%s, 父类 %@ 自己本来就无法响应 %@ 方法", __func__, NSStringFromClass(superclass), NSStringFromSelector(selector)); return NO; } Method superclassMethod = class_getInstanceMethod(superclass, selector); - Method instanceMethod = class_getInstanceMethod([self class], selector); + Method instanceMethod = class_getInstanceMethod(aClass, selector); if (!instanceMethod || instanceMethod == superclassMethod) { return NO; } @@ -49,24 +62,188 @@ - (id)qmui_performSelectorToSuperclass:(SEL)aSelector withObject:(id)object { return (*objc_superAllocTyped)(&mySuper, aSelector, object); } -- (void)qmui_enumrateInstanceMethodsUsingBlock:(void (^)(SEL))block { - [NSObject qmui_enumrateInstanceMethodsOfClass:self.class usingBlock:block]; +- (id)qmui_performSelector:(SEL)selector withArguments:(void *)firstArgument, ... { + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; + [invocation setTarget:self]; + [invocation setSelector:selector]; + + if (firstArgument) { + va_list valist; + va_start(valist, firstArgument); + [invocation setArgument:firstArgument atIndex:2];// 0->self, 1->_cmd + + void *currentArgument; + NSInteger index = 3; + while ((currentArgument = va_arg(valist, void *))) { + [invocation setArgument:currentArgument atIndex:index]; + index++; + } + va_end(valist); + } + + [invocation invoke]; + + const char *typeEncoding = method_getTypeEncoding(class_getInstanceMethod(object_getClass(self), selector)); + if (isObjectTypeEncoding(typeEncoding)) { + __unsafe_unretained id returnValue; + [invocation getReturnValue:&returnValue]; + return returnValue; + } + return nil; +} + +- (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(void *)returnValue { + [self qmui_performSelector:selector withPrimitiveReturnValue:returnValue arguments:nil]; +} + +- (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(void *)returnValue arguments:(void *)firstArgument, ... { + 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]; + + if (firstArgument) { + va_list valist; + va_start(valist, firstArgument); + [invocation setArgument:firstArgument atIndex:2];// 0->self, 1->_cmd + + void *currentArgument; + NSInteger index = 3; + while ((currentArgument = va_arg(valist, void *))) { + [invocation setArgument:currentArgument atIndex:index]; + index++; + } + va_end(valist); + } + + [invocation invoke]; + + if (returnValue) { + [invocation getReturnValue:returnValue]; + } +} + +- (void)qmui_enumrateIvarsUsingBlock:(void (^)(Ivar ivar, NSString *ivarDescription))block { + [self qmui_enumrateIvarsIncludingInherited:NO usingBlock: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(self.class, &outCount); + for (unsigned int i = 0; i < outCount; i ++) { + Ivar ivar = ivars[i]; + 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 = 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); + + for (unsigned int i = 0; i < propertiesCount; i++) { + objc_property_t property = properties[i]; + if (block) block(property, [NSString stringWithFormat:@"%s", property_getName(property)]); + } + + free(properties); + + if (includingInherited) { + Class superclass = class_getSuperclass(aClass); + if (superclass) { + [NSObject qmui_enumratePropertiesOfClass:superclass includingInherited:includingInherited usingBlock:block]; + } + } +} + +- (void)qmui_enumrateInstanceMethodsUsingBlock:(void (^)(Method, SEL))block { + [NSObject qmui_enumrateInstanceMethodsOfClass:self.class includingInherited:NO usingBlock:block]; } -+ (void)qmui_enumrateInstanceMethodsOfClass:(Class)aClass usingBlock:(void (^)(SEL selector))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); for (unsigned int i = 0; i < methodCount; i++) { Method method = methods[i]; SEL selector = method_getName(method); - if (block) block(selector); + if (block) block(method, selector); } free(methods); + + if (includingInherited) { + Class superclass = class_getSuperclass(aClass); + if (superclass) { + [NSObject qmui_enumrateInstanceMethodsOfClass:superclass includingInherited:includingInherited usingBlock:block]; + } + } } + (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++) { @@ -79,3 +256,276 @@ + (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) + +static char kAssociatedObjectKey_QMUIAllBoundObjects; +- (NSMutableDictionary *)qmui_allBoundObjects { + NSMutableDictionary *dict = objc_getAssociatedObject(self, &kAssociatedObjectKey_QMUIAllBoundObjects); + if (!dict) { + dict = [NSMutableDictionary dictionary]; + objc_setAssociatedObject(self, &kAssociatedObjectKey_QMUIAllBoundObjects, dict, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return dict; +} + +- (void)qmui_bindObject:(id)object forKey:(NSString *)key { + if (!key.length) { + return; + } + if (object) { + [[self qmui_allBoundObjects] setObject:object forKey:key]; + } else { + [[self qmui_allBoundObjects] removeObjectForKey:key]; + } +} + +- (void)qmui_bindObjectWeakly:(id)object forKey:(NSString *)key { + if (!key.length) { + return; + } + if (object) { + QMUIWeakObjectContainer *container = [[QMUIWeakObjectContainer alloc] initWithObject:object]; + [self qmui_bindObject:container forKey:key]; + } else { + [[self qmui_allBoundObjects] removeObjectForKey:key]; + } +} + +- (id)qmui_getBoundObjectForKey:(NSString *)key { + if (!key.length) { + return nil; + } + id storedObj = [[self qmui_allBoundObjects] objectForKey:key]; + if ([storedObj respondsToSelector:@selector(isQMUIWeakObjectContainer)] && ((QMUIWeakObjectContainer *)storedObj).isQMUIWeakObjectContainer) { + storedObj = [(QMUIWeakObjectContainer *)storedObj object]; + } + return storedObj; +} + +- (void)qmui_bindDouble:(double)doubleValue forKey:(NSString *)key { + [self qmui_bindObject:@(doubleValue) forKey:key]; +} + +- (double)qmui_getBoundDoubleForKey:(NSString *)key { + id object = [self qmui_getBoundObjectForKey:key]; + if ([object isKindOfClass:[NSNumber class]]) { + double doubleValue = [(NSNumber *)object doubleValue]; + return doubleValue; + + } else { + return 0.0; + } +} + +- (void)qmui_bindBOOL:(BOOL)boolValue forKey:(NSString *)key { + [self qmui_bindObject:@(boolValue) forKey:key]; +} + +- (BOOL)qmui_getBoundBOOLForKey:(NSString *)key { + id object = [self qmui_getBoundObjectForKey:key]; + if ([object isKindOfClass:[NSNumber class]]) { + BOOL boolValue = [(NSNumber *)object boolValue]; + return boolValue; + + } else { + return NO; + } +} + +- (void)qmui_bindLong:(long)longValue forKey:(NSString *)key { + [self qmui_bindObject:@(longValue) forKey:key]; +} + +- (long)qmui_getBoundLongForKey:(NSString *)key { + id object = [self qmui_getBoundObjectForKey:key]; + if ([object isKindOfClass:[NSNumber class]]) { + long longValue = [(NSNumber *)object longValue]; + return longValue; + + } else { + return 0; + } +} + +- (void)qmui_clearBindingForKey:(NSString *)key { + [self qmui_bindObject:nil forKey:key]; +} + +- (void)qmui_clearAllBinding { + [[self qmui_allBoundObjects] removeAllObjects]; +} + +- (NSArray *)qmui_allBindingKeys { + NSArray *allKeys = [[self qmui_allBoundObjects] allKeys]; + return allKeys; +} + +- (BOOL)qmui_hasBindingKey:(NSString *)key { + return [[self qmui_allBindingKeys] containsObject: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 483fed0b..829c5288 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.h @@ -1,18 +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. + */ + // // NSParagraphStyle+QMUI.h // qmui // -// Created by MoLice on 16/8/9. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/8/9. // #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 a29c0ac4..bc1a8356 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.m @@ -1,14 +1,21 @@ +/** + * 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. + */ + // // NSParagraphStyle+QMUI.m // qmui // -// Created by MoLice on 16/8/9. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/8/9. // #import "NSParagraphStyle+QMUI.h" -@implementation NSMutableParagraphStyle (QMUI) +@implementation NSParagraphStyle (QMUI) + (instancetype)qmui_paragraphStyleWithLineHeight:(CGFloat)lineHeight { return [self qmui_paragraphStyleWithLineHeight:lineHeight lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentLeft]; @@ -19,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 new file mode 100644 index 00000000..d9c0f072 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.h @@ -0,0 +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. + */ + +// +// NSPointerArray+QMUI.h +// QMUIKit +// +// Created by QMUI Team on 2018/4/12. +// + +#import + +@interface NSPointerArray (QMUI) + +- (NSUInteger)qmui_indexOfPointer:(nullable void *)pointer; +- (BOOL)qmui_containsPointer:(nullable void *)pointer; +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.m new file mode 100644 index 00000000..e7cba777 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSPointerArray+QMUI.m @@ -0,0 +1,60 @@ +/** + * 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. + */ + +// +// NSPointerArray+QMUI.m +// QMUIKit +// +// Created by QMUI Team on 2018/4/12. +// + +#import "NSPointerArray+QMUI.h" +#import "QMUICore.h" + +@implementation NSPointerArray (QMUI) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ExtendImplementationOfNonVoidMethodWithoutArguments([NSPointerArray class], @selector(description), NSString *, ^NSString *(NSPointerArray *selfObject, NSString *originReturnValue) { + NSMutableString *result = [[NSMutableString alloc] initWithString:originReturnValue]; + NSPointerArray *array = [selfObject copy]; + for (NSInteger i = 0; i < array.count; i++) { + ([result appendFormat:@"\npointer[%@] is %@", @(i), [array pointerAtIndex:i]]); + } + return result; + }); + }); +} + + +- (NSUInteger)qmui_indexOfPointer:(nullable void *)pointer { + if (!pointer) { + return NSNotFound; + } + + NSPointerArray *array = [self copy]; + for (NSUInteger i = 0; i < array.count; i++) { + if ([array pointerAtIndex:i] == ((void *)pointer)) { + return i; + } + } + return NSNotFound; +} + +- (BOOL)qmui_containsPointer:(void *)pointer { + if (!pointer) { + return NO; + } + if ([self qmui_indexOfPointer:pointer] != NSNotFound) { + return YES; + } + return NO; +} + +@end 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/UIKitExtensions/NSShadow+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSShadow+QMUI.m new file mode 100644 index 00000000..7374dc78 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSShadow+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. + */ +// +// NSShadow+QMUI.m +// QMUIKit +// +// Created by molice on 2022/9/6. +// + +#import "NSShadow+QMUI.h" + +@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 3f1f7754..38a3facd 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.h @@ -1,52 +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. + */ + // // NSString+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import -#import - -@interface NSString (QMUI) - -/// 判断是否包含某个子字符串 -- (BOOL)qmui_includesString:(NSString *)string; +#import -/// 去掉头尾的空白字符 -- (NSString *)qmui_trim; +NS_ASSUME_NONNULL_BEGIN -/// 去掉整段文字内的所有空白字符(包括换行符) -- (NSString *)qmui_trimAllWhiteSpace; - -/// 将文字中的换行符替换为空格 -- (NSString *)qmui_trimLineBreakCharacter; - -/// 把该字符串转换为对应的 md5 -- (NSString *)qmui_md5; - -/// 把某个十进制数字转换成十六进制的数字的字符串,例如“10”->“A” -+ (NSString *)qmui_hexStringWithInteger:(NSInteger)integer; - -/// 把参数列表拼接成一个字符串并返回,相当于用另一种语法来代替 [NSString stringWithFormat:] -+ (NSString *)qmui_stringByConcat:(id)firstArgv, ...; - -/** - * 将秒数转换为同时包含分钟和秒数的格式的字符串,例如 100->"01:40" - */ -+ (NSString *)qmui_timeStringWithMinsAndSecsFromSecs:(double)seconds; - -/** - * 用正则表达式匹配的方式去除字符串里一些特殊字符,避免UI上的展示问题 - * @link http://www.croton.su/en/uniblock/Diacriticals.html - */ -- (NSString *)qmui_removeMagicalChar; +@protocol QMUIStringProtocol /** * 按照中文 2 个字符、英文 1 个字符的方式来计算文本长度 */ -- (NSUInteger)qmui_lengthWhenCountingNonASCIICharacterAsTwo; +@property(readonly) NSUInteger qmui_lengthWhenCountingNonASCIICharacterAsTwo; /** * 将字符串从指定的 index 开始裁剪到结尾,裁剪时会避免将 emoji 等 "character sequences" 拆散(一个 emoji 表情占用1-4个长度的字符)。 @@ -54,36 +31,37 @@ * 例如对于字符串“😊😞”,它的长度为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个长度的字符)。 * - * 例如对于字符串“😊😞”,它的长度为4,若调用 [string qmui_substringAvoidBreakingUpCharacterSequencesToIndex:1],将返回“😊”。 + * 例如对于字符串“😊😞”,它的长度为4,若调用 [string qmui_substringAvoidBreakingUpCharacterSequencesToIndex:1 lessValue:NO countingNonASCIICharacterAsTwo:NO],将返回“😊”。 * 若调用系统的 [string substringToIndex:1],将返回“?”。(?表示乱码,因为第一个 emoji 表情被从中间裁开了)。 * - * @param index 要裁剪到哪个 index - * @return 裁剪完的字符 + * @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个长度的字符)。 @@ -92,35 +70,117 @@ * 在非 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; + +/** + 用正则表达式匹配字符串,将匹配到的第一个结果返回,大小写不敏感 + + @param pattern 正则表达式 + @return 匹配到的第一个结果,如果没有匹配成功则返回 nil + */ +- (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; + +/** + * 用正则表达式匹配字符串并将其替换为指定的另一个字符串,大小写不敏感 + * @param pattern 正则表达式 + * @param replacement 要替换为的字符串 + * @return 最终替换后的完整字符串,如果正则表达式匹配不成功则返回原字符串 + */ +- (NSString *)qmui_stringByReplacingPattern:(NSString *)pattern withString:(NSString *)replacement; + +/// 把某个十进制数字转换成十六进制的数字的字符串,例如“10”->“A” ++ (NSString *)qmui_hexStringWithInteger:(NSInteger)integer; + +/// 把参数列表拼接成一个字符串并返回,相当于用另一种语法来代替 [NSString stringWithFormat:] ++ (NSString *)qmui_stringByConcat:(id)firstArgv, ...; + +/** + * 将秒数转换为同时包含分钟和秒数的格式的字符串,例如 100->"01:40" + */ ++ (NSString *)qmui_timeStringWithMinsAndSecsFromSecs:(double)seconds; @end @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 1971eb92..b2fa90ad 100644 --- a/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/NSString+QMUI.m @@ -1,38 +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. + */ + // // NSString+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import "NSString+QMUI.h" #import -#import - -#define MD5_CHAR_TO_STRING_16 [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], \ -result[4], result[5], result[6], result[7], \ -result[8], result[9], result[10], result[11], \ -result[12], result[13], result[14], result[15]] \ +#import "QMUICore.h" +#import "NSArray+QMUI.h" +#import "NSCharacterSet+QMUI.h" +#import "QMUIStringPrivate.h" +#import "NSRegularExpression+QMUI.h" @implementation NSString (QMUI) -- (BOOL)qmui_includesString:(NSString *)string { - if (!string || string.length <= 0) { - return NO; +- (NSArray *)qmui_toArray { + if (!self.length) { + return nil; } - if ([self respondsToSelector:@selector(containsString:)]) { - return [self containsString:string]; + NSMutableArray *array = [[NSMutableArray alloc] init]; + for (NSInteger i = 0; i < self.length; i++) { + NSString *stringItem = [self substringWithRange:NSMakeRange(i, 1)]; + [array addObject:stringItem]; } - - return [self rangeOfString:string].location != NSNotFound; + return [array copy]; +} + +- (NSArray *)qmui_toTrimmedArray { + return [[self qmui_toArray] qmui_filterWithBlock:^BOOL(NSString *item) { + return item.qmui_trim.length > 0; + }]; } - (NSString *)qmui_trim { - return [self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSMutableCharacterSet * characterSet = [NSMutableCharacterSet whitespaceAndNewlineCharacterSet]; + [characterSet addCharactersInString:@"\0"]; + return [self stringByTrimmingCharactersInSet:characterSet]; } - (NSString *)qmui_trimAllWhiteSpace { @@ -46,12 +59,34 @@ - (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); - return MD5_CHAR_TO_STRING_16; + 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], + result[4], result[5], result[6], result[7], + result[8], result[9], result[10], result[11], + result[12], result[13], result[14], result[15]]; +} + +- (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) { @@ -124,114 +159,95 @@ - (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 characterLength = 0; - char *p = (char *)[self cStringUsingEncoding:NSUnicodeStringEncoding]; - for (NSInteger i = 0, l = [self lengthOfBytesUsingEncoding:NSUnicodeStringEncoding]; i < l; i++) { - if (*p) { - characterLength++; - } - p++; +- (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 characterLength; + return nil; } -- (NSUInteger)transformIndexToDefaultModeWithIndex:(NSUInteger)index { - CGFloat strlength = 0.f; - NSInteger 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); - NSInteger 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; -} - -- (NSString *)qmui_stringByRemoveCharacterAtIndex:(NSUInteger)index { - NSRange rangeForRemove = [self rangeOfComposedCharacterSequenceAtIndex:index]; - NSString *resultString = [self stringByReplacingCharactersInRange:rangeForRemove withString:@""]; - return resultString; +- (instancetype)qmui_stringByRemoveCharacterAtIndex:(NSUInteger)index { + return [QMUIStringPrivate string:self avoidBreakingUpCharacterSequencesByRemoveCharacterAtIndex:index]; } -- (NSString *)qmui_stringByRemoveLastCharacter { +- (instancetype)qmui_stringByRemoveLastCharacter { return [self qmui_stringByRemoveCharacterAtIndex:self.length - 1]; } @@ -239,15 +255,15 @@ - (NSString *)qmui_stringByRemoveLastCharacter { @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 new file mode 100644 index 00000000..a6e6c43a --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.h @@ -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. + */ + +// +// NSURL+QMUI.h +// QMUIKit +// +// Created by QMUI Team on 2018/11/11. +// + +#import + +@interface NSURL (QMUI) + +/** + * 获取当前 query 的参数列表。 + * + * @return query 参数列表,以字典返回。如果 absoluteString 为 nil 则返回 nil + */ +@property(nonatomic, copy, readonly) NSDictionary *qmui_queryItems; + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.m new file mode 100644 index 00000000..811d6fd2 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/NSURL+QMUI.m @@ -0,0 +1,36 @@ +/** + * 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. + */ + +// +// NSURL+QMUI.m +// QMUIKit +// +// Created by QMUI Team on 2018/11/11. +// + +#import "NSURL+QMUI.h" + +@implementation NSURL (QMUI) + +- (NSDictionary *)qmui_queryItems { + if (!self.absoluteString.length) { + return nil; + } + + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithString:self.absoluteString]; + + [urlComponents.queryItems enumerateObjectsUsingBlock:^(NSURLQueryItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (obj.name) { + [params setObject:obj.value ?: @"" forKey:obj.name]; + } + }]; + return [params copy]; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUIAlertController.h b/QMUI/QMUIKit/UIKitExtensions/QMUIAlertController.h deleted file mode 100644 index 6f1208d8..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUIAlertController.h +++ /dev/null @@ -1,252 +0,0 @@ -// -// QMUIAlertController.h -// qmui -// -// Created by QQMail on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import - -@class QMUIModalPresentationViewController; -@class QMUIButton; -@class QMUITextField; -@class QMUIAlertController; - - -typedef NS_ENUM(NSInteger, QMUIAlertActionStyle) { - QMUIAlertActionStyleDefault = 0, - QMUIAlertActionStyleCancel, - QMUIAlertActionStyleDestructive -}; - -typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) { - QMUIAlertControllerStyleActionSheet = 0, - QMUIAlertControllerStyleAlert -}; - - -@protocol QMUIAlertControllerDelegate - -- (void)willShowAlertController:(QMUIAlertController *)alertController; -- (void)willHideAlertController:(QMUIAlertController *)alertController; -- (void)didShowAlertController:(QMUIAlertController *)alertController; -- (void)didHideAlertController:(QMUIAlertController *)alertController; - -@end - - -/** - * QMUIAlertController的按钮,初始化完通过`QMUIAlertController`的`addAction:`方法添加到 AlertController 上即可。 - */ -@interface QMUIAlertAction : NSObject - -/** - * 初始化`QMUIAlertController`的按钮 - * - * @param title 按钮标题 - * @param style 按钮style,跟系统一样,有 Default、Cancel、Destructive 三种类型 - * @param handler 处理点击时间的block - * - * @return QMUIAlertController按钮的实例 - */ -+ (instancetype)actionWithTitle:(NSString *)title style:(QMUIAlertActionStyle)style handler:(void (^)(QMUIAlertAction *action))handler; - -/// `QMUIAlertAction`对应的 button 对象 -@property(nonatomic, strong, readonly) QMUIButton *button; - -/// `QMUIAlertAction`对应的标题 -@property(nonatomic, copy, readonly) NSString *title; - -/// `QMUIAlertAction`对应的样式 -@property(nonatomic, assign, readonly) QMUIAlertActionStyle style; - -/// `QMUIAlertAction`是否允许操作 -@property(nonatomic, assign, getter=isEnabled) BOOL enabled; - -/// `QMUIAlertAction`按钮样式,默认nil。当此值为nil的时候,则使用`QMUIAlertController`的`alertButtonAttributes`或者`sheetButtonAttributes`的值。 -@property(nonatomic, strong) NSDictionary *buttonAttributes; - -/// 原理同上`buttonAttributes` -@property(nonatomic, strong) NSDictionary *buttonDisabledAttributes; - -@end - - -/** - * `QMUIAlertController`是模仿系统`UIAlertController`的控件,所以系统有的功能在QMUIAlertController里面基本都有。同时`QMUIAlertController`还提供了一些扩展功能,例如:它的每个 button 都是开放出来的,可以对默认的按钮进行二次处理(比如加一个图片);可以通过 appearance 在 app 启动的时候修改整个`QMUIAlertController`的主题样式。 - */ -@interface QMUIAlertController : UIViewController - -/// alert距离屏幕四边的间距,默认UIEdgeInsetsMake(0, 0, 0, 0)。alert的宽度最终是通过屏幕宽度减去水平的 alertContentMargin 和 alertContentMaximumWidth 决定的。 -@property(nonatomic, assign) UIEdgeInsets alertContentMargin UI_APPEARANCE_SELECTOR; - -/// alert的最大宽度,默认270。 -@property(nonatomic, assign) CGFloat alertContentMaximumWidth UI_APPEARANCE_SELECTOR; - -/// alert上分隔线颜色,默认UIColorMake(211, 211, 219)。 -@property(nonatomic, strong) UIColor *alertSeperatorColor UI_APPEARANCE_SELECTOR; - -/// alert标题样式,默认@{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontBoldMake(17),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]} -@property(nonatomic, strong) NSDictionary *alertTitleAttributes UI_APPEARANCE_SELECTOR; - -/// alert信息样式,默认@{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]} -@property(nonatomic, strong) NSDictionary *alertMessageAttributes UI_APPEARANCE_SELECTOR; - -/// alert按钮样式,默认@{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)} -@property(nonatomic, strong) NSDictionary *alertButtonAttributes UI_APPEARANCE_SELECTOR; - -/// alert按钮disabled时的样式,默认@{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)} -@property(nonatomic, strong) NSDictionary *alertButtonDisabledAttributes UI_APPEARANCE_SELECTOR; - -/// alert cancel 按钮样式,默认@{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(17),NSKernAttributeName:@(0)} -@property(nonatomic, strong) NSDictionary *alertCancelButtonAttributes UI_APPEARANCE_SELECTOR; - -/// alert destructive 按钮样式,默认@{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(17),NSKernAttributeName:@(0)} -@property(nonatomic, strong) NSDictionary *alertDestructiveButtonAttributes UI_APPEARANCE_SELECTOR; - -/// alert圆角大小,默认值是:IOS_VERSION >= 9.0 ? 13 : 6,以保持与系统默认样式一致 -@property(nonatomic, assign) CGFloat alertContentCornerRadius UI_APPEARANCE_SELECTOR; - -/// alert按钮高度,默认44pt -@property(nonatomic, assign) CGFloat alertButtonHeight UI_APPEARANCE_SELECTOR; - -/// alert头部(非按钮部分)背景色,默认值是:(IOS_VERSION < 8.0) ? UIColorWhite : UIColorMakeWithRGBA(247, 247, 247, 1) -@property(nonatomic, strong) UIColor *alertHeaderBackgroundColor UI_APPEARANCE_SELECTOR; - -/// alert按钮背景色,默认值同`alertHeaderBackgroundColor` -@property(nonatomic, strong) UIColor *alertButtonBackgroundColor UI_APPEARANCE_SELECTOR; - -/// alert按钮高亮背景色,默认UIColorMake(232, 232, 232) -@property(nonatomic, strong) UIColor *alertButtonHighlightBackgroundColor UI_APPEARANCE_SELECTOR; - -/// alert头部四边insets间距 -@property(nonatomic, assign) UIEdgeInsets alertHeaderInsets UI_APPEARANCE_SELECTOR; - -/// alert头部title和message之间的间距,默认3pt -@property(nonatomic, assign) CGFloat alertTitleMessageSpacing UI_APPEARANCE_SELECTOR; - - -/// sheet距离屏幕四边的间距,默认UIEdgeInsetsMake(10, 10, 10, 10)。 -@property(nonatomic, assign) UIEdgeInsets sheetContentMargin UI_APPEARANCE_SELECTOR; - -/// sheet的最大宽度,默认值是5.5英寸的屏幕的宽度减去水平的 sheetContentMargin -@property(nonatomic, assign) CGFloat sheetContentMaximumWidth UI_APPEARANCE_SELECTOR; - -/// sheet分隔线颜色,默认UIColorMake(211, 211, 219) -@property(nonatomic, strong) UIColor *sheetSeperatorColor UI_APPEARANCE_SELECTOR; - -/// sheet标题样式,默认@{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontBoldMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]} -@property(nonatomic, strong) NSDictionary *sheetTitleAttributes UI_APPEARANCE_SELECTOR; - -/// sheet信息样式,默认@{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]} -@property(nonatomic, strong) NSDictionary *sheetMessageAttributes UI_APPEARANCE_SELECTOR; - -/// sheet按钮样式,默认@{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)} -@property(nonatomic, strong) NSDictionary *sheetButtonAttributes UI_APPEARANCE_SELECTOR; - -/// sheet按钮disabled时的样式,默认@{NSForegroundColorAttributeName:UIColorMake(129, 129, 129),NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)} -@property(nonatomic, strong) NSDictionary *sheetButtonDisabledAttributes UI_APPEARANCE_SELECTOR; - -/// sheet cancel 按钮样式,默认@{NSForegroundColorAttributeName:UIColorBlue,NSFontAttributeName:UIFontBoldMake(20),NSKernAttributeName:@(0)} -@property(nonatomic, strong) NSDictionary *sheetCancelButtonAttributes UI_APPEARANCE_SELECTOR; - -/// sheet destructive 按钮样式,默认@{NSForegroundColorAttributeName:UIColorRed,NSFontAttributeName:UIFontMake(20),NSKernAttributeName:@(0)} -@property(nonatomic, strong) NSDictionary *sheetDestructiveButtonAttributes UI_APPEARANCE_SELECTOR; - -/// sheet cancel 按钮距离其上面元素(按钮或者header)的间距,默认8pt -@property(nonatomic, assign) CGFloat sheetCancelButtonMarginTop UI_APPEARANCE_SELECTOR; - -/// sheet内容的圆角,默认值是:(IOS_VERSION >= 9.0 ? 13 : 6),以保持与系统默认样式一致 -@property(nonatomic, assign) CGFloat sheetContentCornerRadius UI_APPEARANCE_SELECTOR; - -/// sheet按钮高度,默认值是:(IOS_VERSION >= 9.0 ? 57 : 44),以保持与系统默认样式一致 -@property(nonatomic, assign) CGFloat sheetButtonHeight UI_APPEARANCE_SELECTOR; - -/// sheet头部(非按钮部分)背景色,默认值是:(IOS_VERSION < 8.0) ? UIColorWhite : UIColorMakeWithRGBA(247, 247, 247, 1) -@property(nonatomic, strong) UIColor *sheetHeaderBackgroundColor UI_APPEARANCE_SELECTOR; - -/// sheet按钮背景色,默认值同`sheetHeaderBackgroundColor` -@property(nonatomic, strong) UIColor *sheetButtonBackgroundColor UI_APPEARANCE_SELECTOR; - -/// sheet按钮高亮背景色,默认UIColorMake(232, 232, 232) -@property(nonatomic, strong) UIColor *sheetButtonHighlightBackgroundColor UI_APPEARANCE_SELECTOR; - -/// sheet头部四边insets间距 -@property(nonatomic, assign) UIEdgeInsets sheetHeaderInsets UI_APPEARANCE_SELECTOR; - -/// sheet头部title和message之间的间距,默认8pt -@property(nonatomic, assign) CGFloat sheetTitleMessageSpacing UI_APPEARANCE_SELECTOR; - - -/// 默认初始化方法 -- (instancetype)initWithTitle:(NSString *)title message:(NSString *)message preferredStyle:(QMUIAlertControllerStyle)preferredStyle; - -/// 通过类方法初始化实例 -+ (instancetype)alertControllerWithTitle:(NSString *)title message:(NSString *)message preferredStyle:(QMUIAlertControllerStyle)preferredStyle; - -/// @see `QMUIAlertControllerDelegate` -@property(nonatomic,weak) iddelegate; - -/// 增加一个按钮 -- (void)addAction:(QMUIAlertAction *)action; - -/// 增加一个输入框 -- (void)addTextFieldWithConfigurationHandler:(void (^)(UITextField *textField))configurationHandler; - -/// 增加一个自定义的view作为`QMUIAlertController`的customView -- (void)addCustomView:(UIView *)view; - -/// 显示`QMUIAlertController` -- (void)showWithAnimated:(BOOL)animated; - -/// 隐藏`QMUIAlertController` -- (void)hideWithAnimated:(BOOL)animated; - -/// 所有`QMUIAlertAction`对象 -@property(nonatomic, copy, readonly) NSArray *actions; - -/// 当前所有通过`addTextFieldWithConfigurationHandler:`接口添加的输入框 -@property(nonatomic, copy, readonly) NSArray *textFields; - -/// 设置自定义view。通过`addCustomView:`方法添加一个自定义的view,`QMUIAlertController`会在布局的时候去掉用这个view的`sizeThatFits:`方法来获取size,至于x和y坐标则由控件自己控制。 -@property(nonatomic, strong, readonly) UIView *customView; - -/// 当前标题title -@property(nonatomic, copy) NSString *title; - -/// 当前信息message -@property(nonatomic, copy) NSString *message; - -/// 当前样式style -@property(nonatomic, assign, readonly) QMUIAlertControllerStyle preferredStyle; - -/// 将`QMUIAlertController`弹出来的`QMUIModalPresentationViewController`对象 -@property(nonatomic, strong, readonly) QMUIModalPresentationViewController *modalPresentationViewController; - -/** - * 设置按钮的排序是否要由用户添加的顺序来决定,默认为NO,也即与系统原生`UIAlertController`一致,QMUIAlertActionStyleDestructive 类型的action必定在最后面。 - * - * @warning 注意 QMUIAlertActionStyleCancel 按钮不受这个属性的影响 - */ -@property(nonatomic, assign) BOOL orderActionsByAddedOrdered; - -/// maskView是否响应点击,alert默认为NO,sheet默认为YES -@property(nonatomic, assign) BOOL shouldRespondMaskViewTouch; - -@end - - -@interface QMUIAlertController (UIAppearance) - -+ (instancetype)appearance; - -@end - - -@interface QMUIAlertController (Manager) - -/// 可方便地判断是否有 alertController 正在显示,全局生效 -+ (BOOL)isAnyAlertControllerVisible; - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUIAlertController.m b/QMUI/QMUIKit/UIKitExtensions/QMUIAlertController.m deleted file mode 100644 index bebb6b10..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUIAlertController.m +++ /dev/null @@ -1,1069 +0,0 @@ -// -// QMUIAlertController.m -// qmui -// -// Created by QQMail on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUIAlertController.h" -#import "QMUICore.h" -#import "QMUIModalPresentationViewController.h" -#import "QMUIButton.h" -#import "QMUITextField.h" -#import "UIView+QMUI.h" -#import "UIControl+QMUI.h" -#import "NSParagraphStyle+QMUI.h" -#import "UIImage+QMUI.h" - -static NSUInteger alertControllerCount = 0; - -#pragma mark - QMUIBUttonWrapView - -@interface QMUIAlertButtonWrapView : UIView - -@property(nonatomic, strong) QMUIButton *button; - -@end - -@implementation QMUIAlertButtonWrapView - -- (instancetype)init { - self = [super init]; - if (self) { - self.button = [[QMUIButton alloc] init]; - self.button.adjustsButtonWhenDisabled = NO; - self.button.adjustsButtonWhenHighlighted = NO; - [self addSubview:self.button]; - } - return self; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - self.button.frame = self.bounds; -} - -@end - - -#pragma mark - QMUIAlertAction - -@protocol QMUIAlertActionDelegate - -- (void)didClickAlertAction:(QMUIAlertAction *)alertAction; - -@end - -@interface QMUIAlertAction () - -@property(nonatomic, strong) QMUIAlertButtonWrapView *buttonWrapView; -@property(nonatomic, copy, readwrite) NSString *title; -@property(nonatomic, assign, readwrite) QMUIAlertActionStyle style; -@property(nonatomic, copy) void (^handler)(QMUIAlertAction *action); -@property(nonatomic, weak) id delegate; - -@end - -@implementation QMUIAlertAction - -+ (instancetype)actionWithTitle:(NSString *)title style:(QMUIAlertActionStyle)style handler:(void (^)(QMUIAlertAction *action))handler { - QMUIAlertAction *alertAction = [[QMUIAlertAction alloc] init]; - alertAction.title = title; - alertAction.style = style; - alertAction.handler = handler; - return alertAction; -} - -- (instancetype)init { - self = [super init]; - if (self) { - self.buttonWrapView = [[QMUIAlertButtonWrapView alloc] init]; - self.button.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; - [self.button addTarget:self action:@selector(handleAlertActionEvent:) forControlEvents:UIControlEventTouchUpInside]; - } - return self; -} - -- (QMUIButton *)button { - return self.buttonWrapView.button; -} - -- (void)setEnabled:(BOOL)enabled { - _enabled = enabled; - self.button.enabled = enabled; -} - -- (void)handleAlertActionEvent:(id)sender { - // 需要先调delegate,里面会先恢复keywindow - if (self.delegate && [self.delegate respondsToSelector:@selector(didClickAlertAction:)]) { - [self.delegate didClickAlertAction:self]; - } - // 再调block回调 - if (self.handler) { - self.handler(self); - } -} - -@end - - -@implementation QMUIAlertController (UIAppearance) - -+ (void)initialize { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self appearance]; - }); -} - -static QMUIAlertController *alertControllerAppearance; -+ (instancetype)appearance { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self resetAppearance]; - }); - return alertControllerAppearance; -} - -+ (void)resetAppearance { - if (!alertControllerAppearance) { - - alertControllerAppearance = [[QMUIAlertController alloc] init]; - - alertControllerAppearance.alertContentMargin = UIEdgeInsetsMake(0, 0, 0, 0); - alertControllerAppearance.alertContentMaximumWidth = 270; - alertControllerAppearance.alertSeperatorColor = UIColorMake(211, 211, 219); - alertControllerAppearance.alertTitleAttributes = @{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontBoldMake(17),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]}; - alertControllerAppearance.alertMessageAttributes = @{NSForegroundColorAttributeName:UIColorBlack,NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]}; - 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 = (IOS_VERSION >= 9.0 ? 13 : 6); - alertControllerAppearance.alertButtonHeight = 44; - alertControllerAppearance.alertHeaderBackgroundColor = (IOS_VERSION < 8.0) ? UIColorWhite : 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.sheetContentMargin = UIEdgeInsetsMake(10, 10, 10, 10); - alertControllerAppearance.sheetContentMaximumWidth = [QMUIHelper screenSizeFor55Inch].width - UIEdgeInsetsGetHorizontalValue(alertControllerAppearance.sheetContentMargin); - alertControllerAppearance.sheetSeperatorColor = UIColorMake(211, 211, 219); - alertControllerAppearance.sheetTitleAttributes = @{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontBoldMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]}; - alertControllerAppearance.sheetMessageAttributes = @{NSForegroundColorAttributeName:UIColorMake(143, 143, 143),NSFontAttributeName:UIFontMake(13),NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:0 lineBreakMode:NSLineBreakByTruncatingTail]}; - 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 = (IOS_VERSION >= 9.0 ? 13 : 6); - alertControllerAppearance.sheetButtonHeight = (IOS_VERSION >= 9.0 ? 57 : 44); - alertControllerAppearance.sheetHeaderBackgroundColor = (IOS_VERSION < 8.0) ? UIColorWhite : 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; - } -} - -@end - - -#pragma mark - QMUIAlertController - -@interface QMUIAlertController () - -@property(nonatomic, assign, readwrite) QMUIAlertControllerStyle preferredStyle; -@property(nonatomic, strong, readwrite) QMUIModalPresentationViewController *modalPresentationViewController; - -@property(nonatomic, strong) UIView *containerView; -@property(nonatomic, strong) UIControl *maskView; - -@property(nonatomic, strong) UIView *scrollWrapView; -@property(nonatomic, strong) UIScrollView *headerScrollView; -@property(nonatomic, strong) UIScrollView *buttonScrollView; - -@property(nonatomic, strong) UIView *headerEffectView; -@property(nonatomic, strong) UIView *cancelButtoneEffectView; - -@property(nonatomic, strong) UILabel *titleLabel; -@property(nonatomic, strong) UILabel *messageLabel; -@property(nonatomic, strong) QMUIAlertAction *cancelAction; - -@property(nonatomic, strong) NSMutableArray *alertActions; -@property(nonatomic, strong) NSMutableArray *destructiveActions; -@property(nonatomic, strong) NSMutableArray *alertTextFields; - -@property(nonatomic, assign) CGFloat keyboardHeight; -@property(nonatomic, assign) BOOL isShowing; - -@end - -@implementation QMUIAlertController { - NSString *_title; - BOOL _needsUpdateAction; - BOOL _needsUpdateTitle; - BOOL _needsUpdateMessage; -} - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - if (alertControllerAppearance) { - self.alertContentMargin = [QMUIAlertController appearance].alertContentMargin; - self.alertContentMaximumWidth = [QMUIAlertController appearance].alertContentMaximumWidth; - self.alertSeperatorColor = [QMUIAlertController appearance].alertSeperatorColor; - 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.sheetContentMargin = [QMUIAlertController appearance].sheetContentMargin; - self.sheetContentMaximumWidth = [QMUIAlertController appearance].sheetContentMaximumWidth; - self.sheetSeperatorColor = [QMUIAlertController appearance].sheetSeperatorColor; - 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; - } -} - -- (void)setAlertButtonAttributes:(NSDictionary *)alertButtonAttributes { - _alertButtonAttributes = alertButtonAttributes; - _needsUpdateAction = YES; -} - -- (void)setSheetButtonAttributes:(NSDictionary *)sheetButtonAttributes { - _sheetButtonAttributes = sheetButtonAttributes; - _needsUpdateAction = YES; -} - -- (void)setAlertButtonDisabledAttributes:(NSDictionary *)alertButtonDisabledAttributes { - _alertButtonDisabledAttributes = alertButtonDisabledAttributes; - _needsUpdateAction = YES; -} - -- (void)setSheetButtonDisabledAttributes:(NSDictionary *)sheetButtonDisabledAttributes { - _sheetButtonDisabledAttributes = sheetButtonDisabledAttributes; - _needsUpdateAction = YES; -} - -- (void)setAlertCancelButtonAttributes:(NSDictionary *)alertCancelButtonAttributes { - _alertCancelButtonAttributes = alertCancelButtonAttributes; - _needsUpdateAction = YES; -} - -- (void)setSheetCancelButtonAttributes:(NSDictionary *)sheetCancelButtonAttributes { - _sheetCancelButtonAttributes = sheetCancelButtonAttributes; - _needsUpdateAction = YES; -} - -- (void)setAlertDestructiveButtonAttributes:(NSDictionary *)alertDestructiveButtonAttributes { - _alertDestructiveButtonAttributes = alertDestructiveButtonAttributes; - _needsUpdateAction = YES; -} - -- (void)setSheetDestructiveButtonAttributes:(NSDictionary *)sheetDestructiveButtonAttributes { - _sheetDestructiveButtonAttributes = sheetDestructiveButtonAttributes; - _needsUpdateAction = YES; -} - -- (void)setAlertButtonBackgroundColor:(UIColor *)alertButtonBackgroundColor { - _alertButtonBackgroundColor = alertButtonBackgroundColor; - _needsUpdateAction = YES; -} - -- (void)setSheetButtonBackgroundColor:(UIColor *)sheetButtonBackgroundColor { - _sheetButtonBackgroundColor = sheetButtonBackgroundColor; - _needsUpdateAction = YES; -} - -- (void)setAlertButtonHighlightBackgroundColor:(UIColor *)alertButtonHighlightBackgroundColor { - _alertButtonHighlightBackgroundColor = alertButtonHighlightBackgroundColor; - _needsUpdateAction = YES; -} - -- (void)setSheetButtonHighlightBackgroundColor:(UIColor *)sheetButtonHighlightBackgroundColor { - _sheetButtonHighlightBackgroundColor = sheetButtonHighlightBackgroundColor; - _needsUpdateAction = YES; -} - -- (void)setAlertTitleAttributes:(NSDictionary *)alertTitleAttributes { - _alertTitleAttributes = alertTitleAttributes; - _needsUpdateTitle = YES; -} - -- (void)setAlertMessageAttributes:(NSDictionary *)alertMessageAttributes { - _alertMessageAttributes = alertMessageAttributes; - _needsUpdateMessage = YES; -} - -- (void)setSheetTitleAttributes:(NSDictionary *)sheetTitleAttributes { - _sheetTitleAttributes = sheetTitleAttributes; - _needsUpdateTitle = YES; -} - -- (void)setSheetMessageAttributes:(NSDictionary *)sheetMessageAttributes { - _sheetMessageAttributes = sheetMessageAttributes; - _needsUpdateMessage = YES; -} - -- (void)setAlertHeaderBackgroundColor:(UIColor *)alertHeaderBackgroundColor { - _alertHeaderBackgroundColor = alertHeaderBackgroundColor; - [self updateHeaderBackgrondColor]; -} - -- (void)setSheetHeaderBackgroundColor:(UIColor *)sheetHeaderBackgroundColor { - _sheetHeaderBackgroundColor = sheetHeaderBackgroundColor; - [self updateHeaderBackgrondColor]; -} - -- (void)updateHeaderBackgrondColor { - if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { - if (self.headerScrollView) { self.headerScrollView.backgroundColor = self.sheetHeaderBackgroundColor; } - } else if (self.preferredStyle == QMUIAlertControllerStyleAlert) { - if (self.headerScrollView) { self.headerScrollView.backgroundColor = self.alertHeaderBackgroundColor; } - } -} - -- (void)setAlertSeperatorColor:(UIColor *)alertSeperatorColor { - _alertSeperatorColor = alertSeperatorColor; - [self updateEffectBackgroundColor]; -} - -- (void)setSheetSeperatorColor:(UIColor *)sheetSeperatorColor { - _sheetSeperatorColor = sheetSeperatorColor; - [self updateEffectBackgroundColor]; -} - -- (void)updateEffectBackgroundColor { - if (self.preferredStyle == QMUIAlertControllerStyleAlert && self.alertSeperatorColor) { - if (self.headerEffectView) { self.headerEffectView.backgroundColor = self.alertSeperatorColor; } - if (self.cancelButtoneEffectView) { self.cancelButtoneEffectView.backgroundColor = self.alertSeperatorColor; } - } else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet && self.sheetSeperatorColor) { - if (self.headerEffectView) { self.headerEffectView.backgroundColor = self.sheetSeperatorColor; } - if (self.cancelButtoneEffectView) { self.cancelButtoneEffectView.backgroundColor = self.sheetSeperatorColor; } - } -} - -- (void)setAlertContentCornerRadius:(CGFloat)alertContentCornerRadius { - _alertContentCornerRadius = alertContentCornerRadius; - [self updateCornerRadius]; -} - -- (void)setSheetContentCornerRadius:(CGFloat)sheetContentCornerRadius { - _sheetContentCornerRadius = sheetContentCornerRadius; - [self updateCornerRadius]; -} - -- (void)updateCornerRadius { - if (self.preferredStyle == QMUIAlertControllerStyleAlert) { - if (self.containerView) { self.containerView.layer.cornerRadius = self.alertContentCornerRadius; self.containerView.clipsToBounds = YES; } - if (self.cancelButtoneEffectView) { self.cancelButtoneEffectView.layer.cornerRadius = 0; self.cancelButtoneEffectView.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; } - if (self.cancelButtoneEffectView) { self.cancelButtoneEffectView.layer.cornerRadius = self.sheetContentCornerRadius; self.cancelButtoneEffectView.clipsToBounds = YES; } - if (self.scrollWrapView) { self.scrollWrapView.layer.cornerRadius = self.sheetContentCornerRadius; self.scrollWrapView.clipsToBounds = YES; } - } -} - -+ (instancetype)alertControllerWithTitle:(NSString *)title message:(NSString *)message preferredStyle:(QMUIAlertControllerStyle)preferredStyle { - QMUIAlertController *alertController = [[self alloc] initWithTitle:title message:message preferredStyle:preferredStyle]; - if (alertController) { - return alertController; - } - return nil; -} - -- (instancetype)initWithTitle:(NSString *)title message:(NSString *)message preferredStyle:(QMUIAlertControllerStyle)preferredStyle { - self = [self init]; - if (self) { - - self.isShowing = NO; - self.shouldRespondMaskViewTouch = 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.headerEffectView = [[UIView alloc] init]; - self.cancelButtoneEffectView = [[UIView alloc] init]; - self.headerScrollView = [[UIScrollView alloc] init]; - self.buttonScrollView = [[UIScrollView alloc] init]; - - self.title = title; - self.message = message; - self.preferredStyle = preferredStyle; - - [self updateHeaderBackgrondColor]; - [self updateEffectBackgroundColor]; - [self updateCornerRadius]; - - } - return self; -} - -- (void)setPreferredStyle:(QMUIAlertControllerStyle)preferredStyle { - _preferredStyle = IS_IPAD ? QMUIAlertControllerStyleAlert : preferredStyle; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - [self.view addSubview:self.maskView]; - [self.view addSubview:self.containerView]; - [self.containerView addSubview:self.scrollWrapView]; - [self.scrollWrapView addSubview:self.headerEffectView]; - [self.scrollWrapView addSubview:self.headerScrollView]; - [self.scrollWrapView addSubview:self.buttonScrollView]; -} - -- (void)viewDidLayoutSubviews { - - [super viewDidLayoutSubviews]; - - BOOL hasTitle = (self.titleLabel.text && ![self.titleLabel.text isEqualToString:@""] && !self.titleLabel.hidden); - BOOL hasMessage = (self.messageLabel.text && ![self.messageLabel.text isEqualToString:@""] && !self.messageLabel.hidden); - BOOL hasTextField = self.alertTextFields.count > 0; - BOOL hasCustomView = !!_customView; - CGFloat contentOriginY = 0; - - self.maskView.frame = self.view.bounds; - - if (self.preferredStyle == QMUIAlertControllerStyleAlert) { - - CGFloat contentPaddingLeft = self.alertHeaderInsets.left; - CGFloat contentPaddingRight = self.alertHeaderInsets.right; - - CGFloat contentPaddingTop = (hasTitle || hasMessage || hasTextField || hasCustomView) ? self.alertHeaderInsets.top : 0; - CGFloat contentPaddingBottom = (hasTitle || hasMessage || hasTextField || hasCustomView) ? self.alertHeaderInsets.bottom : 0; - [self.containerView qmui_setWidth:fmin(self.alertContentMaximumWidth, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(self.alertContentMargin))]; - [self.scrollWrapView qmui_setWidth:CGRectGetWidth(self.containerView.bounds)]; - self.headerScrollView.frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollWrapView.bounds), 0); - contentOriginY = contentPaddingTop; - // 标题和副标题布局 - if (hasTitle) { - CGFloat titleLabelLimitWidth = CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight; - CGSize titleLabelSize = [self.titleLabel sizeThatFits:CGSizeMake(titleLabelLimitWidth, CGFLOAT_MAX)]; - self.titleLabel.frame = CGRectFlatted(CGRectMake(contentPaddingLeft, contentOriginY, titleLabelLimitWidth, titleLabelSize.height)); - contentOriginY = CGRectGetMaxY(self.titleLabel.frame) + (hasMessage ? self.alertTitleMessageSpacing : contentPaddingBottom); - } - if (hasMessage) { - CGFloat messageLabelLimitWidth = CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight; - CGSize messageLabelSize = [self.messageLabel sizeThatFits:CGSizeMake(messageLabelLimitWidth, CGFLOAT_MAX)]; - self.messageLabel.frame = CGRectFlatted(CGRectMake(contentPaddingLeft, contentOriginY, messageLabelLimitWidth, messageLabelSize.height)); - contentOriginY = CGRectGetMaxY(self.messageLabel.frame) + contentPaddingBottom; - } - // 输入框布局 - 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; - } - contentOriginY += 16; - } - // 自定义view的布局 - 自动居中 - if (hasCustomView) { - CGSize customViewSize = [_customView sizeThatFits:CGSizeMake(CGRectGetWidth(self.headerScrollView.bounds), CGFLOAT_MAX)]; - _customView.frame = CGRectFlatted(CGRectMake((CGRectGetWidth(self.headerScrollView.bounds) - customViewSize.width) / 2, contentOriginY, customViewSize.width, customViewSize.height)); - contentOriginY = CGRectGetMaxY(_customView.frame) + contentPaddingBottom; - } - // 内容scrollView的布局 - self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, contentOriginY); - self.headerScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.headerScrollView.bounds), contentOriginY); - 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.alertActions.count > 0) { - BOOL verticalLayout = YES; - if (self.alertActions.count == 2) { - CGFloat halfWidth = CGRectGetWidth(self.buttonScrollView.bounds) / 2; - QMUIAlertAction *action1 = newOrderActions[0]; - QMUIAlertAction *action2 = newOrderActions[1]; - CGSize actionSize1 = [action1.button sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]; - CGSize actionSize2 = [action2.button sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]; - if (actionSize1.width < halfWidth && actionSize2.width < halfWidth) { - verticalLayout = NO; - } - } - if (!verticalLayout) { - QMUIAlertAction *action1 = newOrderActions[1]; - action1.buttonWrapView.frame = CGRectMake(0, contentOriginY + PixelOne, CGRectGetWidth(self.buttonScrollView.bounds) / 2, self.alertButtonHeight); - QMUIAlertAction *action2 = newOrderActions[0]; - action2.buttonWrapView.frame = CGRectMake(CGRectGetMaxX(action1.buttonWrapView.frame) + PixelOne, contentOriginY + PixelOne, CGRectGetWidth(self.buttonScrollView.bounds) / 2 - PixelOne, self.alertButtonHeight); - contentOriginY = CGRectGetMaxY(action1.buttonWrapView.frame); - } - else { - for (int i = 0; i < newOrderActions.count; i++) { - QMUIAlertAction *action = newOrderActions[i]; - action.buttonWrapView.frame = CGRectMake(0, contentOriginY + PixelOne, CGRectGetWidth(self.containerView.bounds), self.alertButtonHeight - PixelOne); - contentOriginY = CGRectGetMaxY(action.buttonWrapView.frame); - } - } - } - // 按钮scrollView的布局 - self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, contentOriginY); - 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); - if (contentHeight > screenSpaceHeight - 20) { - screenSpaceHeight -= 20; - CGFloat contentH = fminf(CGRectGetHeight(self.headerScrollView.bounds), screenSpaceHeight / 2); - CGFloat buttonH = fminf(CGRectGetHeight(self.buttonScrollView.bounds), screenSpaceHeight / 2); - if (contentH >= screenSpaceHeight / 2 && buttonH >= screenSpaceHeight / 2) { - self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, screenSpaceHeight / 2); - self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); - self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, screenSpaceHeight / 2); - } else if (contentH < screenSpaceHeight / 2) { - self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, contentH); - self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); - self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, screenSpaceHeight - contentH); - } else if (buttonH < screenSpaceHeight / 2) { - self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, screenSpaceHeight - buttonH); - self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); - self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, buttonH); - } - contentHeight = CGRectGetHeight(self.headerScrollView.bounds) + CGRectGetHeight(self.buttonScrollView.bounds); - screenSpaceHeight += 20; - } - self.scrollWrapView.frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollWrapView.bounds), contentHeight); - self.headerEffectView.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)); - } - - else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { - - CGFloat contentPaddingLeft = self.alertHeaderInsets.left; - CGFloat contentPaddingRight = self.alertHeaderInsets.right; - - CGFloat contentPaddingTop = (hasTitle || hasMessage || hasTextField) ? self.sheetHeaderInsets.top : 0; - CGFloat contentPaddingBottom = (hasTitle || hasMessage || hasTextField) ? self.sheetHeaderInsets.bottom : 0; - [self.containerView qmui_setWidth:fmin(self.sheetContentMaximumWidth, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(self.sheetContentMargin))]; - [self.scrollWrapView qmui_setWidth:CGRectGetWidth(self.containerView.bounds)]; - self.headerScrollView.frame = CGRectMake(0, 0, CGRectGetWidth(self.containerView.bounds), 0); - contentOriginY = contentPaddingTop; - // 标题和副标题布局 - if (hasTitle) { - CGFloat titleLabelLimitWidth = CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight; - CGSize titleLabelSize = [self.titleLabel sizeThatFits:CGSizeMake(titleLabelLimitWidth, CGFLOAT_MAX)]; - self.titleLabel.frame = CGRectFlatted(CGRectMake(contentPaddingLeft, contentOriginY, titleLabelLimitWidth, titleLabelSize.height)); - contentOriginY = CGRectGetMaxY(self.titleLabel.frame) + (hasMessage ? self.sheetTitleMessageSpacing : contentPaddingBottom); - } - if (hasMessage) { - CGFloat messageLabelLimitWidth = CGRectGetWidth(self.headerScrollView.bounds) - contentPaddingLeft - contentPaddingRight; - CGSize messageLabelSize = [self.messageLabel sizeThatFits:CGSizeMake(messageLabelLimitWidth, CGFLOAT_MAX)]; - self.messageLabel.frame = CGRectFlatted(CGRectMake(contentPaddingLeft, contentOriginY, messageLabelLimitWidth, messageLabelSize.height)); - contentOriginY = CGRectGetMaxY(self.messageLabel.frame) + contentPaddingBottom; - } - // 自定义view的布局 - 自动居中 - if (hasCustomView) { - CGSize customViewSize = [_customView sizeThatFits:CGSizeMake(CGRectGetWidth(self.headerScrollView.bounds), CGFLOAT_MAX)]; - _customView.frame = CGRectFlatted(CGRectMake((CGRectGetWidth(self.headerScrollView.bounds) - customViewSize.width) / 2, contentOriginY, customViewSize.width, customViewSize.height)); - contentOriginY = CGRectGetMaxY(_customView.frame) + contentPaddingBottom; - } - // 内容scrollView布局 - self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, contentOriginY); - self.headerScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.headerScrollView.bounds), contentOriginY); - 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.alertActions.count > 0) { - contentOriginY = (hasTitle || hasMessage || hasCustomView) ? contentOriginY + PixelOne : contentOriginY; - for (int i = 0; i < newOrderActions.count; i++) { - QMUIAlertAction *action = newOrderActions[i]; - if (action.style == QMUIAlertActionStyleCancel && i == newOrderActions.count - 1) { - continue; - } else { - action.buttonWrapView.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.buttonScrollView.bounds), self.sheetButtonHeight - PixelOne); - contentOriginY = CGRectGetMaxY(action.buttonWrapView.frame) + PixelOne; - } - } - contentOriginY -= PixelOne; - } - // 按钮scrollView布局 - self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, contentOriginY); - self.buttonScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.buttonScrollView.bounds), contentOriginY); - // 容器最终布局 - self.scrollWrapView.frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollWrapView.bounds), CGRectGetMaxY(self.buttonScrollView.frame)); - self.headerEffectView.frame = self.scrollWrapView.bounds; - contentOriginY = CGRectGetMaxY(self.scrollWrapView.frame) + self.sheetCancelButtonMarginTop; - if (self.cancelAction) { - self.cancelButtoneEffectView.frame = CGRectMake(0, contentOriginY, CGRectGetWidth(self.containerView.bounds), self.sheetButtonHeight); - self.cancelAction.buttonWrapView.frame = self.cancelButtoneEffectView.bounds; - contentOriginY = CGRectGetMaxY(self.cancelButtoneEffectView.frame); - } - // 把上下的margin都加上用于跟整个屏幕的高度做比较 - CGFloat contentHeight = contentOriginY + UIEdgeInsetsGetVerticalValue(self.sheetContentMargin); - CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds); - if (contentHeight > screenSpaceHeight) { - CGFloat cancelButtonAreaHeight = (self.cancelAction ? (CGRectGetHeight(self.cancelAction.buttonWrapView.bounds) + self.sheetCancelButtonMarginTop) : 0); - screenSpaceHeight = screenSpaceHeight - cancelButtonAreaHeight - UIEdgeInsetsGetVerticalValue(self.sheetContentMargin); - CGFloat contentH = MIN(CGRectGetHeight(self.headerScrollView.bounds), screenSpaceHeight / 2); - CGFloat buttonH = MIN(CGRectGetHeight(self.buttonScrollView.bounds), screenSpaceHeight / 2); - if (contentH >= screenSpaceHeight / 2 && buttonH >= screenSpaceHeight / 2) { - self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, screenSpaceHeight / 2); - self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); - self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, screenSpaceHeight / 2); - } else if (contentH < screenSpaceHeight / 2) { - self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, contentH); - self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); - self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, screenSpaceHeight - contentH); - } else if (buttonH < screenSpaceHeight / 2) { - self.headerScrollView.frame = CGRectSetHeight(self.headerScrollView.frame, screenSpaceHeight - buttonH); - self.buttonScrollView.frame = CGRectSetY(self.buttonScrollView.frame, CGRectGetMaxY(self.headerScrollView.frame)); - self.buttonScrollView.frame = CGRectSetHeight(self.buttonScrollView.frame, buttonH); - } - self.scrollWrapView.frame = CGRectSetHeight(self.scrollWrapView.frame, CGRectGetHeight(self.headerScrollView.bounds) + CGRectGetHeight(self.buttonScrollView.bounds)); - if (self.cancelAction) { - self.cancelButtoneEffectView.frame = CGRectSetY(self.cancelButtoneEffectView.frame, CGRectGetMaxY(self.scrollWrapView.frame) + self.sheetCancelButtonMarginTop); - } - contentHeight = CGRectGetHeight(self.headerScrollView.bounds) + CGRectGetHeight(self.buttonScrollView.bounds) + cancelButtonAreaHeight + self.sheetContentMargin.bottom; - screenSpaceHeight += (cancelButtonAreaHeight + UIEdgeInsetsGetVerticalValue(self.sheetContentMargin)); - } else { - // 如果小于屏幕高度,则把顶部的top减掉 - contentHeight -= self.sheetContentMargin.top; - } - - CGRect containerRect = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.bounds)) / 2, screenSpaceHeight - contentHeight, CGRectGetWidth(self.containerView.bounds), contentHeight); - self.containerView.frame = CGRectFlatted(CGRectApplyAffineTransform(containerRect, self.containerView.transform)); - } -} - -- (NSArray *)orderedAlertActions:(NSArray *)actions { - NSMutableArray *newActions = [[NSMutableArray alloc] init]; - // 按照用户addAction的先后顺序来排序 - if (self.orderActionsByAddedOrdered) { - [newActions addObjectsFromArray:self.alertActions]; - // 取消按钮不参与排序,所以先移除,在最后再重新添加 - if (self.cancelAction) { - [newActions removeObject:self.cancelAction]; - } - } else { - for (QMUIAlertAction *action in self.alertActions) { - if (action.style != QMUIAlertActionStyleCancel && action.style != QMUIAlertActionStyleDestructive) { - [newActions addObject:action]; - } - } - for (QMUIAlertAction *action in self.destructiveActions) { - [newActions addObject:action]; - } - } - if (self.cancelAction) { - [newActions addObject:self.cancelAction]; - } - return newActions; -} - -- (void)initModalPresentationController { - _modalPresentationViewController = [[QMUIModalPresentationViewController alloc] init]; - self.modalPresentationViewController.delegate = self; - self.modalPresentationViewController.maximumContentViewWidth = CGFLOAT_MAX; - self.modalPresentationViewController.contentViewMargins = UIEdgeInsetsZero; - self.modalPresentationViewController.dimmingView = nil; - self.modalPresentationViewController.contentViewController = self; - [self customModalPresentationControllerAnimation]; -} - -- (void)customModalPresentationControllerAnimation { - - __weak __typeof(self)weakSelf = self; - - self.modalPresentationViewController.layoutBlock = ^(CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewDefaultFrame) { - weakSelf.view.frame = CGRectMake(0, 0, CGRectGetWidth(containerBounds), CGRectGetHeight(containerBounds)); - weakSelf.keyboardHeight = keyboardHeight; - [weakSelf.view setNeedsLayout]; - }; - - self.modalPresentationViewController.showingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, CGRect contentViewFrame, void(^completion)(BOOL finished)) { - if (self.preferredStyle == QMUIAlertControllerStyleAlert) { - if ([weakSelf.delegate respondsToSelector:@selector(willShowAlertController:)]) { - [weakSelf.delegate willShowAlertController:weakSelf]; - } - 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.containerView.alpha = 1; - weakSelf.containerView.layer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0); - } completion:^(BOOL finished) { - weakSelf.isShowing = YES; - if ([weakSelf.delegate respondsToSelector:@selector(didShowAlertController:)]) { - [weakSelf.delegate didShowAlertController:weakSelf]; - } - if (completion) { - completion(finished); - } - }]; - } else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { - if ([weakSelf.delegate respondsToSelector:@selector(willShowAlertController:)]) { - [weakSelf.delegate willShowAlertController:weakSelf]; - } - weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, CGRectGetHeight(weakSelf.containerView.bounds), 0); - [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - weakSelf.maskView.alpha = 1; - weakSelf.containerView.layer.transform = CATransform3DIdentity; - } completion:^(BOOL finished) { - weakSelf.isShowing = YES; - if ([weakSelf.delegate respondsToSelector:@selector(didShowAlertController:)]) { - [weakSelf.delegate didShowAlertController:weakSelf]; - } - if (completion) { - completion(finished); - } - }]; - } - }; - - self.modalPresentationViewController.hidingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, void(^completion)(BOOL finished)) { - if ([weakSelf.delegate respondsToSelector:@selector(willHideAlertController:)]) { - [weakSelf.delegate willHideAlertController:weakSelf]; - } - if (self.preferredStyle == QMUIAlertControllerStyleAlert) { - [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - weakSelf.maskView.alpha = 0; - weakSelf.containerView.alpha = 0; - } completion:^(BOOL finished) { - weakSelf.isShowing = NO; - weakSelf.containerView.alpha = 1; - if ([weakSelf.delegate respondsToSelector:@selector(didHideAlertController:)]) { - [weakSelf.delegate didHideAlertController:weakSelf]; - } - if (completion) { - completion(finished); - } - }]; - } else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { - [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - weakSelf.maskView.alpha = 0; - weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, CGRectGetHeight(weakSelf.containerView.bounds), 0); - } completion:^(BOOL finished) { - weakSelf.isShowing = NO; - if ([weakSelf.delegate respondsToSelector:@selector(didHideAlertController:)]) { - [weakSelf.delegate didHideAlertController:weakSelf]; - } - if (completion) { - completion(finished); - } - }]; - } - }; -} - -- (void)showWithAnimated:(BOOL)animated { - if (self.isShowing) { - return; - } - if (self.alertTextFields.count > 0) { - [self.alertTextFields.firstObject becomeFirstResponder]; - } - if (_needsUpdateAction) { - [self updateAction]; - } - if (_needsUpdateTitle) { - [self updateTitleLabel]; - } - if (_needsUpdateMessage) { - [self updateMessageLabel]; - } - [self initModalPresentationController]; - if (animated) { - [self.modalPresentationViewController showWithAnimated:YES completion:NULL]; - } else { - __weak __typeof(self)weakSelf = self; - if ([weakSelf.delegate respondsToSelector:@selector(willShowAlertController:)]) { - [weakSelf.delegate willShowAlertController:weakSelf]; - } - if (self.preferredStyle == QMUIAlertControllerStyleAlert) { - weakSelf.maskView.alpha = 1; - weakSelf.isShowing = YES; - } else { - weakSelf.maskView.alpha = 1; - weakSelf.isShowing = YES; - } - if ([weakSelf.delegate respondsToSelector:@selector(didShowAlertController:)]) { - [weakSelf.delegate didShowAlertController:weakSelf]; - } - } - - // 增加alertController计数 - alertControllerCount++; -} - -- (void)hideWithAnimated:(BOOL)animated { - if (!self.isShowing) { - return; - } - __weak __typeof(self)weakSelf = self; - if (animated) { - [self.modalPresentationViewController hideWithAnimated:YES completion:^(BOOL finished) { - weakSelf.modalPresentationViewController = nil; - }]; - } else { - if ([weakSelf.delegate respondsToSelector:@selector(willHideAlertController:)]) { - [weakSelf.delegate willHideAlertController:weakSelf]; - } - if (self.preferredStyle == QMUIAlertControllerStyleAlert) { - weakSelf.isShowing = NO; - weakSelf.maskView.alpha = 0; - weakSelf.containerView.alpha = 0; - } else { - weakSelf.isShowing = NO; - weakSelf.maskView.alpha = 0; - weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, CGRectGetHeight(weakSelf.containerView.bounds), 0); - } - if ([weakSelf.delegate respondsToSelector:@selector(didHideAlertController:)]) { - [weakSelf.delegate didHideAlertController:weakSelf]; - } - } - - // 减少alertController计数 - alertControllerCount--; -} - -- (void)addAction:(QMUIAlertAction *)action { - if (action.style == QMUIAlertActionStyleCancel && self.cancelAction) { - [NSException raise:@"QMUIAlertController使用错误" format:@"同一个alertController不可以同时添加两个cancel按钮"]; - } - if (action.style == QMUIAlertActionStyleCancel) { - self.cancelAction = action; - } - if (action.style == QMUIAlertActionStyleDestructive) { - [self.destructiveActions addObject:action]; - } - // 只有ActionSheet的取消按钮不参与滚动 - if (self.preferredStyle == QMUIAlertControllerStyleActionSheet && action.style == QMUIAlertActionStyleCancel && !IS_IPAD) { - if (!self.cancelButtoneEffectView.superview) { - [self.containerView addSubview:self.cancelButtoneEffectView]; - } - [self.cancelButtoneEffectView addSubview:action.buttonWrapView]; - } else { - [self.buttonScrollView addSubview:action.buttonWrapView]; - } - action.delegate = self; - [self.alertActions addObject:action]; -} - -- (void)addTextFieldWithConfigurationHandler:(void (^)(UITextField *textField))configurationHandler { - if (_customView) { - [NSException raise:@"QMUIAlertController使用错误" format:@"UITextField和CustomView不能共存"]; - } - if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) { - [NSException raise:@"QMUIAlertController使用错误" format:@"Sheet类型不运行添加UITextField"]; - } - QMUITextField *textField = [[QMUITextField alloc] init]; - textField.borderStyle = UITextBorderStyleNone; - textField.backgroundColor = UIColorWhite; - textField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter; - textField.font = UIFontMake(14); - textField.textColor = UIColorBlack; - textField.autocapitalizationType = UITextAutocapitalizationTypeNone; - textField.clearButtonMode = UITextFieldViewModeWhileEditing; - textField.layer.borderColor = UIColorMake(210, 210, 210).CGColor; - textField.layer.borderWidth = PixelOne; - [self.headerScrollView addSubview:textField]; - [self.alertTextFields addObject:textField]; - if (configurationHandler) { - configurationHandler(textField); - } -} - -- (void)addCustomView:(UIView *)view { - if (self.alertTextFields.count > 0) { - [NSException raise:@"QMUIAlertController使用错误" format:@"UITextField和CustomView不能共存"]; - } - _customView = view; - [self.headerScrollView addSubview:_customView]; -} - -- (void)setTitle:(NSString *)title { - _title = title; - if (!self.titleLabel) { - self.titleLabel = [[UILabel alloc] init]; - self.titleLabel.numberOfLines = 0; - [self.headerScrollView addSubview:self.titleLabel]; - } - if (!_title || [_title isEqualToString:@""]) { - self.titleLabel.hidden = YES; - } else { - self.titleLabel.hidden = NO; - [self updateTitleLabel]; - } -} - -- (NSString *)title { - return _title; -} - -- (void)updateTitleLabel { - if (self.titleLabel && !self.titleLabel.hidden) { - NSAttributedString *attributeString = [[NSAttributedString alloc] initWithString:self.title attributes:self.preferredStyle == QMUIAlertControllerStyleAlert ? self.alertTitleAttributes : self.sheetTitleAttributes]; - self.titleLabel.attributedText = attributeString; - self.titleLabel.textAlignment = NSTextAlignmentCenter; - } -} - -- (void)setMessage:(NSString *)message { - _message = message; - if (!self.messageLabel) { - self.messageLabel = [[UILabel alloc] init]; - self.messageLabel.numberOfLines = 0; - [self.headerScrollView addSubview:self.messageLabel]; - } - if (!_message || [_message isEqualToString:@""]) { - self.messageLabel.hidden = YES; - } else { - self.messageLabel.hidden = NO; - [self updateMessageLabel]; - } -} - -- (void)updateMessageLabel { - if (self.messageLabel && !self.messageLabel.hidden) { - NSAttributedString *attributeString = [[NSAttributedString alloc] initWithString:self.message attributes:self.preferredStyle == QMUIAlertControllerStyleAlert ? self.alertMessageAttributes : self.sheetMessageAttributes]; - self.messageLabel.attributedText = attributeString; - self.messageLabel.textAlignment = NSTextAlignmentCenter; - } -} - -- (NSArray *)actions { - return self.alertActions; -} - -- (void)updateAction { - - for (QMUIAlertAction *alertAction in self.alertActions) { - - UIColor *backgroundColor = self.preferredStyle == QMUIAlertControllerStyleAlert ? self.alertButtonBackgroundColor : self.sheetButtonBackgroundColor; - UIColor *highlightBackgroundColor = self.preferredStyle == QMUIAlertControllerStyleAlert ? self.alertButtonHighlightBackgroundColor : self.sheetButtonHighlightBackgroundColor; - - alertAction.buttonWrapView.clipsToBounds = alertAction.style == QMUIAlertActionStyleCancel; - alertAction.button.backgroundColor = backgroundColor; - alertAction.button.highlightedBackgroundColor = highlightBackgroundColor; - - NSAttributedString *attributeString = nil; - if (alertAction.style == QMUIAlertActionStyleCancel) { - - NSDictionary *attributes = (self.preferredStyle == QMUIAlertControllerStyleAlert) ? self.alertCancelButtonAttributes : self.sheetCancelButtonAttributes; - if (alertAction.buttonAttributes) { - attributes = alertAction.buttonAttributes; - } - - attributeString = [[NSAttributedString alloc] initWithString:alertAction.title attributes:attributes]; - - } else if (alertAction.style == QMUIAlertActionStyleDestructive) { - - NSDictionary *attributes = (self.preferredStyle == QMUIAlertControllerStyleAlert) ? self.alertDestructiveButtonAttributes : self.sheetDestructiveButtonAttributes; - if (alertAction.buttonAttributes) { - attributes = alertAction.buttonAttributes; - } - - attributeString = [[NSAttributedString alloc] initWithString:alertAction.title attributes:attributes]; - - } else { - - NSDictionary *attributes = (self.preferredStyle == QMUIAlertControllerStyleAlert) ? self.alertButtonAttributes : self.sheetButtonAttributes; - if (alertAction.buttonAttributes) { - attributes = alertAction.buttonAttributes; - } - - attributeString = [[NSAttributedString alloc] initWithString:alertAction.title attributes:attributes]; - } - - [alertAction.button setAttributedTitle:attributeString forState:UIControlStateNormal]; - - NSDictionary *attributes = (self.preferredStyle == QMUIAlertControllerStyleAlert) ? self.alertButtonDisabledAttributes : self.sheetButtonDisabledAttributes; - if (alertAction.buttonDisabledAttributes) { - attributes = alertAction.buttonDisabledAttributes; - } - - attributeString = [[NSAttributedString alloc] initWithString:alertAction.title attributes:attributes]; - [alertAction.button setAttributedTitle:attributeString forState:UIControlStateDisabled]; - - if ([alertAction.button imageForState:UIControlStateNormal]) { - NSRange range = NSMakeRange(0, attributeString.length); - UIColor *disabledColor = [attributeString attribute:NSForegroundColorAttributeName atIndex:0 effectiveRange:&range]; - [alertAction.button setImage:[[alertAction.button imageForState:UIControlStateNormal] qmui_imageWithTintColor:disabledColor] forState:UIControlStateDisabled]; - } - } -} - -- (NSArray *)textFields { - return self.alertTextFields; -} - -- (void)handleMaskViewEvent:(id)sender { - if (_shouldRespondMaskViewTouch) { - [self hideWithAnimated:YES]; - } -} - -#pragma mark - - -- (void)didClickAlertAction:(QMUIAlertAction *)alertAction { - [self hideWithAnimated:YES]; -} - -#pragma mark - - -- (void)requestHideAllModalPresentationViewController { - [self hideWithAnimated:NO]; -} - -@end - -@implementation QMUIAlertController (Manager) - -+ (BOOL)isAnyAlertControllerVisible { - return alertControllerCount > 0; -} - -@end 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/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h new file mode 100644 index 00000000..612932a9 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h @@ -0,0 +1,23 @@ +/** + * 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.h +// QMUIKit +// +// Created by molice on 2022/5/18. +// + +#import +#import "QMUIBarProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface UINavigationBar (QMUIBarProtocol) +@end + +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/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h new file mode 100644 index 00000000..3258f9bd --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h @@ -0,0 +1,23 @@ +/** + * 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.h +// QMUIKit +// +// Created by molice on 2022/5/18. +// + +#import +#import "QMUIBarProtocol.h" + +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/QMUIButton.h b/QMUI/QMUIKit/UIKitExtensions/QMUIButton.h deleted file mode 100644 index 97f918ca..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUIButton.h +++ /dev/null @@ -1,367 +0,0 @@ -// -// QMUIButton.h -// qmui -// -// Created by MoLice on 14-7-7. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import - -/// 控制图片在UIButton里的位置,默认为QMUIButtonImagePositionLeft -typedef NS_ENUM(NSUInteger, QMUIButtonImagePosition) { - QMUIButtonImagePositionTop, // imageView在titleLabel上面 - QMUIButtonImagePositionLeft, // imageView在titleLabel左边 - QMUIButtonImagePositionBottom, // imageView在titleLabel下面 - QMUIButtonImagePositionRight, // imageView在titleLabel右边 -}; - -typedef NS_ENUM(NSUInteger, QMUIGhostButtonColor) { - QMUIGhostButtonColorBlue, - QMUIGhostButtonColorRed, - QMUIGhostButtonColorGreen, - QMUIGhostButtonColorGray, - QMUIGhostButtonColorWhite, -}; - -typedef NS_ENUM(NSUInteger, QMUIFillButtonColor) { - QMUIFillButtonColorBlue, - QMUIFillButtonColorRed, - QMUIFillButtonColorGreen, - QMUIFillButtonColorGray, - QMUIFillButtonColorWhite, -}; - -typedef NS_ENUM(NSUInteger, QMUINavigationButtonType) { - QMUINavigationButtonTypeNormal, // 普通导航栏文字按钮 - QMUINavigationButtonTypeBold, // 导航栏加粗按钮 - QMUINavigationButtonTypeImage, // 图标按钮 - QMUINavigationButtonTypeBack // 自定义返回按钮(可以同时带有title) -}; - -typedef NS_ENUM(NSUInteger, QMUIToolbarButtonType) { - QMUIToolbarButtonTypeNormal, // 普通工具栏按钮 - QMUIToolbarButtonTypeRed, // 工具栏红色按钮,用于删除等警告性操作 - QMUIToolbarButtonTypeImage, // 图标类型的按钮 -}; - -typedef NS_ENUM(NSInteger, QMUINavigationButtonPosition) { - QMUINavigationButtonPositionNone = -1, // 不处于navigationBar最左(右)边的按钮,则使用None。用None则不会在alignmentRectInsets里调整位置 - QMUINavigationButtonPositionLeft, // 用于leftBarButtonItem,如果用于leftBarButtonItems,则只对最左边的item使用,其他item使用QMUINavigationButtonPositionNone - QMUINavigationButtonPositionRight, // 用于rightBarButtonItem,如果用于rightBarButtonItems,则只对最右边的item使用,其他item使用QMUINavigationButtonPositionNone -}; - -/** - * 提供以下功能: - * 1. highlighted、disabled 状态均通过改变整个按钮的alpha来表现,无需分别设置不同 state 下的 titleColor、image。alpha 的值可在配置表里修改 ButtonHighlightedAlpha、ButtonDisabledAlpha。 - * 2. 支持点击时改变背景色颜色(highlightedBackgroundColor) - * 3. 支持点击时改变边框颜色(highlightedBorderColor) - * 4. 支持设置图片相对于 titleLabel 的位置(imagePosition) - * 5. 支持设置图片和 titleLabel 之间的间距,无需自行调整 titleEdgeInests、imageEdgeInsets(spacingBetweenImageAndTitle) - * @warning QMUIButton 重新定义了 UIButton.titleEdgeInests、imageEdgeInsets、contentEdgeInsets 这三者的布局逻辑,sizeThatFits: 里会把 titleEdgeInests 和 imageEdgeInsets 也考虑在内(UIButton 不会),以使这三个接口的使用更符合直觉。 - */ - -@interface QMUIButton : UIButton - -/** - * 让按钮的文字颜色自动跟随tintColor调整(系统默认titleColor是不跟随的)
- * 默认为NO - */ -@property(nonatomic, assign) IBInspectable BOOL adjustsTitleTintColorAutomatically; - -/** - * 让按钮的图片颜色自动跟随tintColor调整(系统默认image是需要更改renderingMode才可以达到这种效果)
- * 默认为NO - */ -@property(nonatomic, assign) IBInspectable BOOL adjustsImageTintColorAutomatically; - -/** - * 是否自动调整highlighted时的按钮样式,默认为YES。
- * 当值为YES时,按钮highlighted时会改变自身的alpha属性为ButtonHighlightedAlpha - */ -@property(nonatomic, assign) IBInspectable BOOL adjustsButtonWhenHighlighted; - -/** - * 是否自动调整disabled时的按钮样式,默认为YES。
- * 当值为YES时,按钮disabled时会改变自身的alpha属性为ButtonDisabledAlpha - */ -@property(nonatomic, assign) IBInspectable BOOL adjustsButtonWhenDisabled; - -/** - * 设置按钮点击时的背景色,默认为nil。 - * @warning 不支持带透明度的背景颜色。当设置highlightedBackgroundColor时,会强制把adjustsButtonWhenHighlighted设为NO,避免两者效果冲突。 - * @see adjustsButtonWhenHighlighted - */ -@property(nonatomic, strong) IBInspectable UIColor *highlightedBackgroundColor; - -/** - * 设置按钮点击时的边框颜色,默认为nil。 - * @warning 当设置highlightedBorderColor时,会强制把adjustsButtonWhenHighlighted设为NO,避免两者效果冲突。 - * @see adjustsButtonWhenHighlighted - */ -@property(nonatomic, strong) IBInspectable UIColor *highlightedBorderColor; - -/** - * 设置按钮里图标和文字的相对位置,默认为QMUIButtonImagePositionLeft
- * 可配合imageEdgeInsets、titleEdgeInsets、contentHorizontalAlignment、contentVerticalAlignment使用 - */ -@property(nonatomic, assign) QMUIButtonImagePosition imagePosition; - -/** - * 设置按钮里图标和文字之间的间隔,会自动响应 imagePosition 的变化而变化,默认为0。
- * 系统默认实现需要同时设置 titleEdgeInsets 和 imageEdgeInsets,同时还需考虑 contentEdgeInsets 的增加(否则不会影响布局,可能会让图标或文字溢出或挤压),使用该属性可以避免以上情况。
- * @warning 会与 imageEdgeInsets、 imageEdgeInsets、 contentEdgeInsets 共同作用。 - */ -@property(nonatomic, assign) CGFloat spacingBetweenImageAndTitle; - -@end - - -/** - * QMUINavigationButton 是用于 UINavigationItem 的按钮,有两种使用方式: - * 1. 利用类方法,快速生成所需的 UIBarButtonItem,其中大部分 UIBarButtonItem 均使用系统的 initWithBarButtonSystemItem 或 initWithImage 接口创建,仅有返回按钮利用了 customView 来创建 UIBarButtonItem。 - * 2. 利用 init 方法生成一个 QMUINavigationButton 实例,再通过类方法 + barButtonItemWithNavigationButton:position:target:action: 来生成一个对应的 UIBarButtonItem,此时 QMUINavigationButton 将作为 UIBarButtonItem 的 customView。 - * 若能满足需求,建议优先使用第 1 种方式。 - * @note 关于 tintColor:UIBarButtonItem 如果使用了 customView,则需要修改 customView.tintColor,如果没使用 customView,则直接修改 UIBarButtonItem.tintColor。 - */ -@interface QMUINavigationButton : UIButton - -/** - * 获取当前按钮的`QMUINavigationButtonType` - */ -@property(nonatomic, assign, readonly) QMUINavigationButtonType type; - -/** - * 设置按钮是否用于UINavigationBar上的UIBarButtonItem。若为YES,则会参照系统的按钮布局去更改QMUINavigationButton的内容布局,若为NO,则内容布局与普通按钮没差别。默认为YES。 - */ -@property(nonatomic, assign) BOOL useForBarButtonItem; - -/** - * 导航栏按钮的初始化函数,指定的初始化方法 - * @param type 按钮类型 - * @param title 按钮的title - */ -- (instancetype)initWithType:(QMUINavigationButtonType)type title:(NSString *)title; - -/** - * 导航栏按钮的初始化函数 - * @param type 按钮类型 - */ -- (instancetype)initWithType:(QMUINavigationButtonType)type; - -/** - * 导航栏按钮的初始化函数 - * @param image 按钮的image - */ -- (instancetype)initWithImage:(UIImage *)image; - -/** - * 创建一个 type 为 QMUINavigationButtonTypeBack 的 button 并作为 customView 用于生成一个 UIBarButtonItem,返回按钮的图片由配置表里的宏 NavBarBackIndicatorImage 决定。 - * @param target 按钮点击事件的接收者 - * @param selector 按钮点击事件的方法 - * @param tintColor 按钮要显示的颜色,如果为 nil,则表示跟随当前 UINavigationBar 的 tintColor - */ -+ (UIBarButtonItem *)backBarButtonItemWithTarget:(id)target action:(SEL)selector tintColor:(UIColor *)tintColor; - -/** - * 创建一个 type 为 QMUINavigationButtonTypeBack 的 button 并作为 customView 用于生成一个 UIBarButtonItem,返回按钮的图片由配置表里的宏 NavBarBackIndicatorImage 决定,按钮颜色跟随 UINavigationBar 的 tintColor。 - * @param target 按钮点击事件的接收者 - * @param selector 按钮点击事件的方法 - */ -+ (UIBarButtonItem *)backBarButtonItemWithTarget:(id)target action:(SEL)selector; - -/** - * 创建一个以 “×” 为图标的关闭按钮,图片由配置表里的宏 NavBarCloseButtonImage 决定。 - * @param target 按钮点击事件的接收者 - * @param selector 按钮点击事件的方法 - * @param tintColor 按钮要显示的颜色,如果为 nil,则表示跟随当前 UINavigationBar 的 tintColor - */ -+ (UIBarButtonItem *)closeBarButtonItemWithTarget:(id)target action:(SEL)selector tintColor:(UIColor *)tintColor; - -/** - * 创建一个以 “×” 为图标的关闭按钮,图片由配置表里的宏 NavBarCloseButtonImage 决定,图片颜色跟随 UINavigationBar.tintColor。 - * @param target 按钮点击事件的接收者 - * @param selector 按钮点击事件的方法 - */ -+ (UIBarButtonItem *)closeBarButtonItemWithTarget:(id)target action:(SEL)selector; - -/** - * 创建一个 UIBarButtonItem - * @param type 按钮的类型 - * @param title 按钮的标题 - * @param tintColor 按钮的颜色,如果为 nil,则表示跟随当前 UINavigationBar 的 tintColor - * @param position 按钮在 UINavigationBar 上的左右位置,如果某一边的按钮有多个,则只有最左边(最右边)的按钮需要设置为 QMUINavigationButtonPositionLeft(QMUINavigationButtonPositionRight),靠里的按钮使用 QMUINavigationButtonPositionNone 即可 - * @param target 按钮点击事件的接收者 - * @param selector 按钮点击事件的方法 - */ -+ (UIBarButtonItem *)barButtonItemWithType:(QMUINavigationButtonType)type title:(NSString *)title tintColor:(UIColor *)tintColor position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector; - -/** - * 创建一个 UIBarButtonItem - * @param type 按钮的类型 - * @param title 按钮的标题 - * @param position 按钮在 UINavigationBar 上的左右位置,如果某一边的按钮有多个,则只有最左边(最右边)的按钮需要设置为 QMUINavigationButtonPositionLeft(QMUINavigationButtonPositionRight),靠里的按钮使用 QMUINavigationButtonPositionNone 即可 - * @param target 按钮点击事件的接收者 - * @param selector 按钮点击事件的方法 - */ -+ (UIBarButtonItem *)barButtonItemWithType:(QMUINavigationButtonType)type title:(NSString *)title position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector; - -/** - * 将参数传进来的 button 作为 customView 用于生成一个 UIBarButtonItem。 - * @param button 要作为 customView 的 QMUINavigationButton - * @param tintColor 按钮的颜色,如果为 nil,则表示跟随当前 UINavigationBar 的 tintColor - * @param position 按钮在 UINavigationBar 上的左右位置,如果某一边的按钮有多个,则只有最左边(最右边)的按钮需要设置为 QMUINavigationButtonPositionLeft(QMUINavigationButtonPositionRight),靠里的按钮使用 QMUINavigationButtonPositionNone 即可 - * @param target 按钮点击事件的接收者 - * @param selector 按钮点击事件的方法 - * - * @note tintColor、position、target、selector 等参数不需要对 QMUINavigationButton 设置,通过参数传进来就可以了,就算设置了也会在这个方法里被覆盖。 - */ -+ (UIBarButtonItem *)barButtonItemWithNavigationButton:(QMUINavigationButton *)button tintColor:(UIColor *)tintColor position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector; - -/** - * 将参数传进来的 button 作为 customView 用于生成一个 UIBarButtonItem。 - * @param button 要作为 customView 的 QMUINavigationButton - * @param position 按钮在 UINavigationBar 上的左右位置,如果某一边的按钮有多个,则只有最左边(最右边)的按钮需要设置为 QMUINavigationButtonPositionLeft(QMUINavigationButtonPositionRight),靠里的按钮使用 QMUINavigationButtonPositionNone 即可 - * @param target 按钮点击事件的接收者 - * @param selector 按钮点击事件的方法 - * - * @note position、target、selector 等参数不需要对 QMUINavigationButton 设置,通过参数传进来就可以了,就算设置了也会在这个方法里被覆盖。 - */ -+ (UIBarButtonItem *)barButtonItemWithNavigationButton:(QMUINavigationButton *)button position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector; - -/** - * 创建一个图片类型的 UIBarButtonItem - * @param image 按钮的图标 - * @param tintColor 按钮的颜色,如果为 nil,则表示跟随当前 UINavigationBar 的 tintColor - * @param position 按钮在 UINavigationBar 上的左右位置,如果某一边的按钮有多个,则只有最左边(最右边)的按钮需要设置为 QMUINavigationButtonPositionLeft(QMUINavigationButtonPositionRight),靠里的按钮使用 QMUINavigationButtonPositionNone 即可 - * @param target 按钮点击事件的接收者 - * @param selector 按钮点击事件的方法 - */ -+ (UIBarButtonItem *)barButtonItemWithImage:(UIImage *)image tintColor:(UIColor *)tintColor position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector; - -/** - * 创建一个图片类型的 UIBarButtonItem - * @param image 按钮的图标 - * @param position 按钮在 UINavigationBar 上的左右位置,如果某一边的按钮有多个,则只有最左边(最右边)的按钮需要设置为 QMUINavigationButtonPositionLeft(QMUINavigationButtonPositionRight),靠里的按钮使用 QMUINavigationButtonPositionNone 即可 - * @param target 按钮点击事件的接收者 - * @param selector 按钮点击事件的方法 - */ -+ (UIBarButtonItem *)barButtonItemWithImage:(UIImage *)image position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector; - -@end - - -/** - * `QMUIToolbarButton`是用于底部工具栏的按钮 - */ -@interface QMUIToolbarButton : UIButton - -/// 获取当前按钮的type -@property(nonatomic, assign, readonly) QMUIToolbarButtonType type; - -/** - * 工具栏按钮的初始化函数 - * @param type 按钮类型 - */ -- (instancetype)initWithType:(QMUIToolbarButtonType)type; - -/** - * 工具栏按钮的初始化函数 - * @param type 按钮类型 - * @param title 按钮的title - */ -- (instancetype)initWithType:(QMUIToolbarButtonType)type title:(NSString *)title; - -/** - * 工具栏按钮的初始化函数 - * @param image 按钮的image - */ -- (instancetype)initWithImage:(UIImage *)image; - -/// 在原有的QMUIToolbarButton上创建一个UIBarButtonItem -+ (UIBarButtonItem *)barButtonItemWithToolbarButton:(QMUIToolbarButton *)button target:(id)target action:(SEL)selector; - -/// 创建一个特定type的UIBarButtonItem -+ (UIBarButtonItem *)barButtonItemWithType:(QMUIToolbarButtonType)type title:(NSString *)title target:(id)target action:(SEL)selector; - -/// 创建一个图标类型的UIBarButtonItem -+ (UIBarButtonItem *)barButtonItemWithImage:(UIImage *)image target:(id)target action:(SEL)selector; - -@end - - -/** - * 支持显示下划线的按钮,可用于需要链接的场景。下划线默认和按钮宽度一样,可通过 `underlineInsets` 调整。 - */ -@interface QMUILinkButton : QMUIButton - -/// 控制下划线隐藏或显示,默认为NO,也即显示下划线 -@property(nonatomic, assign) IBInspectable BOOL underlineHidden; - -/// 设置下划线的宽度,默认为 1 -@property(nonatomic, assign) IBInspectable CGFloat underlineWidth; - -/// 控制下划线颜色,若设置为nil,则使用当前按钮的titleColor的颜色作为下划线的颜色。默认为 nil。 -@property(nonatomic, strong) IBInspectable UIColor *underlineColor; - -/// 下划线的位置是基于 titleLabel 的位置来计算的,默认x、width均和titleLabel一致,而可以通过这个属性来调整下划线的偏移值。默认为UIEdgeInsetsZero。 -@property(nonatomic, assign) UIEdgeInsets underlineInsets; - -@end - -/** - * 用于 `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) 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:(UIColor *)ghostColor; - -@end - - -/** - * 用于 `QMUIFillButton.cornerRadius` 属性,当 `cornerRadius` 为 `QMUIFillButtonCornerRadiusAdjustsBounds` 时,`QMUIFillButton` 会在高度变化时自动调整 `cornerRadius`,使其始终保持为高度的 1/2。 - */ -extern const CGFloat QMUIFillButtonCornerRadiusAdjustsBounds; - -/** - * QMUIFillButton - * 实心填充颜色的按钮,支持预定义的几个色值 - */ -@interface QMUIFillButton : QMUIButton - -@property(nonatomic, strong) IBInspectable UIColor *fillColor; // 默认为 FillButtonColorBlue -@property(nonatomic, strong) 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:(UIColor *)fillColor titleTextColor:(UIColor *)textColor; -- (instancetype)initWithFillColor:(UIColor *)fillColor titleTextColor:(UIColor *)textColor frame:(CGRect)frame; - -@end - diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUIButton.m b/QMUI/QMUIKit/UIKitExtensions/QMUIButton.m deleted file mode 100644 index 963a1142..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUIButton.m +++ /dev/null @@ -1,1213 +0,0 @@ -// -// QMUIButton.m -// qmui -// -// Created by MoLice on 14-7-7. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUIButton.h" -#import "QMUICore.h" -#import "QMUICommonViewController.h" -#import "QMUINavigationController.h" -#import "UIImage+QMUI.h" -#import "UIViewController+QMUI.h" -#import "CALayer+QMUI.h" -#import "UIButton+QMUI.h" -#import "NSParagraphStyle+QMUI.h" - -@interface QMUIButton () - -@property(nonatomic, strong) CALayer *highlightedBackgroundLayer; -@property(nonatomic, strong) UIColor *originBorderColor; - -- (void)didInitialized;// UISubclassingHooks - -@end - -@implementation QMUIButton - -- (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - [self didInitialized]; - - self.tintColor = ButtonTintColor; - if (!self.adjustsTitleTintColorAutomatically) { - [self setTitleColor:self.tintColor forState:UIControlStateNormal]; - } - - // iOS7以后的button,sizeToFit后默认会自带一个上下的contentInsets,为了保证按钮大小即为内容大小,这里直接去掉,改为一个最小的值。 - self.contentEdgeInsets = UIEdgeInsetsMake(CGFLOAT_MIN, 0, CGFLOAT_MIN, 0); - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - self.adjustsTitleTintColorAutomatically = NO; - self.adjustsImageTintColorAutomatically = NO; - - // 默认接管highlighted和disabled的表现,去掉系统默认的表现 - self.adjustsImageWhenHighlighted = NO; - self.adjustsImageWhenDisabled = NO; - self.adjustsButtonWhenHighlighted = YES; - self.adjustsButtonWhenDisabled = YES; - - // 图片默认在按钮左边,与系统UIButton保持一致 - self.imagePosition = QMUIButtonImagePositionLeft; -} - -- (CGSize)sizeThatFits:(CGSize)size { - // 如果调用 sizeToFit,那么传进来的 size 就是当前按钮的 size,此时的计算不要去限制宽高 - if (CGSizeEqualToSize(self.bounds.size, size)) { - size = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX); - } - - CGSize resultSize = CGSizeZero; - CGSize contentLimitSize = CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets), size.height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets)); - - switch (self.imagePosition) { - case QMUIButtonImagePositionTop: - case QMUIButtonImagePositionBottom: { - // 图片和文字上下排版时,宽度以文字或图片的最大宽度为最终宽度 - CGFloat imageLimitWidth = contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets); - CGSize imageSize = [self.imageView sizeThatFits:CGSizeMake(imageLimitWidth, CGFLOAT_MAX)];// 假设图片高度必定完整显示 - - CGSize titleLimitSize = CGSizeMake(contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), contentLimitSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets) - imageSize.height - self.spacingBetweenImageAndTitle - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize]; - titleSize.height = fmin(titleSize.height, titleLimitSize.height); - - resultSize.width = UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets); - resultSize.width += fmax(UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets) + imageSize.width, UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) + titleSize.width); - resultSize.height = UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets) + imageSize.height + self.spacingBetweenImageAndTitle + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets) + titleSize.height; - } - break; - - case QMUIButtonImagePositionLeft: - case QMUIButtonImagePositionRight: { - // 图片和文字水平排版时,高度以文字或图片的最大高度为最终高度 - // titleLabel为多行时,系统的sizeThatFits计算结果依然为单行的,所以当QMUIButtonImagePositionLeft并且titleLabel多行的情况下,使用自己计算的结果 - - CGFloat imageLimitHeight = contentLimitSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets); - CGSize imageSize = [self.imageView sizeThatFits:CGSizeMake(CGFLOAT_MAX, imageLimitHeight)];// 假设图片宽度必定完整显示,高度不超过按钮内容 - - CGSize titleLimitSize = CGSizeMake(contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) - imageSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets) - self.spacingBetweenImageAndTitle, contentLimitSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize]; - titleSize.height = fmin(titleSize.height, titleLimitSize.height); - - resultSize.width = UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets) + imageSize.width + self.spacingBetweenImageAndTitle + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) + titleSize.width; - resultSize.height = UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets); - resultSize.height += fmax(UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets) + imageSize.height, UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets) + titleSize.height); - } - break; - } - return resultSize; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - - if (CGRectIsEmpty(self.bounds)) { - return; - } - - CGSize contentSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets)); - - if (self.imagePosition == QMUIButtonImagePositionTop || self.imagePosition == QMUIButtonImagePositionBottom) { - - CGFloat imageLimitWidth = contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets); - CGSize imageSize = self.imageView.hidden ? CGSizeZero : [self.imageView sizeThatFits:CGSizeMake(imageLimitWidth, CGFLOAT_MAX)];// 假设图片高度必定完整显示 - CGRect imageFrame = CGRectMakeWithSize(imageSize); - - CGSize titleLimitSize = CGSizeMake(contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets) - imageSize.height - self.spacingBetweenImageAndTitle - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize]; - titleSize.height = fmin(titleSize.height, titleLimitSize.height); - titleSize = self.titleLabel.hidden ? CGSizeZero : titleSize; - CGRect titleFrame = CGRectMakeWithSize(titleSize); - - switch (self.contentHorizontalAlignment) { - case UIControlContentHorizontalAlignmentLeft: - imageFrame = CGRectSetX(imageFrame, self.contentEdgeInsets.left + self.imageEdgeInsets.left); - titleFrame = CGRectSetX(titleFrame, self.contentEdgeInsets.left + self.titleEdgeInsets.left); - break; - case UIControlContentHorizontalAlignmentCenter: - imageFrame = CGRectSetX(imageFrame, self.contentEdgeInsets.left + self.imageEdgeInsets.left + CGFloatGetCenter(imageLimitWidth, imageSize.width)); - titleFrame = CGRectSetX(titleFrame, self.contentEdgeInsets.left + self.titleEdgeInsets.left + CGFloatGetCenter(titleLimitSize.width, titleSize.width)); - break; - case UIControlContentHorizontalAlignmentRight: - imageFrame = CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - self.contentEdgeInsets.right - self.imageEdgeInsets.right - imageSize.width); - titleFrame = CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - self.contentEdgeInsets.right - self.titleEdgeInsets.right - titleSize.width); - break; - case UIControlContentHorizontalAlignmentFill: - imageFrame = CGRectSetX(imageFrame, self.contentEdgeInsets.left + self.imageEdgeInsets.left); - imageFrame = CGRectSetWidth(imageFrame, imageLimitWidth); - titleFrame = CGRectSetX(titleFrame, self.contentEdgeInsets.left + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, titleLimitSize.width); - break; - default: - break; - } - - if (self.imagePosition == QMUIButtonImagePositionTop) { - switch (self.contentVerticalAlignment) { - case UIControlContentVerticalAlignmentTop: - imageFrame = CGRectSetY(imageFrame, self.contentEdgeInsets.top + self.imageEdgeInsets.top); - titleFrame = CGRectSetY(titleFrame, CGRectGetMaxY(imageFrame) + self.imageEdgeInsets.bottom + self.spacingBetweenImageAndTitle + self.titleEdgeInsets.top); - break; - case UIControlContentVerticalAlignmentCenter: { - CGFloat contentHeight = CGRectGetHeight(imageFrame) + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets) + CGRectGetHeight(titleFrame) + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets) + self.spacingBetweenImageAndTitle; - CGFloat minY = CGFloatGetCenter(contentSize.height, contentHeight) + self.contentEdgeInsets.top; - imageFrame = CGRectSetY(imageFrame, minY + self.imageEdgeInsets.top); - titleFrame = CGRectSetY(titleFrame, CGRectGetMaxY(imageFrame) + self.imageEdgeInsets.bottom + self.spacingBetweenImageAndTitle + self.titleEdgeInsets.top); - } - break; - case UIControlContentVerticalAlignmentBottom: - titleFrame = CGRectSetY(titleFrame, CGRectGetHeight(self.bounds) - self.contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)); - imageFrame = CGRectSetY(imageFrame, CGRectGetMinY(titleFrame) - self.spacingBetweenImageAndTitle - self.titleEdgeInsets.top - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)); - break; - case UIControlContentVerticalAlignmentFill: { - if (imageSize.height > 0 && titleSize.height > 0) { - - // 同时显示图片和 label 的情况下,图片高度按本身大小显示,剩余空间留给 label - imageFrame = CGRectSetY(imageFrame, self.contentEdgeInsets.top + self.imageEdgeInsets.top); - titleFrame = CGRectSetY(titleFrame, CGRectGetMaxY(imageFrame) + self.imageEdgeInsets.bottom + self.titleEdgeInsets.top + self.spacingBetweenImageAndTitle); - titleFrame = CGRectSetHeight(titleFrame, CGRectGetHeight(self.bounds) - self.contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetMinY(titleFrame)); - - } else if (imageSize.height > 0) { - imageFrame = CGRectSetY(imageFrame, self.contentEdgeInsets.top + self.imageEdgeInsets.top); - imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets)); - } else { - titleFrame = CGRectSetY(titleFrame, self.contentEdgeInsets.top + self.titleEdgeInsets.top); - titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - } - } - break; - } - } else { - switch (self.contentVerticalAlignment) { - case UIControlContentVerticalAlignmentTop: - titleFrame = CGRectSetY(titleFrame, self.contentEdgeInsets.top + self.titleEdgeInsets.top); - imageFrame = CGRectSetY(imageFrame, CGRectGetMaxY(titleFrame) + self.titleEdgeInsets.bottom + self.spacingBetweenImageAndTitle + self.imageEdgeInsets.top); - break; - case UIControlContentVerticalAlignmentCenter: { - CGFloat contentHeight = CGRectGetHeight(titleFrame) + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets) + self.spacingBetweenImageAndTitle + CGRectGetHeight(imageFrame) + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets); - CGFloat minY = CGFloatGetCenter(contentSize.height, contentHeight) + self.contentEdgeInsets.top; - titleFrame = CGRectSetY(titleFrame, minY + self.titleEdgeInsets.top); - imageFrame = CGRectSetY(imageFrame, CGRectGetMaxY(titleFrame) + self.titleEdgeInsets.bottom + self.spacingBetweenImageAndTitle + self.imageEdgeInsets.top); - } - break; - case UIControlContentVerticalAlignmentBottom: - imageFrame = CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - self.contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)); - titleFrame = CGRectSetY(titleFrame, CGRectGetMinY(imageFrame) - self.imageEdgeInsets.top - self.spacingBetweenImageAndTitle - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)); - - break; - case UIControlContentVerticalAlignmentFill: { - if (imageSize.height > 0 && titleSize.height > 0) { - - // 同时显示图片和 label 的情况下,图片高度按本身大小显示,剩余空间留给 label - imageFrame = CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - self.contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)); - titleFrame = CGRectSetY(titleFrame, self.contentEdgeInsets.top + self.titleEdgeInsets.top); - titleFrame = CGRectSetHeight(titleFrame, CGRectGetMinY(imageFrame) - self.imageEdgeInsets.top - self.spacingBetweenImageAndTitle - self.titleEdgeInsets.bottom - CGRectGetMinY(titleFrame)); - - } else if (imageSize.height > 0) { - imageFrame = CGRectSetY(imageFrame, self.contentEdgeInsets.top + self.imageEdgeInsets.top); - imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets)); - } else { - titleFrame = CGRectSetY(titleFrame, self.contentEdgeInsets.top + self.titleEdgeInsets.top); - titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - } - } - break; - } - } - - self.imageView.frame = CGRectFlatted(imageFrame); - self.titleLabel.frame = CGRectFlatted(titleFrame); - - } else if (self.imagePosition == QMUIButtonImagePositionLeft || self.imagePosition == QMUIButtonImagePositionRight) { - - CGFloat imageLimitHeight = contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets); - CGSize imageSize = self.imageView.hidden ? CGSizeZero : [self.imageView sizeThatFits:CGSizeMake(CGFLOAT_MAX, imageLimitHeight)];// 假设图片宽度必定完整显示,高度不超过按钮内容 - CGRect imageFrame = CGRectMakeWithSize(imageSize); - - CGSize titleLimitSize = CGSizeMake(contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) - CGRectGetWidth(imageFrame) - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets) - self.spacingBetweenImageAndTitle, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize]; - titleSize.height = fmin(titleSize.height, titleLimitSize.height); - titleSize = self.titleLabel.hidden ? CGSizeZero : titleSize; - CGRect titleFrame = CGRectMakeWithSize(titleSize); - - switch (self.contentVerticalAlignment) { - case UIControlContentVerticalAlignmentTop: - titleFrame = CGRectSetY(titleFrame, self.contentEdgeInsets.top + self.titleEdgeInsets.top); - imageFrame = CGRectSetY(imageFrame, self.contentEdgeInsets.top + self.imageEdgeInsets.top); - break; - case UIControlContentVerticalAlignmentCenter: - titleFrame = CGRectSetY(titleFrame, self.contentEdgeInsets.top + CGFloatGetCenter(contentSize.height, CGRectGetHeight(titleFrame)) + self.titleEdgeInsets.top); - imageFrame = CGRectSetY(imageFrame, self.contentEdgeInsets.top + CGFloatGetCenter(contentSize.height, CGRectGetHeight(imageFrame)) + self.imageEdgeInsets.top); - break; - case UIControlContentVerticalAlignmentBottom: - titleFrame = CGRectSetY(titleFrame, CGRectGetHeight(self.bounds) - self.contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)); - imageFrame = CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - self.contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)); - break; - case UIControlContentVerticalAlignmentFill: - titleFrame = CGRectSetY(titleFrame, self.contentEdgeInsets.top + self.titleEdgeInsets.top); - titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets)); - imageFrame = CGRectSetY(imageFrame, self.contentEdgeInsets.top + self.imageEdgeInsets.top); - imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets)); - break; - } - - if (self.imagePosition == QMUIButtonImagePositionLeft) { - switch (self.contentHorizontalAlignment) { - case UIControlContentHorizontalAlignmentLeft: - imageFrame = CGRectSetX(imageFrame, self.contentEdgeInsets.left + self.imageEdgeInsets.left); - titleFrame = CGRectSetX(titleFrame, CGRectGetMaxX(imageFrame) + self.imageEdgeInsets.right + self.spacingBetweenImageAndTitle + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, fmin(CGRectGetWidth(titleFrame), contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets) - CGRectGetWidth(imageFrame) - self.spacingBetweenImageAndTitle - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets)));// 保护 titleLabel 不要溢出 button - break; - case UIControlContentHorizontalAlignmentCenter: { - CGFloat contentWidth = CGRectGetWidth(titleFrame) + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) + self.spacingBetweenImageAndTitle + CGRectGetWidth(imageFrame) + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets); - CGFloat minX = self.contentEdgeInsets.left + CGFloatGetCenter(contentSize.width, contentWidth); - imageFrame = CGRectSetX(imageFrame, minX + self.imageEdgeInsets.left); - titleFrame = CGRectSetX(titleFrame, CGRectGetMaxX(imageFrame) + self.imageEdgeInsets.right + self.spacingBetweenImageAndTitle + self.titleEdgeInsets.left); - } - break; - case UIControlContentHorizontalAlignmentRight: { - if (CGRectGetWidth(imageFrame) + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets) + self.spacingBetweenImageAndTitle + CGRectGetWidth(titleFrame) + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) > contentSize.width) { - // 图片和文字总宽超过按钮宽度,则优先完整显示图片 - imageFrame = CGRectSetX(imageFrame, self.contentEdgeInsets.left + self.imageEdgeInsets.left); - titleFrame = CGRectSetX(titleFrame, CGRectGetMaxX(imageFrame) + self.imageEdgeInsets.right + self.spacingBetweenImageAndTitle + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, CGRectGetWidth(self.bounds) - self.contentEdgeInsets.right - self.titleEdgeInsets.right - CGRectGetMinX(titleFrame)); - } else { - // 内容不超过按钮宽度,则靠右布局即可 - titleFrame = CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - self.contentEdgeInsets.right - self.titleEdgeInsets.right - CGRectGetWidth(titleFrame)); - imageFrame = CGRectSetX(imageFrame, CGRectGetMinX(titleFrame) - self.titleEdgeInsets.left - self.spacingBetweenImageAndTitle - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)); - } - } - break; - case UIControlContentHorizontalAlignmentFill: { - if (imageSize.width > 0 && titleSize.width > 0) { - - // 同时显示图片和 label 的情况下,图片按本身宽度显示,剩余空间留给 label - imageFrame = CGRectSetX(imageFrame, self.contentEdgeInsets.left + self.imageEdgeInsets.left); - titleFrame = CGRectSetX(titleFrame, CGRectGetMaxX(imageFrame) + self.imageEdgeInsets.right + self.spacingBetweenImageAndTitle + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, CGRectGetWidth(self.bounds) - CGRectGetMinX(titleFrame) - self.titleEdgeInsets.right - self.contentEdgeInsets.right); - - } else if (imageSize.width > 0) { - imageFrame = CGRectSetX(imageFrame, self.contentEdgeInsets.left + self.imageEdgeInsets.left); - imageFrame = CGRectSetWidth(imageFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets)); - } else { - titleFrame = CGRectSetX(titleFrame, self.contentEdgeInsets.left + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets)); - } - } - break; - default: - break; - } - } else { - switch (self.contentHorizontalAlignment) { - case UIControlContentHorizontalAlignmentLeft: { - if (CGRectGetWidth(imageFrame) + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets) + self.spacingBetweenImageAndTitle + CGRectGetWidth(titleFrame) + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) > contentSize.width) { - // 图片和文字总宽超过按钮宽度,则优先完整显示图片 - imageFrame = CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - self.contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)); - titleFrame = CGRectSetX(titleFrame, self.contentEdgeInsets.left + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, CGRectGetMinX(imageFrame) - self.imageEdgeInsets.left - self.spacingBetweenImageAndTitle - self.titleEdgeInsets.right - CGRectGetMinX(titleFrame)); - } else { - // 内容不超过按钮宽度,则靠左布局即可 - titleFrame = CGRectSetX(titleFrame, self.contentEdgeInsets.left + self.titleEdgeInsets.left); - imageFrame = CGRectSetX(imageFrame, CGRectGetMaxX(titleFrame) + self.titleEdgeInsets.right + self.spacingBetweenImageAndTitle + self.imageEdgeInsets.left); - } - } - break; - case UIControlContentHorizontalAlignmentCenter: { - CGFloat contentWidth = CGRectGetWidth(titleFrame) + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) + CGRectGetWidth(imageFrame) + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets) + self.spacingBetweenImageAndTitle; - CGFloat minX = self.contentEdgeInsets.left + CGFloatGetCenter(contentSize.width, contentWidth); - titleFrame = CGRectSetX(titleFrame, minX + self.titleEdgeInsets.left); - imageFrame = CGRectSetX(imageFrame, CGRectGetMaxX(titleFrame) + self.titleEdgeInsets.right + self.spacingBetweenImageAndTitle + self.imageEdgeInsets.left); - } - break; - case UIControlContentHorizontalAlignmentRight: - imageFrame = CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - self.contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)); - titleFrame = CGRectSetX(titleFrame, CGRectGetMinX(imageFrame) - self.imageEdgeInsets.left - self.spacingBetweenImageAndTitle - self.titleEdgeInsets.right - CGRectGetWidth(titleFrame)); - titleFrame = CGRectSetWidth(titleFrame, fmin(CGRectGetWidth(titleFrame), contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets) - CGRectGetWidth(imageFrame) - self.spacingBetweenImageAndTitle - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets)));// 保护 titleLabel 不要溢出 button - break; - case UIControlContentHorizontalAlignmentFill: { - if (imageSize.width > 0 && titleSize.width > 0) { - - // 图片按自身大小显示,剩余空间由标题占满 - imageFrame = CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - self.contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)); - titleFrame = CGRectSetX(titleFrame, self.contentEdgeInsets.left + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, CGRectGetMinX(imageFrame) - self.imageEdgeInsets.left - self.spacingBetweenImageAndTitle - self.titleEdgeInsets.right - CGRectGetMinX(titleFrame)); - - } else if (imageSize.width > 0) { - imageFrame = CGRectSetX(imageFrame, self.contentEdgeInsets.left + self.imageEdgeInsets.left); - imageFrame = CGRectSetWidth(imageFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets)); - } else { - titleFrame = CGRectSetX(titleFrame, self.contentEdgeInsets.left + self.titleEdgeInsets.left); - titleFrame = CGRectSetWidth(titleFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets)); - } - } - break; - default: - break; - } - } - - self.imageView.frame = CGRectFlatted(imageFrame); - self.titleLabel.frame = CGRectFlatted(titleFrame); - } -} - -- (void)setSpacingBetweenImageAndTitle:(CGFloat)spacingBetweenImageAndTitle { - _spacingBetweenImageAndTitle = spacingBetweenImageAndTitle; - - [self setNeedsLayout]; -} - -- (void)setImagePosition:(QMUIButtonImagePosition)imagePosition { - _imagePosition = imagePosition; - - [self setNeedsLayout]; -} - -- (void)setHighlightedBackgroundColor:(UIColor *)highlightedBackgroundColor { - _highlightedBackgroundColor = highlightedBackgroundColor; - if (_highlightedBackgroundColor) { - // 只要开启了highlightedBackgroundColor,就默认不需要alpha的高亮 - self.adjustsButtonWhenHighlighted = NO; - } -} - -- (void)setHighlightedBorderColor:(UIColor *)highlightedBorderColor { - _highlightedBorderColor = highlightedBorderColor; - if (_highlightedBorderColor) { - // 只要开启了highlightedBorderColor,就默认不需要alpha的高亮 - self.adjustsButtonWhenHighlighted = NO; - } -} - -- (void)setHighlighted:(BOOL)highlighted { - [super setHighlighted:highlighted]; - - if (highlighted && !self.originBorderColor) { - // 手指按在按钮上会不断触发setHighlighted:,所以这里做了保护,设置过一次就不用再设置了 - self.originBorderColor = [UIColor colorWithCGColor:self.layer.borderColor]; - } - - // 渲染背景色 - if (self.highlightedBackgroundColor || self.highlightedBorderColor) { - [self adjustsButtonHighlighted]; - } - // 如果此时是disabled,则disabled的样式优先 - if (!self.enabled) { - return; - } - // 自定义highlighted样式 - if (self.adjustsButtonWhenHighlighted) { - if (highlighted) { - self.alpha = ButtonHighlightedAlpha; - } else { - [UIView animateWithDuration:0.25f animations:^{ - self.alpha = 1; - }]; - } - } -} - -- (void)setEnabled:(BOOL)enabled { - [super setEnabled:enabled]; - if (!enabled && self.adjustsButtonWhenDisabled) { - self.alpha = ButtonDisabledAlpha; - } else { - [UIView animateWithDuration:0.25f animations:^{ - self.alpha = 1; - }]; - } -} - -- (void)adjustsButtonHighlighted { - if (self.highlightedBackgroundColor) { - if (!self.highlightedBackgroundLayer) { - self.highlightedBackgroundLayer = [CALayer layer]; - [self.highlightedBackgroundLayer qmui_removeDefaultAnimations]; - [self.layer insertSublayer:self.highlightedBackgroundLayer atIndex:0]; - } - self.highlightedBackgroundLayer.frame = self.bounds; - self.highlightedBackgroundLayer.cornerRadius = self.layer.cornerRadius; - self.highlightedBackgroundLayer.backgroundColor = self.highlighted ? self.highlightedBackgroundColor.CGColor : UIColorClear.CGColor; - } - - if (self.highlightedBorderColor) { - self.layer.borderColor = self.highlighted ? self.highlightedBorderColor.CGColor : self.originBorderColor.CGColor; - } -} - -- (void)setAdjustsTitleTintColorAutomatically:(BOOL)adjustsTitleTintColorAutomatically { - _adjustsTitleTintColorAutomatically = adjustsTitleTintColorAutomatically; - [self updateTitleColorIfNeeded]; -} - -- (void)updateTitleColorIfNeeded { - if (self.adjustsTitleTintColorAutomatically && self.currentTitleColor) { - [self setTitleColor:self.tintColor forState:UIControlStateNormal]; - } - if (self.adjustsTitleTintColorAutomatically && 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]; - } -} - -- (void)setAdjustsImageTintColorAutomatically:(BOOL)adjustsImageTintColorAutomatically { - BOOL valueDifference = _adjustsImageTintColorAutomatically != adjustsImageTintColorAutomatically; - _adjustsImageTintColorAutomatically = adjustsImageTintColorAutomatically; - - if (valueDifference) { - [self updateImageRenderingModeIfNeeded]; - } -} - -- (void)updateImageRenderingModeIfNeeded { - if (self.currentImage) { - NSArray *states = @[@(UIControlStateNormal), @(UIControlStateHighlighted), @(UIControlStateDisabled)]; - for (NSNumber *number in states) { - UIImage *image = [self imageForState:[number unsignedIntegerValue]]; - if (!image) { - continue; - } - - if (self.adjustsImageTintColorAutomatically) { - // 这里的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.adjustsImageTintColorAutomatically) { - image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - } - [super setImage:image forState:state]; -} - -- (void)tintColorDidChange { - [super tintColorDidChange]; - - [self updateTitleColorIfNeeded]; - - if (self.adjustsImageTintColorAutomatically) { - [self updateImageRenderingModeIfNeeded]; - } -} - -@end - -@interface QMUINavigationButton() - -@property(nonatomic, assign) QMUINavigationButtonPosition buttonPosition; -@end - - -@implementation QMUINavigationButton - -- (instancetype)init { - return [self initWithType:QMUINavigationButtonTypeNormal]; -} - -- (instancetype)initWithType:(QMUINavigationButtonType)type { - return [self initWithType:type title:nil]; -} - -- (instancetype)initWithType:(QMUINavigationButtonType)type title:(NSString *)title { - if (self = [super initWithFrame:CGRectZero]) { - _type = type; - self.buttonPosition = QMUINavigationButtonPositionNone; - self.useForBarButtonItem = YES; - [self setTitle:title forState:UIControlStateNormal]; - [self renderButtonStyle]; - [self sizeToFit]; - } - return self; -} - -- (instancetype)initWithImage:(UIImage *)image { - if (self = [self initWithType:QMUINavigationButtonTypeImage]) { - [self setImage:image forState:UIControlStateNormal]; - // 系统在iOS8及以后的版本默认对image的UIBarButtonItem加了上下3、左右11的padding,所以这里统一一下 - self.contentEdgeInsets = UIEdgeInsetsMake(3, 11, 3, 11); - [self sizeToFit]; - } - return self; -} - -// 对按钮内容添加偏移,让UIBarButtonItem适配最新设备的系统行为,统一位置 -- (UIEdgeInsets)alignmentRectInsets { - - UIEdgeInsets insets = [super alignmentRectInsets]; - if (!self.useForBarButtonItem || self.buttonPosition == QMUINavigationButtonPositionNone) { - return insets; - } - - if (self.buttonPosition == QMUINavigationButtonPositionLeft) { - // 正值表示往左偏移 - if (self.type == QMUINavigationButtonTypeImage) { - insets = UIEdgeInsetsSetLeft(insets, 11); - } else { - insets = UIEdgeInsetsSetLeft(insets, 8); - } - } else if (self.buttonPosition == QMUINavigationButtonPositionRight) { - // 正值表示往右偏移 - if (self.type == QMUINavigationButtonTypeImage) { - insets = UIEdgeInsetsSetRight(insets, 11); - } else { - insets = UIEdgeInsetsSetRight(insets, 8); - } - } - - - BOOL isBackOrImageType = self.type == QMUINavigationButtonTypeBack || self.type == QMUINavigationButtonTypeImage; - if (isBackOrImageType) { - insets = UIEdgeInsetsSetTop(insets, PixelOne); - } else { - insets = UIEdgeInsetsSetTop(insets, 1); - } - - return insets; -} - -- (void)renderButtonStyle { - - self.titleLabel.backgroundColor = UIColorClear; - self.titleLabel.font = NavBarButtonFont; - self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; - self.contentMode = UIViewContentModeCenter; - self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter; - self.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter; - self.adjustsImageWhenHighlighted = NO; - self.adjustsImageWhenDisabled = NO; - - switch (self.type) { - case QMUINavigationButtonTypeNormal: - case QMUINavigationButtonTypeImage: - break; - case QMUINavigationButtonTypeBold: { - self.titleLabel.font = NavBarButtonFontBold; - } - break; - case QMUINavigationButtonTypeBack: { - self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; - UIImage *backIndicatorImage = NavBarBackIndicatorImage; - if (!backIndicatorImage) { - QMUILog(@"NavBarBackIndicatorImage 为 nil,无法创建正确的 QMUINavigationButtonTypeBack 按钮"); - return; - } - [self setImage:backIndicatorImage forState:UIControlStateNormal]; - [self setImage:[backIndicatorImage qmui_imageWithAlpha:NavBarHighlightedAlpha] forState:UIControlStateHighlighted]; - [self setImage:[backIndicatorImage qmui_imageWithAlpha:NavBarDisabledAlpha] forState:UIControlStateDisabled]; - } - break; - - default: - break; - } -} - -- (void)setUseForBarButtonItem:(BOOL)useForBarButtonItem { - if (_useForBarButtonItem != useForBarButtonItem) { - if (self.type == QMUINavigationButtonTypeBack) { - // 只针对返回按钮,调整箭头和title之间的间距 - // @warning 这些数值都是每个iOS版本核对过没问题的,如果修改则要检查要每个版本里与系统UIBarButtonItem的布局是否一致 - if (useForBarButtonItem) { - UIOffset titleOffset = NavBarBarBackButtonTitlePositionAdjustment; - CGFloat customTitleOffset = IOS_VERSION < 8.0 ? 1 : 0; // 除了全局设置的titleOffset,有些版本的自定义返回按钮还需要一个偏移值 - CGFloat titleHorizontalOffset = 7.0; - self.titleEdgeInsets = UIEdgeInsetsMake(titleOffset.vertical + customTitleOffset, titleHorizontalOffset + titleOffset.horizontal, - titleOffset.vertical, -titleHorizontalOffset - titleOffset.horizontal); - self.contentEdgeInsets = UIEdgeInsetsMake(1, 0, 0, titleHorizontalOffset + titleOffset.horizontal);// 箭头左边的间距 - } - // 由于contentEdgeInsets会影响frame的大小,所以更新数值后需要重新计算size - [self sizeToFit]; - } - } - _useForBarButtonItem = useForBarButtonItem; -} - -// 修复系统的UIBarButtonItem里的图片无法跟着tintColor走 -- (void)setImage:(UIImage *)image forState:(UIControlState)state { - if (image && [image respondsToSelector:@selector(imageWithRenderingMode:)]) { - image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - } - [super setImage:image forState:state]; -} - -// 自定义nav按钮,需要根据这个来修改title的三态颜色。 -- (void)tintColorDidChange { - [super tintColorDidChange]; - [self setTitleColor:self.tintColor forState:UIControlStateNormal]; - [self setTitleColor:[self.tintColor colorWithAlphaComponent:NavBarHighlightedAlpha] forState:UIControlStateHighlighted]; - [self setTitleColor:[self.tintColor colorWithAlphaComponent:NavBarDisabledAlpha] forState:UIControlStateDisabled]; -} - -// 返回按钮的文字会自动匹配上一个界面的title,如果需要自定义title,则直接用initWithType:title:工具类来做 -+ (UIBarButtonItem *)backBarButtonItemWithTarget:(id)target action:(SEL)selector tintColor:(UIColor *)tintColor { - NSString *backTitle = nil; - if (NeedsBackBarButtonItemTitle) { - backTitle = @"返回"; // 默认文字用返回 - - if ([target isKindOfClass:[UIViewController class]]) { - UIViewController *viewController = (UIViewController *)target; - UIViewController *previousViewController = viewController.qmui_previousViewController; - if (previousViewController.navigationItem.backBarButtonItem) { - // 如果前一个界面有 - backTitle = previousViewController.navigationItem.backBarButtonItem.title; - - } else if (previousViewController.title) { - backTitle = previousViewController.title; - } - } - - } else { - backTitle = @" "; - } - - return [self systemBarButtonItemWithType:QMUINavigationButtonTypeBack title:backTitle tintColor:tintColor position:QMUINavigationButtonPositionLeft target:target action:selector]; -} - -+ (UIBarButtonItem *)backBarButtonItemWithTarget:(id)target action:(SEL)selector { - return [self backBarButtonItemWithTarget:target action:selector tintColor:nil]; -} - -+ (UIBarButtonItem *)closeBarButtonItemWithTarget:(id)target action:(SEL)selector tintColor:(UIColor *)tintColor { - UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:NavBarCloseButtonImage style:UIBarButtonItemStylePlain target:target action:selector]; - item.tintColor = tintColor; - return item; -} - -+ (UIBarButtonItem *)closeBarButtonItemWithTarget:(id)target action:(SEL)selector { - return [self closeBarButtonItemWithTarget:target action:selector tintColor:nil]; -} - -+ (UIBarButtonItem *)barButtonItemWithNavigationButton:(QMUINavigationButton *)button tintColor:(UIColor *)tintColor position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector { - if (target) { - [button addTarget:target action:selector forControlEvents:UIControlEventTouchUpInside]; - } - button.tintColor = tintColor; - button.buttonPosition = position; - UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button]; - return barButtonItem; -} - -+ (UIBarButtonItem *)barButtonItemWithNavigationButton:(QMUINavigationButton *)button position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector { - return [self barButtonItemWithNavigationButton:button tintColor:nil position:position target:target action:selector]; -} - -+ (UIBarButtonItem *)barButtonItemWithType:(QMUINavigationButtonType)type title:(NSString *)title tintColor:(UIColor *)tintColor position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector { - UIBarButtonItem *barButtonItem = [QMUINavigationButton systemBarButtonItemWithType:type title:title tintColor:tintColor position:position target:target action:selector]; - return barButtonItem; -} - -+ (UIBarButtonItem *)barButtonItemWithType:(QMUINavigationButtonType)type title:(NSString *)title position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector { - return [QMUINavigationButton barButtonItemWithType:type title:title tintColor:nil position:position target:target action:selector]; -} - -+ (UIBarButtonItem *)barButtonItemWithImage:(UIImage *)image tintColor:(UIColor *)tintColor position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector { - UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStylePlain target:target action:selector]; - barButtonItem.tintColor = tintColor; - return barButtonItem; -} - -+ (UIBarButtonItem *)barButtonItemWithImage:(UIImage *)image position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector { - return [QMUINavigationButton barButtonItemWithImage:image tintColor:nil position:position target:target action:selector]; -} - -+ (UIBarButtonItem *)systemBarButtonItemWithType:(QMUINavigationButtonType)type title:(NSString *)title tintColor:(UIColor *)tintColor position:(QMUINavigationButtonPosition)position target:(id)target action:(SEL)selector { - switch (type) { - - case QMUINavigationButtonTypeBack: - { - // 因为有可能出现有箭头图片又有title的情况,所以这里不适合用barButtonItemWithImage:target:action:的那个接口 - QMUINavigationButton *button = [[QMUINavigationButton alloc] initWithType:QMUINavigationButtonTypeBack title:title]; - button.buttonPosition = position; - [button addTarget:target action:selector forControlEvents:UIControlEventTouchUpInside]; - button.tintColor = tintColor; - UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:button]; - return barButtonItem; - } - break; - - case QMUINavigationButtonTypeBold: - { - UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:target action:selector]; - [barButtonItem setTitleTextAttributes:@{NSFontAttributeName:NavBarButtonFontBold} forState:UIControlStateNormal]; - barButtonItem.tintColor = tintColor; - return barButtonItem; - } - break; - - case QMUINavigationButtonTypeImage: - // icon - 这种类型请通过barButtonItemWithImage:position:target:action:来定义 - return nil; - - default: - { - UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:target action:selector]; - barButtonItem.tintColor = tintColor; - return barButtonItem; - } - break; - } -} - -@end - - -@implementation QMUIToolbarButton - -- (instancetype)init { - return [self initWithType:QMUIToolbarButtonTypeNormal]; -} - -- (instancetype)initWithType:(QMUIToolbarButtonType)type { - return [self initWithType:type title:nil]; -} - -- (instancetype)initWithType:(QMUIToolbarButtonType)type title:(NSString *)title { - if (self = [super init]) { - _type = type; - [self setTitle:title forState:UIControlStateNormal]; - [self renderButtonStyle]; - [self sizeToFit]; - } - return self; -} - -- (instancetype)initWithImage:(UIImage *)image { - if (self = [self initWithType:QMUIToolbarButtonTypeImage]) { - [self setImage:image forState:UIControlStateNormal]; - [self setImage:[image qmui_imageWithAlpha:ToolBarHighlightedAlpha] forState:UIControlStateHighlighted]; - [self setImage:[image qmui_imageWithAlpha:ToolBarDisabledAlpha] forState:UIControlStateDisabled]; - [self sizeToFit]; - } - return self; -} - -- (void)renderButtonStyle { - self.imageView.contentMode = UIViewContentModeCenter; - self.imageView.tintColor = nil; // 重置默认值,nil表示跟随父元素 - self.titleLabel.font = ToolBarButtonFont; - switch (self.type) { - case QMUIToolbarButtonTypeNormal: - [self setTitleColor:ToolBarTintColor forState:UIControlStateNormal]; - [self setTitleColor:ToolBarTintColorHighlighted forState:UIControlStateHighlighted]; - [self setTitleColor:ToolBarTintColorDisabled forState:UIControlStateDisabled]; - break; - case QMUIToolbarButtonTypeRed: - [self setTitleColor:UIColorRed forState:UIControlStateNormal]; - [self setTitleColor:[UIColorRed colorWithAlphaComponent:ToolBarHighlightedAlpha] forState:UIControlStateHighlighted]; - [self setTitleColor:[UIColorRed colorWithAlphaComponent:ToolBarDisabledAlpha] forState:UIControlStateDisabled]; - self.imageView.tintColor = UIColorRed; // 修改为红色 - break; - case QMUIToolbarButtonTypeImage: - break; - default: - break; - } -} - -+ (UIBarButtonItem *)barButtonItemWithToolbarButton:(QMUIToolbarButton *)button target:(id)target action:(SEL)selector { - [button addTarget:target action:selector forControlEvents:UIControlEventTouchUpInside]; - UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithCustomView:button]; - return buttonItem; -} - -+ (UIBarButtonItem *)barButtonItemWithType:(QMUIToolbarButtonType)type title:(NSString *)title target:(id)target action:(SEL)selector { - UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:target action:selector]; - if (type == QMUIToolbarButtonTypeRed) { - // 默认继承toolBar的tintColor,红色需要重置 - buttonItem.tintColor = UIColorRed; - } - return buttonItem; -} - -+ (UIBarButtonItem *)barButtonItemWithImage:(UIImage *)image target:(id)target action:(SEL)selector { - UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStylePlain target:target action:selector]; - return buttonItem; -} - -@end - -@interface QMUILinkButton () - -@property(nonatomic, strong) CALayer *underlineLayer; -@end - -@implementation QMUILinkButton - -- (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - [super didInitialized]; - - 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 - -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 = flat(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; - if (IOS_VERSION >= 8.0) { - appearance.adjustsImageWithGhostColor = NO; - } -} - -@end - - -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 = flat(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; - if (IOS_VERSION >= 8.0) { - appearance.adjustsImageWithTitleTextColor = NO; - } -} - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUICellHeightCache.h b/QMUI/QMUIKit/UIKitExtensions/QMUICellHeightCache.h deleted file mode 100644 index f07e1149..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUICellHeightCache.h +++ /dev/null @@ -1,51 +0,0 @@ -// -// QMUICellHeightCache.h -// qmui -// -// Created by zhoonchen on 15/12/23. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import -#import - -@interface QMUICellHeightCache : NSObject - -@end - -/** - * 通过业务定义的一个 key 来缓存 cell 的高度,需搭配 UITableView 或 UICollectionView 使用。 - */ -@interface QMUICellHeightKeyCache : NSObject - -- (BOOL)existsHeightForKey:(id)key; -- (void)cacheHeight:(CGFloat)height byKey:(id)key; -- (CGFloat)heightForKey:(id)key; - -// 使cache失效,多用在data更新之后 -- (void)invalidateHeightForKey:(id)key; -- (void)invalidateAllHeightCache; - -@end - -/** - * 通过 NSIndexPath 来缓存 cell 的高度,需搭配 UITableView 或 UICollectionView 使用。 - */ -@interface QMUICellHeightIndexPathCache : NSObject - -@property (nonatomic, assign) BOOL automaticallyInvalidateEnabled; - -- (BOOL)existsHeightAtIndexPath:(NSIndexPath *)indexPath; -- (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath; -- (CGFloat)heightForIndexPath:(NSIndexPath *)indexPath; - -// 使cache失效,多用在data更新之后 -- (void)invalidateHeightAtIndexPath:(NSIndexPath *)indexPath; -- (void)invalidateAllHeightCache; - -// 给 tableview 和 collectionview 调用的方法 -- (void)enumerateAllOrientationsUsingBlock:(void (^)(NSMutableArray *heightsBySection))block; -- (void)buildSectionsIfNeeded:(NSInteger)targetSection; -- (void)buildCachesAtIndexPathsIfNeeded:(NSArray *)indexPaths; - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUICellHeightCache.m b/QMUI/QMUIKit/UIKitExtensions/QMUICellHeightCache.m deleted file mode 100644 index 4a2af8d2..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUICellHeightCache.m +++ /dev/null @@ -1,158 +0,0 @@ -// -// QMUICellHeightCache.m -// qmui -// -// Created by zhoonchen on 15/12/23. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import "QMUICellHeightCache.h" - -@implementation QMUICellHeightCache - -@end - -@implementation QMUICellHeightKeyCache { - NSMutableDictionary *_mutableHeightsByKeyForPortrait; - NSMutableDictionary *_mutableHeightsByKeyForLandscape; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _mutableHeightsByKeyForPortrait = [NSMutableDictionary dictionary]; - _mutableHeightsByKeyForLandscape = [NSMutableDictionary dictionary]; - } - return self; -} - -- (NSMutableDictionary *)mutableHeightsByKeyForCurrentOrientation { - return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation) ? _mutableHeightsByKeyForPortrait : _mutableHeightsByKeyForLandscape; -} - -- (BOOL)existsHeightForKey:(id)key { - NSNumber *number = [self mutableHeightsByKeyForCurrentOrientation][key]; - return number && ![number isEqualToNumber:@-1]; -} - -- (void)cacheHeight:(CGFloat)height byKey:(id)key { - [self mutableHeightsByKeyForCurrentOrientation][key] = @(height); -} - -- (CGFloat)heightForKey:(id)key { -#if CGFLOAT_IS_DOUBLE - return [[self mutableHeightsByKeyForCurrentOrientation][key] doubleValue]; -#else - return [[self mutableHeightsByKeyForCurrentOrientation][key] floatValue]; -#endif -} - -- (void)invalidateHeightForKey:(id)key { - [_mutableHeightsByKeyForPortrait removeObjectForKey:key]; - [_mutableHeightsByKeyForLandscape removeObjectForKey:key]; -} - -- (void)invalidateAllHeightCache { - [_mutableHeightsByKeyForPortrait removeAllObjects]; - [_mutableHeightsByKeyForLandscape removeAllObjects]; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"%@, mutableHeightsByKeyForPortrait = %@, mutableHeightsByKeyForLandscape = %@", [super description], _mutableHeightsByKeyForPortrait, _mutableHeightsByKeyForLandscape]; -} - -@end - -@implementation QMUICellHeightIndexPathCache { - NSMutableArray *_heightsBySectionForPortrait; - NSMutableArray *_heightsBySectionForLandscape; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _heightsBySectionForPortrait = [NSMutableArray array]; - _heightsBySectionForLandscape = [NSMutableArray array]; - } - return self; -} - -- (NSMutableArray *)heightsBySectionForCurrentOrientation { - return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation) ? _heightsBySectionForPortrait : _heightsBySectionForLandscape; -} - -- (void)enumerateAllOrientationsUsingBlock:(void (^)(NSMutableArray *heightsBySection))block { - if (block) { - block(_heightsBySectionForPortrait); - block(_heightsBySectionForLandscape); - } -} - -- (BOOL)existsHeightAtIndexPath:(NSIndexPath *)indexPath { - [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; - NSNumber *number = self.heightsBySectionForCurrentOrientation[indexPath.section][indexPath.row]; - return ![number isEqualToNumber:@-1]; -} - -- (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath { - self.automaticallyInvalidateEnabled = YES; - [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; - self.heightsBySectionForCurrentOrientation[indexPath.section][indexPath.row] = @(height); -} - -- (CGFloat)heightForIndexPath:(NSIndexPath *)indexPath { - [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; - NSNumber *number = self.heightsBySectionForCurrentOrientation[indexPath.section][indexPath.row]; -#if CGFLOAT_IS_DOUBLE - return number.doubleValue; -#else - return number.floatValue; -#endif -} - -- (void)invalidateHeightAtIndexPath:(NSIndexPath *)indexPath { - [self buildCachesAtIndexPathsIfNeeded:@[indexPath]]; - [self enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - heightsBySection[indexPath.section][indexPath.row] = @-1; - }]; -} - -- (void)invalidateAllHeightCache { - [self enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - [heightsBySection removeAllObjects]; - }]; -} - -- (void)buildCachesAtIndexPathsIfNeeded:(NSArray *)indexPaths { - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - [self buildSectionsIfNeeded:indexPath.section]; - [self buildRowsIfNeeded:indexPath.row inExistSection:indexPath.section]; - }]; -} - -- (void)buildSectionsIfNeeded:(NSInteger)targetSection { - [self enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - for (NSInteger section = 0; section <= targetSection; ++section) { - if (section >= heightsBySection.count) { - heightsBySection[section] = [NSMutableArray array]; - } - } - }]; -} - -- (void)buildRowsIfNeeded:(NSInteger)targetRow inExistSection:(NSInteger)section { - [self enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - NSMutableArray *heightsByRow = heightsBySection[section]; - for (NSInteger row = 0; row <= targetRow; ++row) { - if (row >= heightsByRow.count) { - heightsByRow[row] = @(-1); - } - } - }]; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"%@, heightsBySectionForPortrait = %@, heightsBySectionForLandscape = %@", [super description], _heightsBySectionForPortrait, _heightsBySectionForLandscape]; -} - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUICollectionViewPagingLayout.h b/QMUI/QMUIKit/UIKitExtensions/QMUICollectionViewPagingLayout.h deleted file mode 100644 index 411487cc..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUICollectionViewPagingLayout.h +++ /dev/null @@ -1,72 +0,0 @@ -// -// QMUICollectionViewPagingLayout.h -// qmui -// -// Created by QQMail on 15/9/24. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import - -typedef NS_ENUM(NSInteger, QMUICollectionViewPagingLayoutStyle) { - QMUICollectionViewPagingLayoutStyleDefault, // 普通模式,水平滑动 - QMUICollectionViewPagingLayoutStyleScale, // 缩放模式,两边的item会小一点,逐渐向中间放大 - QMUICollectionViewPagingLayoutStyleRotation // 旋转模式,围绕底部某个点为中心旋转 -}; - -/** - * 支持按页横向滚动的 UICollectionViewLayout,可切换不同类型的滚动动画。 - */ -@interface QMUICollectionViewPagingLayout : UICollectionViewFlowLayout - -- (instancetype)initWithStyle:(QMUICollectionViewPagingLayoutStyle)style NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, assign, readonly) QMUICollectionViewPagingLayoutStyle style; - -/** - * 规定超过这个滚动速度就强制翻页,从而使翻页更容易触发。默认为 0.4 - */ -@property(nonatomic, assign) CGFloat velocityForEnsurePageDown; - -/** - * 是否支持一次滑动可以滚动多个 item,默认为 YES - */ -@property(nonatomic, assign) BOOL allowsMultipleItemScroll; - -/** - * 规定了当支持一次滑动允许滚动多个 item 的时候,滑动速度要达到多少才会滚动多个 item,默认为 0.7 - * - * 仅当 allowsMultipleItemScroll 为 YES 时生效 - */ -@property(nonatomic, assign) CGFloat mutipleItemScrollVelocityLimit; - -@end - - -@interface QMUICollectionViewPagingLayout (ScaleStyle) - -/** - * 中间那张卡片基于初始大小的缩放倍数,默认为 1.0 - */ -@property(nonatomic, assign) CGFloat maximumScale; - -/** - * 除了中间之外的其他卡片基于初始大小的缩放倍数,默认为 0.9 - */ -@property(nonatomic, assign) CGFloat minimumScale; -@end - - -extern const CGFloat QMUICollectionViewPagingLayoutRotationRadiusAutomatic; - -@interface QMUICollectionViewPagingLayout (RotationStyle) - -/** - * 旋转卡片相关 - * 左右两个卡片最终旋转的角度有 rotationRadius * 90 计算出来 - * rotationRadius表示旋转的半径 - * @warning 仅当 style 为 QMUICollectionViewPagingLayoutStyleRotation 时才生效 - */ -@property(nonatomic, assign) CGFloat rotationRatio; -@property(nonatomic, assign) CGFloat rotationRadius; -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUICollectionViewPagingLayout.m b/QMUI/QMUIKit/UIKitExtensions/QMUICollectionViewPagingLayout.m deleted file mode 100644 index 8ddeca60..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUICollectionViewPagingLayout.m +++ /dev/null @@ -1,189 +0,0 @@ -// -// QMUICollectionViewPagingLayout.m -// qmui -// -// Created by QQMail on 15/9/24. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import "QMUICollectionViewPagingLayout.h" -#import "QMUICore.h" - -@interface QMUICollectionViewPagingLayout () { - CGFloat _maximumScale; - CGFloat _minimumScale; - CGFloat _rotationRatio; - CGFloat _rotationRadius; - CGSize _finalItemSize; -} - -@end - -@implementation QMUICollectionViewPagingLayout (ScaleStyle) - -- (CGFloat)maximumScale { - return _maximumScale; -} - -- (void)setMaximumScale:(CGFloat)maximumScale { - _maximumScale = maximumScale; -} - -- (CGFloat)minimumScale { - return _minimumScale; -} - -- (void)setMinimumScale:(CGFloat)minimumScale { - _minimumScale = minimumScale; -} - -@end - -const CGFloat QMUICollectionViewPagingLayoutRotationRadiusAutomatic = -1.0; - -@implementation QMUICollectionViewPagingLayout (RotationStyle) - -- (CGFloat)rotationRatio { - return _rotationRatio; -} - -- (void)setRotationRatio:(CGFloat)rotationRatio { - _rotationRatio = [self validatedRotationRatio:rotationRatio]; -} - -- (CGFloat)rotationRadius { - return _rotationRadius; -} - -- (void)setRotationRadius:(CGFloat)rotationRadius { - _rotationRadius = rotationRadius; -} - -- (CGFloat)validatedRotationRatio:(CGFloat)rotationRatio { - return fmaxf(0.0, fminf(1.0, rotationRatio)); -} - -@end - -@implementation QMUICollectionViewPagingLayout - -- (instancetype)initWithStyle:(QMUICollectionViewPagingLayoutStyle)style { - if (self = [super init]) { - _style = style; - self.velocityForEnsurePageDown = 0.4; - self.allowsMultipleItemScroll = YES; - self.mutipleItemScrollVelocityLimit = 0.7; - self.maximumScale = 1.0; - self.minimumScale = 0.94; - self.rotationRatio = .5; - self.rotationRadius = QMUICollectionViewPagingLayoutRotationRadiusAutomatic; - - self.minimumInteritemSpacing = 0; - self.scrollDirection = UICollectionViewScrollDirectionHorizontal; - } - return self; -} - -- (instancetype)init { - return [self initWithStyle:QMUICollectionViewPagingLayoutStyleDefault]; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - return [self init]; -} - -- (void)prepareLayout { - [super prepareLayout]; - CGSize itemSize = self.itemSize; - id layoutDelegate = (id)self.collectionView.delegate; - if ([layoutDelegate respondsToSelector:@selector(collectionView:layout:sizeForItemAtIndexPath:)]) { - itemSize = [layoutDelegate collectionView:self.collectionView layout:self sizeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; - } - _finalItemSize = itemSize; -} - -- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { - if (self.style == QMUICollectionViewPagingLayoutStyleScale || self.style == QMUICollectionViewPagingLayoutStyleRotation) { - return YES; - } - return !CGSizeEqualToSize(self.collectionView.bounds.size, newBounds.size); -} - -- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { - if (self.style == QMUICollectionViewPagingLayoutStyleDefault) { - return [super layoutAttributesForElementsInRect:rect]; - } - - NSArray *resultAttributes = [[NSArray alloc] initWithArray:[super layoutAttributesForElementsInRect:rect] copyItems:YES]; - CGFloat offset = CGRectGetMidX(self.collectionView.bounds);// 当前滚动位置的可视区域的中心点 - CGSize itemSize = _finalItemSize; - - if (self.style == QMUICollectionViewPagingLayoutStyleScale) { - - CGFloat distanceForMinimumScale = itemSize.width + self.minimumLineSpacing; - CGFloat distanceForMaximumScale = 0.0; - - for (UICollectionViewLayoutAttributes *attributes in resultAttributes) { - CGFloat scale = 0; - CGFloat distance = fabs(offset - attributes.center.x); - if (distance >= distanceForMinimumScale) { - scale = self.minimumScale; - } else if (distance == distanceForMaximumScale) { - scale = self.maximumScale; - } else { - scale = self.minimumScale + (distanceForMinimumScale - distance) * (self.maximumScale - self.minimumScale) / (distanceForMinimumScale - distanceForMaximumScale); - } - attributes.transform3D = CATransform3DMakeScale(scale, scale, 1); - attributes.zIndex = 1; - } - return resultAttributes; - } - - if (self.style == QMUICollectionViewPagingLayoutStyleRotation) { - if (self.rotationRadius == QMUICollectionViewPagingLayoutRotationRadiusAutomatic) { - self.rotationRadius = itemSize.height; - } - UICollectionViewLayoutAttributes *centerAttribute = nil; - CGFloat centerMin = 10000; - for (UICollectionViewLayoutAttributes *attributes in resultAttributes) { - CGFloat distance = self.collectionView.contentOffset.x + CGRectGetWidth(self.collectionView.bounds) / 2.0 - attributes.center.x; - CGFloat degress = - 90 * self.rotationRatio * (distance / CGRectGetWidth(self.collectionView.bounds)); - CGFloat cosValue = fabs(cosf(AngleWithDegrees(degress))); - CGFloat translateY = self.rotationRadius - self.rotationRadius * cosValue; - CGAffineTransform transform = CGAffineTransformMakeTranslation(0, translateY); - transform = CGAffineTransformRotate(transform, AngleWithDegrees(degress)); - attributes.transform = transform; - attributes.zIndex = 1; - if (fabs(distance) < centerMin) { - centerMin = fabs(distance); - centerAttribute = attributes; - } - } - centerAttribute.zIndex = 10; - return resultAttributes; - } - - return resultAttributes; -} - -- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity { - CGFloat itemSpacing = _finalItemSize.width + self.minimumLineSpacing; - - if (!self.allowsMultipleItemScroll || fabs(velocity.x) <= fabs(self.mutipleItemScrollVelocityLimit)) { - // 只滚动一页 - - if (fabs(velocity.x) > self.velocityForEnsurePageDown) { - // 为了更容易触发翻页,这里主动增加滚动位置 - BOOL scrollingToRight = proposedContentOffset.x < self.collectionView.contentOffset.x; - proposedContentOffset = CGPointMake(self.collectionView.contentOffset.x + (itemSpacing / 2) * (scrollingToRight ? -1 : 1), self.collectionView.contentOffset.y); - } else { - proposedContentOffset = self.collectionView.contentOffset; - } - } - - proposedContentOffset.x = round(proposedContentOffset.x / itemSpacing) * itemSpacing; - - return proposedContentOffset; -} - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUILabel.h b/QMUI/QMUIKit/UIKitExtensions/QMUILabel.h deleted file mode 100644 index 7dc3ce0b..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUILabel.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// QMUILabel.h -// qmui -// -// Created by QQMail on 14-7-3. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import - -/** - * `QMUILabel`支持通过`contentEdgeInsets`属性来实现类似padding的效果。 - * - * 同时通过将`canPerformCopyAction`置为`YES`来开启长按复制文本的功能,长按时label的背景色默认为`highlightedBackgroundColor` - */ -@interface QMUILabel : UILabel - -/// 控制label内容的padding,默认为UIEdgeInsetsZero -@property(nonatomic,assign) UIEdgeInsets contentEdgeInsets; - -/// 是否需要长按复制的功能,默认为 NO。 -/// 长按时的背景色通过`highlightedBackgroundColor`设置。 -@property(nonatomic,assign) IBInspectable BOOL canPerformCopyAction; - -/// 如果打开了`canPerformCopyAction`,则长按时背景色将会被改为`highlightedBackgroundColor` -@property(nonatomic,strong) IBInspectable UIColor *highlightedBackgroundColor; - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUILabel.m b/QMUI/QMUIKit/UIKitExtensions/QMUILabel.m deleted file mode 100644 index 3542b2ee..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUILabel.m +++ /dev/null @@ -1,120 +0,0 @@ -// -// QMUILabel.m -// qmui -// -// Created by QQMail on 14-7-3. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUILabel.h" -#import "QMUICore.h" - -@interface QMUILabel () - -@property(nonatomic, strong) UIColor *tempBackgroundColor; -@property(nonatomic, strong) UILongPressGestureRecognizer *longGestureRecognizer; -@end - - -@implementation QMUILabel - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (CGSize)sizeThatFits:(CGSize)size { - size = [super sizeThatFits:CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets), size.height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets))]; - size.width += UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets); - size.height += UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets); - return size; -} - -- (void)drawTextInRect:(CGRect)rect { - return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, self.contentEdgeInsets)]; -} - -- (void)setHighlightedBackgroundColor:(UIColor *)highlightedBackgroundColor { - if (highlightedBackgroundColor) { - self.tempBackgroundColor = self.backgroundColor; - _highlightedBackgroundColor = highlightedBackgroundColor; - } -} - -- (void)setHighlighted:(BOOL)highlighted { - [super setHighlighted:highlighted]; - if (self.highlightedBackgroundColor) { - self.backgroundColor = highlighted ? self.highlightedBackgroundColor : self.tempBackgroundColor; - } -} - -#pragma mark - 长按复制功能 - -- (void)setCanPerformCopyAction:(BOOL)canPerformCopyAction { - _canPerformCopyAction = canPerformCopyAction; - if (_canPerformCopyAction && !self.longGestureRecognizer) { - self.userInteractionEnabled = YES; - self.longGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGestureRecognizer:)]; - [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; - self.userInteractionEnabled = NO; - - [[NSNotificationCenter defaultCenter] removeObserver:self]; - } -} - -- (BOOL)canBecomeFirstResponder { - return self.canPerformCopyAction; -} - -- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { - if ([self canBecomeFirstResponder]) { - return action == @selector(copyString:); - } - return NO; -} - -- (void)copyString:(id)sender { - if (self.canPerformCopyAction) { - UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; - if (self.text) { - pasteboard.string = self.text; - } - } -} - -- (void)handleLongPressGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer { - if (!self.canPerformCopyAction) { - return; - } - if (gestureRecognizer.state == UIGestureRecognizerStateBegan) { - [self becomeFirstResponder]; - UIMenuController *menuController = [UIMenuController sharedMenuController]; - UIMenuItem *copyMenuItem = [[UIMenuItem alloc] initWithTitle:@"复制" action:@selector(copyString:)]; - [[UIMenuController sharedMenuController] setMenuItems:@[copyMenuItem]]; - [menuController setTargetRect:self.frame inView:self.superview]; - [menuController setMenuVisible:YES animated:YES]; - - // 默认背景色 - self.tempBackgroundColor = self.backgroundColor; - self.backgroundColor = self.highlightedBackgroundColor; - } -} - -- (void)handleMenuWillHideNotification:(NSNotification *)notification { - if (!self.canPerformCopyAction) { - return; - } - if (self.tempBackgroundColor) { - self.backgroundColor = self.tempBackgroundColor; - } -} - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUISearchBar.h b/QMUI/QMUIKit/UIKitExtensions/QMUISearchBar.h deleted file mode 100644 index 002c2cea..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUISearchBar.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QMUISearchBar.h -// qmui -// -// Created by MoLice on 14-7-2. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import - -@interface QMUISearchBar : UISearchBar - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUISearchBar.m b/QMUI/QMUIKit/UIKitExtensions/QMUISearchBar.m deleted file mode 100644 index dab270d7..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUISearchBar.m +++ /dev/null @@ -1,33 +0,0 @@ -// -// QMUISearchBar.m -// qmui -// -// Created by MoLice on 14-7-2. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUISearchBar.h" -#import "UISearchBar+QMUI.h" - -@implementation QMUISearchBar - -- (instancetype)initWithFrame:(CGRect)frame { - if (self = [super initWithFrame:frame]) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)coder { - self = [super initWithCoder:coder]; - if (self) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - [self qmui_styledAsQMUISearchBar]; -} - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUISearchController.h b/QMUI/QMUIKit/UIKitExtensions/QMUISearchController.h deleted file mode 100644 index 1b7f3a7e..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUISearchController.h +++ /dev/null @@ -1,81 +0,0 @@ -// -// QMUISearchController.h -// Test -// -// Created by MoLice on 16/5/25. -// Copyright © 2016年 MoLice. All rights reserved. -// - -#import -#import "QMUICommonViewController.h" - -@class QMUIEmptyView; -@class QMUISearchController; - -/** - * 配合 QMUISearchController 使用的 protocol,主要负责两件事情: - * - * 1. 响应用户的输入,在搜索框内的文字发生变化后被调用,可在 searchController:updateResultsForSearchString: 方法内更新搜索结果的数据集,在里面请自行调用 [searchController.tableView reloadData] - * 2. 渲染最终用于显示搜索结果的 UITableView 的数据,该 tableView 的 delegate、dataSource 均包含在这个 protocol 里 - */ -@protocol QMUISearchControllerDelegate - -@required -/** - * 搜索框文字发生变化时的回调,请自行调用 `[tableView reloadData]` 来更新界面。 - * @warning 搜索框文字为空(例如第一次点击搜索框进入搜索状态时,或者文字全被删掉了,或者点击搜索框的×)也会走进来,此时参数searchString为@"",这是为了和系统的UISearchController保持一致 - */ -- (void)searchController:(QMUISearchController *)searchController updateResultsForSearchString:(NSString *)searchString; - -@optional -- (void)willPresentSearchController:(QMUISearchController *)searchController; -- (void)didPresentSearchController:(QMUISearchController *)searchController; -- (void)willDismissSearchController:(QMUISearchController *)searchController; -- (void)didDismissSearchController:(QMUISearchController *)searchController; -- (void)searchController:(QMUISearchController *)searchController didLoadSearchResultsTableView:(UITableView *)tableView; -- (void)searchController:(QMUISearchController *)searchController willShowEmptyView:(QMUIEmptyView *)emptyView; - -@end - -/** - * 兼容 iOS 7 及以后的版本的 searchController,在 iOS7 下会使用 UISearchDisplayController 实现,在 iOS 8 及以后会使用 UISearchController 实现。 - * 支持在搜索文字为空时(注意并非“搜索结果为空”)显示一个界面,例如常见的“最近搜索”功能,具体请查看属性 launchView。 - * 使用方法: - * 1. 使用 initWithContentsViewController: 初始化 - * 2. 通过 searchBar 属性得到搜索框的引用并直接使用,例如 `tableHeaderView = searchController.searchBar` - * 3. 指定 searchResultsDelegate 属性并在其中实现 searchController:updateResultsForSearchString: 方法以更新搜索结果数据集 - * - * @note QMUICommonTableViewController 内部自带 QMUISearchController,只需将属性 shouldShowSearchBar 置为 YES 即可,无需自行初始化 QMUISearchController。 - */ -@interface QMUISearchController : QMUICommonViewController - -/** - * 在某个指定的UIViewController上创建一个与其绑定的searchController - * @param viewController 要在哪个viewController上添加搜索功能 - */ -- (instancetype)initWithContentsViewController:(UIViewController *)viewController; - -@property(nonatomic, weak) id searchResultsDelegate; - -/// 搜索框,在 iOS 7 下是 QMUISearchBar,在 iOS 8 及以后是 UISearchBar -@property(nonatomic, strong, readonly) UISearchBar *searchBar; - -/// 搜索结果列表,在 iOS 7 下是 UITableView,并且每次进行搜索时指针都会发生变化(系统如此),在 iOS 8 及以后是 QMUITableView -@property(nonatomic, strong, readonly) UITableView *tableView; - -/// 在搜索文字为空时会展示的一个 view,通常用于实现“最近搜索”之类的功能。launchView 最终会被布局为撑满搜索框以下的所有空间。 -@property(nonatomic, strong) UIView *launchView; - -/// 控制以无动画的形式进入/退出搜索状态 -@property(nonatomic, assign, getter=isActive) BOOL active; - -/** - * 控制进入/退出搜索状态 - * @param active YES 表示进入搜索状态,NO 表示退出搜索状态 - * @param animated 是否要以动画的形式展示状态切换 - */ -- (void)setActive:(BOOL)active animated:(BOOL)animated; - -/// 进入搜索状态时是否要把原界面的 navigationBar 推走,默认为 YES,仅在 iOS 8 及以后有效 -@property(nonatomic, assign) BOOL hidesNavigationBarDuringPresentation; -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUISearchController.m b/QMUI/QMUIKit/UIKitExtensions/QMUISearchController.m deleted file mode 100644 index 4d8c2829..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUISearchController.m +++ /dev/null @@ -1,434 +0,0 @@ -// -// QMUISearchController.m -// Test -// -// Created by MoLice on 16/5/25. -// Copyright © 2016年 MoLice. All rights reserved. -// - -#import "QMUISearchController.h" -#import "QMUICore.h" -#import "QMUISearchBar.h" -#import "QMUICommonTableViewController.h" -#import "QMUIEmptyView.h" -#import "UISearchBar+QMUI.h" -#import "UITableView+QMUI.h" -#import "NSString+QMUI.h" -#import "NSObject+QMUI.h" -#import "UIView+QMUI.h" - -BeginIgnoreDeprecatedWarning - -@class QMUISearchResultsTableViewController; - -@protocol QMUISearchResultsTableViewControllerDelegate - -- (void)didLoadTableViewInSearchResultsTableViewController:(QMUISearchResultsTableViewController *)viewController; -@end - -@interface QMUISearchResultsTableViewController : QMUICommonTableViewController - -@property(nonatomic,weak) id delegate; -@end - -@implementation QMUISearchResultsTableViewController - -- (void)initTableView { - [super initTableView]; - self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; - if ([self.delegate respondsToSelector:@selector(didLoadTableViewInSearchResultsTableViewController:)]) { - [self.delegate didLoadTableViewInSearchResultsTableViewController:self]; - } -} - -@end - -@interface QMUISearchDisplayController : UISearchDisplayController - -@property(nonatomic, strong) UIView *customDimmingView; -@end - -@implementation QMUISearchDisplayController - -- (void)setActive:(BOOL)visible animated:(BOOL)animated { - [super setActive:visible animated:animated]; - if (self.customDimmingView.superview) { - BOOL shouldChangeSize = !CGSizeEqualToSize(self.customDimmingView.frame.size, self.customDimmingView.superview.bounds.size); - [UIView qmui_animateWithAnimated:animated duration:[CATransaction animationDuration] animations:^{ - self.customDimmingView.superview.alpha = visible ? 1 : 0; - if (shouldChangeSize) { - self.customDimmingView.frame = self.customDimmingView.superview.bounds; - } - }]; - } -} - -- (void)setCustomDimmingView:(UIView *)customDimmingView { - if (_customDimmingView != customDimmingView) { - [_customDimmingView removeFromSuperview]; - } - _customDimmingView = customDimmingView; -} - -- (UIColor *)_dimmingViewColor { - if (self.customDimmingView) { - BeginIgnorePerformSelectorLeaksWarning - UIView *containerView = [self performSelector:NSSelectorFromString(@"_containerView")]; - EndIgnorePerformSelectorLeaksWarning - UIView *superviewOfDimmingView = containerView.subviews.lastObject; - UIView *defaultDimmingView = superviewOfDimmingView.subviews.firstObject; - if (defaultDimmingView) { - defaultDimmingView.alpha = 1; - self.customDimmingView.frame = defaultDimmingView.bounds; - if (self.customDimmingView.superview != defaultDimmingView) { - [defaultDimmingView addSubview:self.customDimmingView]; - } - } - } - - return [self qmui_performSelectorToSuperclass:_cmd]; -} - -@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]; - }]; - } -} - -@end - -@interface QMUISearchController () - -// iOS 8 及以后使用这个 -@property(nonatomic,strong) QMUICustomSearchController *searchController; - -// iOS 7 及以前使用这个 -@property(nonatomic,strong) QMUISearchDisplayController *searchDisplayController; -@end - -@implementation QMUISearchController - -- (instancetype)initWithContentsViewController:(UIViewController *)viewController { - if (self = [self initWithNibName:nil bundle:nil]) { - if (NSStringFromClass([UISearchController class])) { - // 将 definesPresentationContext 置为 YES 有两个作用: - // 1、保证从搜索结果界面进入子界面后,顶部的searchBar不会依然停留在navigationBar上 - // 2、使搜索结果界面的tableView的contentInset.top正确适配searchBar - viewController.definesPresentationContext = YES; - - QMUISearchResultsTableViewController *searchResultsViewController = [[QMUISearchResultsTableViewController alloc] init]; - searchResultsViewController.delegate = self; - self.searchController = [[QMUICustomSearchController alloc] initWithSearchResultsController:searchResultsViewController]; - self.searchController.searchResultsUpdater = self; - self.searchController.delegate = self; - _searchBar = self.searchController.searchBar; - if (CGRectIsEmpty(self.searchBar.frame)) { - // iOS8 下 searchBar.frame 默认是 CGRectZero,不 sizeToFit 就看不到了 - [self.searchBar sizeToFit]; - } - [self.searchBar qmui_styledAsQMUISearchBar]; - } else { - _searchBar = [[QMUISearchBar alloc] init]; - self.searchDisplayController = [[QMUISearchDisplayController alloc] initWithSearchBar:self.searchBar contentsController:viewController]; - self.searchDisplayController.delegate = self; - } - - self.hidesNavigationBarDuringPresentation = YES; - } - return self; -} - -- (void)dealloc { - self.searchDisplayController.delegate = nil; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - // 主动触发 loadView,如果不这么做,那么有可能直到 QMUISearchController 被销毁,这期间 self.searchController 都没有被触发 loadView,然后在 dealloc 时就会报错,提示尝试在释放 self.searchController 时触发了 self.searchController 的 loadView - [self.searchController loadViewIfNeeded]; -} - -- (void)setSearchResultsDelegate:(id)searchResultsDelegate { - _searchResultsDelegate = searchResultsDelegate; - - if (self.searchController) { - self.tableView.dataSource = _searchResultsDelegate; - self.tableView.delegate = _searchResultsDelegate; - } else { - self.searchDisplayController.searchResultsDataSource = _searchResultsDelegate; - self.searchDisplayController.searchResultsDelegate = _searchResultsDelegate; - } -} - -- (BOOL)isActive { - if (self.searchController) { - return self.searchController.active; - } else { - return self.searchDisplayController.active; - } -} - -- (void)setActive:(BOOL)active { - [self setActive:active animated:NO]; -} - -- (void)setActive:(BOOL)active animated:(BOOL)animated { - if (self.searchController) { - self.searchController.active = active; - } else { - [self.searchDisplayController setActive:active animated:animated]; - } -} - -- (UITableView *)tableView { - if (self.searchController) { - return ((QMUICommonTableViewController *)self.searchController.searchResultsController).tableView; - } else { - return self.searchDisplayController.searchResultsTableView; - } -} - -- (void)removeDefaultEmptyLabelInSearchDisplayController { - // 移除 UISearchDisplayController 自带的“无结果”的label - for (UIView *subview in self.searchDisplayController.searchResultsTableView.subviews) { - if ([subview isKindOfClass:[UILabel class]]) { - [subview removeFromSuperview]; - subview.hidden = YES; - break; - } - } -} - -- (void)setLaunchView:(UIView *)dimmingView { - _launchView = dimmingView; - - if (self.searchController) { - self.searchController.customDimmingView = _launchView; - } else { - self.searchDisplayController.customDimmingView = _launchView; - } -} - -- (BOOL)hidesNavigationBarDuringPresentation { - if (self.searchController) { - return self.searchController.hidesNavigationBarDuringPresentation; - } else { - NSLog(@"%s 仅支持 iOS 8 及以上版本", __func__); - return YES; - } -} - -- (void)setHidesNavigationBarDuringPresentation:(BOOL)hidesNavigationBarDuringPresentation { - if (self.searchController) { - self.searchController.hidesNavigationBarDuringPresentation = hidesNavigationBarDuringPresentation; - } else { - NSLog(@"%s 仅支持 iOS 8 及以上版本", __func__); - } -} - -#pragma mark - QMUIEmptyView - -- (void)showEmptyView { - // 搜索框文字为空时,界面会显示遮罩,此时不需要显示emptyView了 - // 为什么加这个是因为当搜索框被点击时(进入搜索状态)会触发searchController:updateResultsForSearchString:,里面如果直接根据搜索结果为空来showEmptyView的话,就会导致在遮罩层上有emptyView出现,要么在那边showEmptyView之前判断一下searchBar.text.length,要么在showEmptyView里判断,为了方便,这里选择后者。 - if (self.searchBar.text.length <= 0) { - return; - } - - [super showEmptyView]; - - // 格式化样式,以适应当前项目的需求 - self.emptyView.backgroundColor = TableViewBackgroundColor ?: UIColorWhite; - if ([self.searchResultsDelegate respondsToSelector:@selector(searchController:willShowEmptyView:)]) { - [self.searchResultsDelegate searchController:self willShowEmptyView:self.emptyView]; - } - - if (self.searchController) { - UIView *superview = self.searchController.searchResultsController.view; - [superview addSubview:self.emptyView]; - } else if (self.searchDisplayController) { - // 加到searchResultsTableView里的好处是当搜索框的文字被清空时,搜索界面会出现黑色半透明遮罩,此时searchResultsTableView会被隐藏掉,刚好上面的emptyView也就看不到了 - UIView *superview = self.searchDisplayController.searchResultsTableView; - [superview addSubview:self.emptyView]; - } else { - NSAssert(NO, @"QMUISearchController无法为emptyView找到合适的superview"); - } - - [self layoutEmptyView]; -} - -- (BOOL)layoutEmptyView { - if ([self.emptyView.superview isKindOfClass:[UITableView class]]) { - // iOS7 UISearchDisplayController 里,会把emptyView加到searchResultsTableView上,参照showEmptyView里的代码 - UITableView *tableView = (UITableView *)self.emptyView.superview; - CGSize newEmptyViewSize = CGSizeMake(CGRectGetWidth(tableView.bounds) - UIEdgeInsetsGetHorizontalValue(tableView.contentInset), CGRectGetHeight(tableView.frame) - UIEdgeInsetsGetVerticalValue(tableView.contentInset)); - CGSize oldEmptyViewSize = self.emptyView.frame.size; - if (!CGSizeEqualToSize(newEmptyViewSize, oldEmptyViewSize)) { - self.emptyView.frame = CGRectMake(CGRectGetMinX(self.emptyView.frame), CGRectGetMinY(self.emptyView.frame), newEmptyViewSize.width, newEmptyViewSize.height); - } - return YES; - } else { - return [super layoutEmptyView]; - } -} - -#pragma mark - - -- (void)didLoadTableViewInSearchResultsTableViewController:(QMUISearchResultsTableViewController *)viewController { - if ([self.searchResultsDelegate respondsToSelector:@selector(searchController:didLoadSearchResultsTableView:)]) { - [self.searchResultsDelegate searchController:self didLoadSearchResultsTableView:viewController.tableView]; - } -} - -#pragma mark - - -- (void)updateSearchResultsForSearchController:(UISearchController *)searchController { - if ([self.searchResultsDelegate respondsToSelector:@selector(searchController:updateResultsForSearchString:)]) { - [self.searchResultsDelegate searchController:self updateResultsForSearchString:searchController.searchBar.text]; - } -} - -#pragma mark - - -- (void)willPresentSearchController:(UISearchController *)searchController { - if ([self.searchResultsDelegate respondsToSelector:@selector(willPresentSearchController:)]) { - [self.searchResultsDelegate willPresentSearchController:self]; - } -} - -- (void)didPresentSearchController:(UISearchController *)searchController { - if ([self.searchResultsDelegate respondsToSelector:@selector(didPresentSearchController:)]) { - [self.searchResultsDelegate didPresentSearchController:self]; - } -} - -- (void)willDismissSearchController:(UISearchController *)searchController { - if ([self.searchResultsDelegate respondsToSelector:@selector(willDismissSearchController:)]) { - [self.searchResultsDelegate willDismissSearchController:self]; - } -} - -- (void)didDismissSearchController:(UISearchController *)searchController { - // 退出搜索必定先隐藏emptyView - [self hideEmptyView]; - - if ([self.searchResultsDelegate respondsToSelector:@selector(didDismissSearchController:)]) { - [self.searchResultsDelegate didDismissSearchController:self]; - } -} - -#pragma mark - - -- (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller { - if ([self.searchResultsDelegate respondsToSelector:@selector(willPresentSearchController:)]) { - [self.searchResultsDelegate willPresentSearchController:self]; - } - - // UISearchController在点击搜索框进入搜索状态时,会调用updateSearchResults,为了让iOS7下也保持一致的调用时机,这里补了这句 - [self searchDisplayController:controller shouldReloadTableForSearchString:@""]; -} - -- (void)searchDisplayControllerDidBeginSearch:(UISearchDisplayController *)controller { - if ([self.searchResultsDelegate respondsToSelector:@selector(didPresentSearchController:)]) { - [self.searchResultsDelegate didPresentSearchController:self]; - } -} - -- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller { - if ([self.searchResultsDelegate respondsToSelector:@selector(willDismissSearchController:)]) { - [self.searchResultsDelegate willDismissSearchController:self]; - } -} - -- (void)searchDisplayControllerDidEndSearch:(UISearchDisplayController *)controller { - // 退出搜索必定先隐藏emptyView - [self hideEmptyView]; - - if ([self.searchResultsDelegate respondsToSelector:@selector(didDismissSearchController:)]) { - [self.searchResultsDelegate didDismissSearchController:self]; - } -} - -- (void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView { - [tableView qmui_styledAsQMUITableView]; - tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; - - if ([self.searchResultsDelegate respondsToSelector:@selector(searchController:didLoadSearchResultsTableView:)]) { - [self.searchResultsDelegate searchController:self didLoadSearchResultsTableView:tableView]; - } -} - -- (void)searchDisplayController:(UISearchDisplayController *)controller didShowSearchResultsTableView:(UITableView *)tableView { - // 移除搜索框底部的阴影 - for (UIView *subview in tableView.subviews) { - if ([NSStringFromClass(subview.class) isEqualToString:@"_UISearchBarShadowView"]) { - subview.hidden = YES; - // 用hidden而不要用removeFromSuperview,后者会导致subview被释放,从而可能产生野指针 -// [subview removeFromSuperview]; - break; - } - } -} - -- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(nullable NSString *)searchString { - [self removeDefaultEmptyLabelInSearchDisplayController]; - - if ([self.searchResultsDelegate respondsToSelector:@selector(searchController:updateResultsForSearchString:)]) { - [self.searchResultsDelegate searchController:self updateResultsForSearchString:self.searchBar.text]; - } - return YES; -} - -@end - -EndIgnoreDeprecatedWarning diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUISlider.h b/QMUI/QMUIKit/UIKitExtensions/QMUISlider.h deleted file mode 100644 index bba2e3cd..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUISlider.h +++ /dev/null @@ -1,39 +0,0 @@ -// -// QMUISlider.h -// qmui -// -// Created by MoLice on 2017/6/1. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#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/UIKitExtensions/QMUISlider.m b/QMUI/QMUIKit/UIKitExtensions/QMUISlider.m deleted file mode 100644 index 03679463..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUISlider.m +++ /dev/null @@ -1,89 +0,0 @@ -// -// QMUISlider.m -// qmui -// -// Created by MoLice on 2017/6/1. -// Copyright © 2017年 QMUI Team. All rights reserved. -// - -#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/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/QMUITableView.h b/QMUI/QMUIKit/UIKitExtensions/QMUITableView.h deleted file mode 100644 index 8c72ff53..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUITableView.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// QMUITableView.h -// qmui -// -// Created by QQMail on 14-7-2. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUITableViewProtocols.h" - -@interface QMUITableView : UITableView - -@property(nonatomic, weak) id delegate; -@property(nonatomic, weak) id dataSource; - -@end - diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUITableView.m b/QMUI/QMUIKit/UIKitExtensions/QMUITableView.m deleted file mode 100644 index 5850cb18..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUITableView.m +++ /dev/null @@ -1,80 +0,0 @@ -// -// QMUITableView.m -// qmui -// -// Created by QQMail on 14-7-2. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUITableView.h" -#import "UITableView+QMUI.h" - -@implementation QMUITableView - -@dynamic delegate; -@dynamic dataSource; - -- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style { - if (self = [super initWithFrame:frame style:style]) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - [self qmui_styledAsQMUITableView]; -} - -- (void)dealloc { - self.delegate = nil; - self.dataSource = nil; -} - -// 保证一直存在tableFooterView,以去掉列表内容不满一屏时尾部的空白分割线 -- (void)setTableFooterView:(UIView *)tableFooterView { - if (!tableFooterView) { - tableFooterView = [[UIView alloc] init]; - } - [super setTableFooterView:tableFooterView]; -} - -- (BOOL)touchesShouldCancelInContentView:(UIView *)view { - if (self.delegate && [self.delegate respondsToSelector:@selector(tableView:touchesShouldCancelInContentView:)]) { - return [self.delegate tableView:self touchesShouldCancelInContentView:view]; - } - // 默认情况下只有当view是非UIControl的时候才会返回yes,这里统一对UIButton也返回yes - // 原因是UITableView上面把事件延迟去掉了,但是这样如果拖动的时候手指是在UIControl上面的话,就拖动不了了 - if ([view isKindOfClass:[UIControl class]]) { - if ([view isKindOfClass:[UIButton class]]) { - return YES; - } else { - return NO; - } - } - return YES; -} - -#ifdef DEBUG - -- (void)setContentOffset:(CGPoint)contentOffset { - [super setContentOffset:contentOffset]; -} - -- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated { - [super setContentOffset:contentOffset animated:animated]; -} - -- (void)setContentInset:(UIEdgeInsets)contentInset { - [super setContentInset:contentInset]; -} - -#endif - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUITableViewCell.h b/QMUI/QMUIKit/UIKitExtensions/QMUITableViewCell.h deleted file mode 100644 index c3e09d46..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUITableViewCell.h +++ /dev/null @@ -1,74 +0,0 @@ -// -// QMUITableViewCell.h -// qmui -// -// Created by QQMail on 14-7-7. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import -#import "UITableView+QMUI.h" - - -@interface QMUITableViewCell : UITableViewCell - -@property(nonatomic, assign, readonly) UITableViewCellStyle style; - -/** - * imageEdgeInsets,这个属性用来调整imageView里面图片的位置,有些情况titleLabel前面是一个icon,但是icon与titleLabel的间距不是你想要的。
- * @warning 目前只对UITableViewCellStyleDefault和UITableViewCellStyleSubtitle类型的cell开放 - */ -@property(nonatomic, assign) UIEdgeInsets imageEdgeInsets; - -/** - * textLabelEdgeInsets,这个属性和imageEdgeInsets合作使用,用来调整titleLabel的位置,默认为 UIEdgeInsetsZero。
- * @warning 目前只对UITableViewCellStyleDefault和UITableViewCellStyleSubtitle类型的cell开放。 - */ -@property(nonatomic, assign) UIEdgeInsets textLabelEdgeInsets; - -/// 与textLabelEdgeInsets一致,作用目标为detailTextLabel,默认为 UIEdgeInsetsZero。 -@property(nonatomic, assign) UIEdgeInsets detailTextLabelEdgeInsets; - -/// 用于调整右边 accessoryView 的布局偏移,默认为 UIEdgeInsetsZero。 -@property(nonatomic, assign) UIEdgeInsets accessoryEdgeInsets; - -/// 用于调整accessoryView的点击响应区域,可用负值扩大点击范围,默认为(-12, -12, -12, -12) -@property(nonatomic, assign) UIEdgeInsets accessoryHitTestEdgeInsets; - -/// 设置当前cell是否enabled,setter方法里面会修改当前的subviews样式。 -@property(nonatomic, assign, getter = isEnabled) BOOL enabled; - -/// 保存对tableView的弱引用,在布局时可能会使用到tableView的一些属性例如separatorColor等。只有使用下面两个 initForTableView: 的接口初始化时这个属性才有值,否则就只能自己初始化后赋值 -@property(nonatomic, weak) UITableView *parentTableView; - -/** - * cell 处于 section 中的位置,要求: - * 1. cell 使用 initForTableViewXxx 方法初始化,或者初始化完后为 parentTableView 属性赋值。 - * 2. 在 cellForRow 里调用 [cell updateCellAppearanceWithIndexPath:] 方法。 - * 3. 之后即可通过 cellPosition 获取到正确的位置。 - */ -@property(nonatomic, assign, readonly) QMUITableViewCellPosition cellPosition; - -/** - * 首选初始化方法 - * - * @param tableView cell所在的tableView - * @param style tableView的style - * @param reuseIdentifier tableView的reuseIdentifier - * - * @return 一个QMUITableViewCell实例 - */ -- (instancetype)initForTableView:(UITableView *)tableView withStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier; - -/// 同上 -- (instancetype)initForTableView:(UITableView *)tableView withReuseIdentifier:(NSString *)reuseIdentifier; - -@end - - -@interface QMUITableViewCell (QMUISubclassingHooks) - -/// 用于继承的接口,设置一些cell相关的UI,需要自 cellForRowAtIndexPath 里面调用。默认实现是设置当前cell在哪个position。 -- (void)updateCellAppearanceWithIndexPath:(NSIndexPath *)indexPath; - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUITableViewProtocols.h b/QMUI/QMUIKit/UIKitExtensions/QMUITableViewProtocols.h deleted file mode 100644 index 604ea429..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUITableViewProtocols.h +++ /dev/null @@ -1,35 +0,0 @@ -// -// QMUITableViewProtocols.h -// qmui -// -// Created by MoLice on 2016/12/9. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import - -@class QMUITableView; - -@protocol qmui_UITableViewDataSource - -@optional -- (__kindof UITableViewCell *)qmui_tableView:(UITableView *)tableView cellWithIdentifier:(NSString *)identifier; - -@end - -@protocol QMUITableViewDelegate - -@optional - -/** - * 自定义要在- (BOOL)touchesShouldCancelInContentView:(UIView *)view内的逻辑
- * 若delegate不实现这个方法,则默认对所有UIControl返回NO(UIButton除外,它会返回YES),非UIControl返回YES。 - */ -- (BOOL)tableView:(QMUITableView *)tableView touchesShouldCancelInContentView:(UIView *)view; - -@end - - -@protocol QMUITableViewDataSource - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUITextField.h b/QMUI/QMUIKit/UIKitExtensions/QMUITextField.h deleted file mode 100644 index ad9fe4d8..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUITextField.h +++ /dev/null @@ -1,71 +0,0 @@ -// -// QMUITextField.h -// qmui -// -// Created by MoLice on 16-11-03 -// Copyright (c) 2016年 QMUI Team. All rights reserved. -// - -#import - -@class QMUITextField; - -@protocol QMUITextFieldDelegate - -@optional - -/** - * 配合 `maximumTextLength` 属性使用,在输入文字超过限制时被调用。 - * @warning 在 UIControlEventEditingChanged 里也会触发文字长度拦截,由于此时 textField 的文字已经改变完,所以无法得知发生改变的文本位置及改变的文本内容,所以此时 range 和 replacementString 这两个参数的值也会比较特殊,具体请看参数讲解。 - * - * @param textField 触发的 textField - * @param range 要变化的文字的位置,如果在 UIControlEventEditingChanged 里,这里的 range 也即文字变化后的 range,所以可能比最大长度要大。 - * @param replacementString 要变化的文字,如果在 UIControlEventEditingChanged 里,这里永远传入 nil。 - */ -- (void)textField:(QMUITextField *)textField didPreventTextChangeInRange:(NSRange)range replacementString:(NSString *)replacementString; - -@end - -/** - * 支持的特性包括: - * - * 1. 自定义 placeholderColor。 - * 2. 自定义 UITextField 的文字 padding。 - * 3. 支持限制输入的文字的长度。 - * 4. 修复 iOS 10 之后 UITextField 输入中文超过文本框宽度后再删除,文字往下掉的 bug。 - */ -@interface QMUITextField : UITextField - -@property(nonatomic, weak) id delegate; - -/** - * 修改 placeholder 的颜色,默认是 UIColorPlaceholder。 - */ -@property(nonatomic, strong) IBInspectable UIColor *placeholderColor; - -/** - * 文字在输入框内的 padding。如果出现 clearButton,则 textInsets.right 会控制 clearButton 的右边距 - * - * 默认为 TextFieldTextInsets - */ -@property(nonatomic, assign) UIEdgeInsets textInsets; - -/** - * 当通过 `setText:`、`setAttributedText:`等方式修改文字时,是否应该自动触发 UIControlEventEditingChanged 事件及 UITextFieldTextDidChangeNotification 通知。 - * - * 默认为YES(注意系统的 UITextField 对这种行为默认是 NO) - */ -@property(nonatomic, assign) IBInspectable BOOL shouldResponseToProgrammaticallyTextChanges; - -/** - * 显示允许输入的最大文字长度,默认为 NSUIntegerMax,也即不限制长度。 - */ -@property(nonatomic, assign) IBInspectable NSUInteger maximumTextLength; - -/** - * 在使用 maximumTextLength 功能的时候,是否应该把文字长度按照 [NSString (QMUI) qmui_lengthWhenCountingNonASCIICharacterAsTwo] 的方法来计算。 - * 默认为 NO。 - */ -@property(nonatomic, assign) IBInspectable BOOL shouldCountingNonASCIICharacterAsTwo; - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUITextField.m b/QMUI/QMUIKit/UIKitExtensions/QMUITextField.m deleted file mode 100644 index ddfff6ac..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUITextField.m +++ /dev/null @@ -1,245 +0,0 @@ -// -// QMUITextField.m -// qmui -// -// Created by MoLice on 16-11-03 -// Copyright (c) 2016年 QMUI Team. All rights reserved. -// - -#import "QMUITextField.h" -#import "QMUICore.h" -#import "NSString+QMUI.h" -#import "UITextField+QMUI.h" - -@interface QMUITextField () - -@property(nonatomic, weak) id originalDelegate; - -@end - -@implementation QMUITextField - -@dynamic delegate; - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - [self didInitialized]; - self.tintColor = TextFieldTintColor; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - self.delegate = self; - self.placeholderColor = UIColorPlaceholder; - self.textInsets = TextFieldTextInsets; - self.shouldResponseToProgrammaticallyTextChanges = YES; - self.maximumTextLength = NSUIntegerMax; - [self addTarget:self action:@selector(handleTextChangeEvent:) forControlEvents:UIControlEventEditingChanged]; -} - -- (void)dealloc { - self.delegate = nil; - self.originalDelegate = nil; -} - -#pragma mark - Placeholder - -- (void)setPlaceholderColor:(UIColor *)placeholderColor { - _placeholderColor = placeholderColor; - if (self.placeholder) { - [self updateAttributedPlaceholderIfNeeded]; - } -} - -- (void)setPlaceholder:(NSString *)placeholder { - [super setPlaceholder:placeholder]; - if (self.placeholderColor) { - [self updateAttributedPlaceholderIfNeeded]; - } -} - -- (void)updateAttributedPlaceholderIfNeeded { - self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder attributes:@{NSForegroundColorAttributeName: self.placeholderColor}]; -} - -#pragma mark - TextInsets - -- (CGRect)textRectForBounds:(CGRect)bounds { - bounds = CGRectInsetEdges(bounds, self.textInsets); - CGRect resultRect = [super textRectForBounds:bounds]; - return resultRect; -} - -- (CGRect)editingRectForBounds:(CGRect)bounds { - bounds = CGRectInsetEdges(bounds, self.textInsets); - return [super editingRectForBounds:bounds]; -} - -#pragma mark - TextPosition - -- (void)layoutSubviews { - [super layoutSubviews]; - - // 以下代码修复系统的 UITextField 在 iOS 10 下的 bug:https://github.com/QMUI/QMUI_iOS/issues/64 - if (IOS_VERSION < 10.0) { - return; - } - - UIScrollView *scrollView = self.subviews.firstObject; - if (![scrollView isKindOfClass:[UIScrollView class]]) { - return; - } - - // 默认 delegate 是为 nil 的,所以我们才利用 delegate 修复这 个 bug,如果哪一天 delegate 不为 nil,就先不处理了。 - if (scrollView.delegate) { - return; - } - - scrollView.delegate = self; -} - -#pragma mark - - -- (void)scrollViewDidScroll:(UIScrollView *)scrollView { - - // 以下代码修复系统的 UITextField 在 iOS 10 下的 bug:https://github.com/QMUI/QMUI_iOS/issues/64 - - if (scrollView != self.subviews.firstObject) { - return; - } - - CGFloat lineHeight = ((NSParagraphStyle *)self.defaultTextAttributes[NSParagraphStyleAttributeName]).minimumLineHeight; - lineHeight = lineHeight ?: ((UIFont *)self.defaultTextAttributes[NSFontAttributeName]).lineHeight; - if (scrollView.contentSize.height > ceil(lineHeight) && scrollView.contentOffset.y < 0) { - scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, 0); - } -} - -- (void)setText:(NSString *)text { - NSString *textBeforeChange = self.text; - [super setText:text]; - - if (self.shouldResponseToProgrammaticallyTextChanges && ![textBeforeChange isEqualToString:text]) { - [self fireTextDidChangeEventForTextField:self]; - } -} - -- (void)setAttributedText:(NSAttributedString *)attributedText { - NSAttributedString *textBeforeChange = self.attributedText; - [super setAttributedText:attributedText]; - if (self.shouldResponseToProgrammaticallyTextChanges && ![textBeforeChange isEqualToAttributedString:attributedText]) { - [self fireTextDidChangeEventForTextField:self]; - } -} - -- (void)fireTextDidChangeEventForTextField:(QMUITextField *)textField { - [textField sendActionsForControlEvents:UIControlEventEditingChanged]; - [[NSNotificationCenter defaultCenter] postNotificationName:UITextFieldTextDidChangeNotification object:textField]; -} - -- (NSUInteger)lengthWithString:(NSString *)string { - return self.shouldCountingNonASCIICharacterAsTwo ? string.qmui_lengthWhenCountingNonASCIICharacterAsTwo : string.length; -} - -#pragma mark - - -- (BOOL)textField:(QMUITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { - if (textField.maximumTextLength < NSUIntegerMax) { - - // 如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符),所以在 shouldChange 这里不会限制,而是放在 didChange 那里限制。 - BOOL isDeleting = range.length > 0 && string.length <= 0; - if (isDeleting || textField.markedTextRange) { - - if ([textField.originalDelegate respondsToSelector:_cmd]) { - return [textField.originalDelegate textField:textField shouldChangeCharactersInRange:range replacementString:string]; - } - - return YES; - } - - NSUInteger rangeLength = self.shouldCountingNonASCIICharacterAsTwo ? [textField.text substringWithRange:range].qmui_lengthWhenCountingNonASCIICharacterAsTwo : range.length; - if ([self lengthWithString:textField.text] - rangeLength + [self lengthWithString:string] > textField.maximumTextLength) { - // 将要插入的文字裁剪成这么长,就可以让它插入了 - NSInteger substringLength = textField.maximumTextLength - [self lengthWithString:textField.text] + rangeLength; - if (substringLength > 0 && [self lengthWithString:string] > substringLength) { - NSString *allowedText = [string qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, substringLength) lessValue:YES countingNonASCIICharacterAsTwo:self.shouldCountingNonASCIICharacterAsTwo]; - if ([self lengthWithString:allowedText] <= substringLength) { - textField.text = [textField.text stringByReplacingCharactersInRange:range withString:allowedText]; - - if (!textField.shouldResponseToProgrammaticallyTextChanges) { - [textField fireTextDidChangeEventForTextField:textField]; - } - } - } - - if ([self.originalDelegate respondsToSelector:@selector(textField:didPreventTextChangeInRange:replacementString:)]) { - [self.originalDelegate textField:textField didPreventTextChangeInRange:range replacementString:string]; - } - return NO; - } - } - - if ([textField.originalDelegate respondsToSelector:_cmd]) { - return [textField.originalDelegate textField:textField shouldChangeCharactersInRange:range replacementString:string]; - } - - return YES; -} - -#pragma mark - Delegate Proxy - -- (void)setDelegate:(id)delegate { - self.originalDelegate = delegate != self ? delegate : nil; - [super setDelegate:delegate ? self : nil]; -} - -- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { - NSMethodSignature *a = [super methodSignatureForSelector:aSelector]; - NSMethodSignature *b = [(id)self.originalDelegate methodSignatureForSelector:aSelector]; - NSMethodSignature *result = a ? a : b; - return result; -} - -- (void)forwardInvocation:(NSInvocation *)anInvocation { - if ([(id)self.originalDelegate respondsToSelector:anInvocation.selector]) { - [anInvocation invokeWithTarget:(id)self.originalDelegate]; - } -} - -- (BOOL)respondsToSelector:(SEL)aSelector { - // 修复 iOS 7 下将 UITextField.delegate 指向自身时会死循环的问题 - if (IOS_VERSION < 8.0 && [NSStringFromSelector(aSelector) hasPrefix:@"customOverlayC"]) { - return NO; - } - - BOOL a = [super respondsToSelector:aSelector]; - BOOL c = [self.originalDelegate respondsToSelector:aSelector]; - BOOL result = a || c; - return result; -} - -- (void)handleTextChangeEvent:(QMUITextField *)textField { - // 1、iOS 10 以下的版本,从中文输入法的候选词里选词输入,是不会走到 textField:shouldChangeCharactersInRange:replacementString: 的,所以要在这里截断文字 - // 2、如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符),所以在 shouldChange 那边不会限制,而是放在 didChange 这里限制。 - - if (!textField.markedTextRange) { - if ([self lengthWithString:textField.text] > textField.maximumTextLength) { - textField.text = [textField.text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, textField.maximumTextLength) lessValue:YES countingNonASCIICharacterAsTwo:self.shouldCountingNonASCIICharacterAsTwo]; - - if ([self.originalDelegate respondsToSelector:@selector(textField:didPreventTextChangeInRange:replacementString:)]) { - [self.originalDelegate textField:textField didPreventTextChangeInRange:textField.qmui_selectedRange replacementString:nil]; - } - } - } -} - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUITextView.h b/QMUI/QMUIKit/UIKitExtensions/QMUITextView.h deleted file mode 100644 index 6d5e05b1..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUITextView.h +++ /dev/null @@ -1,92 +0,0 @@ -// -// QMUITextView.h -// qmui -// -// Created by QQMail on 14-8-5. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import - -@class QMUITextView; - -@protocol QMUITextViewDelegate - -@optional -/** - * 输入框高度发生变化时的回调,仅当 `autoResizable` 属性为 YES 时才有效。 - * @note 只有当内容高度与当前输入框的高度不一致时才会调用到这里,所以无需在内部做高度是否变化的判断。 - */ -- (void)textView:(QMUITextView *)textView newHeightAfterTextChanged:(CGFloat)height; - -/** - * 用户点击键盘的 return 按钮时的回调(return 按钮本质上是输入换行符“\n”) - * @return 返回 YES 表示程序认为当前的点击是为了进行类似“发送”之类的操作,所以最终“\n”并不会被输入到文本框里。返回 NO 表示程序认为当前的点击只是普通的输入,所以会继续询问 textView:shouldChangeTextInRange:replacementText: 方法,根据该方法的返回结果来决定是否要输入这个“\n”。 - * @see maximumTextLength - */ -- (BOOL)textViewShouldReturn:(QMUITextView *)textView; - -/** - * 配合 `maximumTextLength` 属性使用,在输入文字超过限制时被调用。例如如果你的输入框在按下键盘“Done”按键时做一些发送操作,就可以在这个方法里判断 [replacementText isEqualToString:@"\n"]。 - * @warning 在 textViewDidChange: 里也会触发文字长度拦截,由于此时 textView 的文字已经改变完,所以无法得知发生改变的文本位置及改变的文本内容,所以此时 range 和 replacementText 这两个参数的值也会比较特殊,具体请看参数讲解。 - * - * @param textView 触发的 textView - * @param range 要变化的文字的位置,如果在 textViewDidChange: 里,这里的 range 也即文字变化后的 range,所以可能比最大长度要大。 - * @param replacementText 要变化的文字,如果在 textViewDidChange: 里,这里永远传入 nil。 - */ -- (void)textView:(QMUITextView *)textView didPreventTextChangeInRange:(NSRange)range replacementText:(NSString *)replacementText; - -@end - -/** - * 自定义 UITextView,提供的特性如下: - * - * 1. 支持 placeholder 并支持更改 placeholderColor;若使用了富文本文字,则 placeholder 的样式也会跟随文字的样式(除了 placeholder 颜色) - * 2. 支持在文字发生变化时计算内容高度并通知 delegate (需打开 autoResizable 属性)。 - * 3. 支持限制输入的文本的最大长度,默认不限制。 - * 4. 修正系统 UITextView 在输入时自然换行的时候,contentOffset 的滚动位置没有考虑 textContainerInset.bottom - */ -@interface QMUITextView : UITextView - -@property(nonatomic, weak) id delegate; - -/** - * 当通过 `setText:`、`setAttributedText:`等方式修改文字时,是否应该自动触发 `UITextViewDelegate` 里的 `textView:shouldChangeTextInRange:replacementText:`、 `textViewDidChange:` 方法 - * - * 默认为YES(注意系统的 UITextView 对这种行为默认是 NO) - */ -@property(nonatomic, assign) IBInspectable BOOL shouldResponseToProgrammaticallyTextChanges; - -/** - * 显示允许输入的最大文字长度,默认为 NSUIntegerMax,也即不限制长度。 - */ -@property(nonatomic, assign) IBInspectable NSUInteger maximumTextLength; - -/** - * 在使用 maximumTextLength 功能的时候,是否应该把文字长度按照 [NSString (QMUI) qmui_lengthWhenCountingNonASCIICharacterAsTwo] 的方法来计算。 - * 默认为 NO。 - */ -@property(nonatomic, assign) IBInspectable BOOL shouldCountingNonASCIICharacterAsTwo; - -/** - * placeholder 的文字 - */ -@property(nonatomic, copy) IBInspectable NSString *placeholder; - -/** - * placeholder 文字的颜色 - */ -@property(nonatomic, strong) IBInspectable UIColor *placeholderColor; - -/** - * placeholder 在默认位置上的偏移(默认位置会自动根据 textContainerInset、contentInset 来调整) - */ -@property(nonatomic, assign) UIEdgeInsets placeholderMargins; - -/** - * 是否支持自动拓展高度,默认为NO - * @see textView:newHeightAfterTextChanged: - */ -@property(nonatomic, assign) BOOL autoResizable; - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/QMUITextView.m b/QMUI/QMUIKit/UIKitExtensions/QMUITextView.m deleted file mode 100644 index 8344dbd3..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/QMUITextView.m +++ /dev/null @@ -1,430 +0,0 @@ -// -// QMUITextView.m -// qmui -// -// Created by QQMail on 14-8-5. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// -#import "QMUITextView.h" -#import "QMUICore.h" -#import "QMUILabel.h" -#import "NSObject+QMUI.h" -#import "NSString+QMUI.h" -#import "UITextView+QMUI.h" - -/// 系统 textView 默认的字号大小,用于 placeholder 默认的文字大小。实测得到,请勿修改。 -const CGFloat kSystemTextViewDefaultFontPointSize = 12.0f; - -/// 当系统的 textView.textContainerInset 为 UIEdgeInsetsZero 时,文字与 textView 边缘的间距。实测得到,请勿修改(在输入框font大于13时准确,小于等于12时,y有-1px的偏差)。 -const UIEdgeInsets kSystemTextViewFixTextInsets = {0, 5, 0, 5}; - -@interface QMUITextView () - -@property(nonatomic, assign) BOOL debug; -@property(nonatomic, assign) BOOL shouldRejectSystemScroll;// 如果在 handleTextChanged: 里主动调整 contentOffset,则为了避免被系统的自动调整覆盖,会利用这个标记去屏蔽系统对 setContentOffset: 的调用 - -@property(nonatomic, strong) UILabel *placeholderLabel; - -@property(nonatomic, weak) id originalDelegate; - -@end - -@implementation QMUITextView - -@dynamic delegate; - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - [self didInitialized]; - self.tintColor = TextFieldTintColor; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - self.debug = NO; - self.delegate = self; - self.scrollsToTop = NO; - self.placeholderColor = UIColorPlaceholder; - self.placeholderMargins = UIEdgeInsetsZero; - self.autoResizable = NO; - self.maximumTextLength = NSUIntegerMax; - self.shouldResponseToProgrammaticallyTextChanges = YES; - - self.placeholderLabel = [[UILabel alloc] init]; - self.placeholderLabel.font = UIFontMake(kSystemTextViewDefaultFontPointSize); - self.placeholderLabel.textColor = self.placeholderColor; - self.placeholderLabel.numberOfLines = 0; - self.placeholderLabel.alpha = 0; - [self addSubview:self.placeholderLabel]; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTextChanged:) name:UITextViewTextDidChangeNotification object:nil]; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - self.delegate = nil; - self.originalDelegate = nil; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"%@; text.length: %@ | %@; markedTextRange: %@", [super description], @(self.text.length), @([self lengthWithString:self.text]), self.markedTextRange]; -} - -- (BOOL)isCurrentTextDifferentOfText:(NSString *)text { - NSString *textBeforeChange = self.text;// UITextView 如果文字为空,self.text 永远返回 @"" 而不是 nil(即便你设置为 nil 后立即 get 出来也是) - if ([textBeforeChange isEqualToString:text] || (textBeforeChange.length == 0 && !text)) { - return NO; - } - 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)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; - } - - // 应该改变文字,则调用 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]; - } -} - -- (void)setTypingAttributes:(NSDictionary *)typingAttributes { - [super setTypingAttributes:typingAttributes]; - [self updatePlaceholderStyle]; -} - -- (void)setFont:(UIFont *)font { - [super setFont:font]; - [self updatePlaceholderStyle]; -} - -- (void)setTextColor:(UIColor *)textColor { - [super setTextColor:textColor]; - [self updatePlaceholderStyle]; -} - -- (void)setTextAlignment:(NSTextAlignment)textAlignment { - [super setTextAlignment:textAlignment]; - [self updatePlaceholderStyle]; -} - -- (void)setPlaceholder:(NSString *)placeholder { - _placeholder = placeholder; - self.placeholderLabel.attributedText = [[NSAttributedString alloc] initWithString:_placeholder attributes:self.typingAttributes]; - if (self.placeholderColor) { - self.placeholderLabel.textColor = self.placeholderColor; - } - [self sendSubviewToBack:self.placeholderLabel]; -} - -- (void)setPlaceholderColor:(UIColor *)placeholderColor { - _placeholderColor = placeholderColor; - self.placeholderLabel.textColor = _placeholderColor; -} - -- (void)updatePlaceholderStyle { - self.placeholder = self.placeholder;// 触发文字样式的更新 -} - -- (void)handleTextChanged:(id)sender { - // 输入字符的时候,placeholder隐藏 - if(self.placeholder.length > 0) { - [self updatePlaceholderLabelHidden]; - } - - QMUITextView *textView = nil; - - if ([sender isKindOfClass:[NSNotification class]]) { - id object = ((NSNotification *)sender).object; - if (object == self) { - textView = (QMUITextView *)object; - } - } else if ([sender isKindOfClass:[QMUITextView class]]) { - textView = (QMUITextView *)sender; - } - - if (textView) { - - // 计算高度 - if (self.autoResizable) { - - CGFloat resultHeight = [textView sizeThatFits:CGSizeMake(CGRectGetWidth(self.bounds), CGFLOAT_MAX)].height; - - if (self.debug) NSLog(@"handleTextDidChange, text = %@, resultHeight = %f", textView.text, resultHeight); - - - // 通知delegate去更新textView的高度 - if ([textView.originalDelegate respondsToSelector:@selector(textView:newHeightAfterTextChanged:)] && resultHeight != CGRectGetHeight(self.bounds)) { - [textView.originalDelegate textView:self newHeightAfterTextChanged:resultHeight]; - } - } - - // textView 尚未被展示到界面上时,此时过早进行光标调整会计算错误 - if (!textView.window) { - return; - } - - self.shouldRejectSystemScroll = YES; - // 用 dispatch 延迟一下,因为在文字发生换行时,系统自己会做一些滚动,我们要延迟一点才能避免被系统的滚动覆盖 - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - self.shouldRejectSystemScroll = NO; - [self qmui_scrollCaretVisibleAnimated:NO]; - }); - } -} - -- (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 = fminf(limitHeight, labelSize.height); - self.placeholderLabel.frame = CGRectFlatMake(labelMargins.left, labelMargins.top, limitWidth, labelSize.height); - } -} - -- (void)drawRect:(CGRect)rect { - [super drawRect: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; -} - -#pragma mark - - -- (BOOL)textView:(QMUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { - if (self.debug) NSLog(@"textView.text(%@ | %@) = %@\nmarkedTextRange = %@\nrange = %@\ntext = %@", @(textView.text.length), @(textView.text.qmui_lengthWhenCountingNonASCIICharacterAsTwo), textView.text, textView.markedTextRange, NSStringFromRange(range), text); - - if ([text isEqualToString:@"\n"]) { - if ([self.delegate respondsToSelector:@selector(textViewShouldReturn:)]) { - BOOL shouldReturn = [self.delegate textViewShouldReturn:self]; - if (shouldReturn) { - return NO; - } - } - } - - if (textView.maximumTextLength < NSUIntegerMax) { - - // 如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符),所以在 shouldChange 这里不会限制,而是放在 didChange 那里限制。 - BOOL isDeleting = range.length > 0 && text.length <= 0; - if (isDeleting || textView.markedTextRange) { - - if ([textView.originalDelegate respondsToSelector:_cmd]) { - return [textView.originalDelegate textView:textView shouldChangeTextInRange:range replacementText:text]; - } - - return YES; - } - - NSUInteger rangeLength = self.shouldCountingNonASCIICharacterAsTwo ? [textView.text substringWithRange:range].qmui_lengthWhenCountingNonASCIICharacterAsTwo : range.length; - BOOL textWillOutofMaximumTextLength = [self lengthWithString:textView.text] - rangeLength + [self lengthWithString:text] > textView.maximumTextLength; - if (textWillOutofMaximumTextLength) { - // 当输入的文本达到最大长度限制后,此时继续点击 return 按钮(相当于尝试插入“\n”),就会认为总文字长度已经超过最大长度限制,所以此次 return 按钮的点击被拦截,外界无法感知到有这个 return 事件发生,所以这里为这种情况做了特殊保护 - if ([self lengthWithString:textView.text] - rangeLength == textView.maximumTextLength && [text isEqualToString:@"\n"]) { - if ([textView.originalDelegate respondsToSelector:_cmd]) { - // 不管外面 return YES 或 NO,都不允许输入了,否则会超出 maximumTextLength。 - [textView.originalDelegate textView:textView shouldChangeTextInRange:range replacementText:text]; - return NO; - } - } - // 将要插入的文字裁剪成多长,就可以让它插入了 - NSInteger substringLength = textView.maximumTextLength - [self lengthWithString:textView.text] + rangeLength; - - if (substringLength > 0 && [self lengthWithString:text] > substringLength) { - NSString *allowedText = [text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, substringLength) lessValue:YES countingNonASCIICharacterAsTwo:self.shouldCountingNonASCIICharacterAsTwo]; - if ([self lengthWithString:allowedText] <= substringLength) { - textView.text = [textView.text stringByReplacingCharactersInRange:range withString:allowedText]; - textView.selectedRange = NSMakeRange(range.location + substringLength, 0); - - if (!textView.shouldResponseToProgrammaticallyTextChanges) { - [textView.originalDelegate textViewDidChange:textView]; - } - } - } - - if ([self.originalDelegate respondsToSelector:@selector(textView:didPreventTextChangeInRange:replacementText:)]) { - [self.originalDelegate textView:textView didPreventTextChangeInRange:range replacementText:text]; - } - return NO; - } - } - - if ([textView.originalDelegate respondsToSelector:_cmd]) { - return [textView.originalDelegate textView:textView shouldChangeTextInRange:range replacementText:text]; - } - - return YES; -} - -- (void)textViewDidChange:(QMUITextView *)textView { - // 1、iOS 10 以下的版本,从中文输入法的候选词里选词输入,是不会走到 textView:shouldChangeTextInRange:replacementText: 的,所以要在这里截断文字 - // 2、如果是中文输入法正在输入拼音的过程中(markedTextRange 不为 nil),是不应该限制字数的(例如输入“huang”这5个字符,其实只是为了输入“黄”这一个字符),所以在 shouldChange 那边不会限制,而是放在 didChange 这里限制。 - if (!textView.markedTextRange) { - if ([self lengthWithString:textView.text] > textView.maximumTextLength) { - - textView.text = [textView.text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, textView.maximumTextLength) lessValue:YES countingNonASCIICharacterAsTwo:self.shouldCountingNonASCIICharacterAsTwo]; - - if ([self.originalDelegate respondsToSelector:@selector(textView:didPreventTextChangeInRange:replacementText:)]) { - // 如果是在这里被截断,是无法得知截断前光标所处的位置及要输入的文本的,所以只能将当前的 selectedRange 传过去,而 replacementText 为 nil - [self.originalDelegate textView:textView didPreventTextChangeInRange:textView.selectedRange replacementText:nil]; - } - - if (textView.shouldResponseToProgrammaticallyTextChanges) { - return; - } - } - } - if ([textView.originalDelegate respondsToSelector:_cmd]) { - [textView.originalDelegate textViewDidChange:textView]; - } -} - -#pragma mark - Delegate Proxy - -- (void)setDelegate:(id)delegate { - self.originalDelegate = delegate != self ? delegate : nil; - [super setDelegate:delegate ? self : nil]; -} - -- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { - NSMethodSignature *a = [super methodSignatureForSelector:aSelector]; - NSMethodSignature *b = [(id)self.originalDelegate methodSignatureForSelector:aSelector]; - NSMethodSignature *result = a ? a : b; - return result; -} - -- (void)forwardInvocation:(NSInvocation *)anInvocation { - if ([(id)self.originalDelegate respondsToSelector:anInvocation.selector]) { - [anInvocation invokeWithTarget:(id)self.originalDelegate]; - } -} - -- (BOOL)respondsToSelector:(SEL)aSelector { - BOOL a = [super respondsToSelector:aSelector]; - BOOL c = [self.originalDelegate respondsToSelector:aSelector]; - BOOL result = a || c; - return result; -} - -// 下面这两个方法比较特殊,无法通过 forwardInvocation: 的方式把消息发送给 self.originalDelegate,只会直接被调用,所以只能在 QMUITextView 内部实现这连个方法然后调用 originalDelegate 的对应方法 -// 注意,测过 UITextView 默认没有实现任何 UIScrollViewDelegate 方法 from 2016-11-01 in iOS 10.1 by molice - -- (void)scrollViewDidScroll:(UIScrollView *)scrollView { - if ([self.originalDelegate respondsToSelector:_cmd]) { - [self.originalDelegate scrollViewDidScroll:scrollView]; - } -} - -- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated { - if (!self.shouldRejectSystemScroll) { - [super setContentOffset:contentOffset animated:animated]; - if (self.debug) NSLog(@"%@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y); - } else { - if (self.debug) NSLog(@"被屏蔽的 %@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y); - } -} - -- (void)setContentOffset:(CGPoint)contentOffset { - if (!self.shouldRejectSystemScroll) { - [super setContentOffset:contentOffset]; - if (self.debug) NSLog(@"%@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y); - } else { - if (self.debug) NSLog(@"被屏蔽的 %@, contentOffset.y = %.2f", NSStringFromSelector(_cmd), contentOffset.y); - } -} - -- (void)scrollViewDidZoom:(UIScrollView *)scrollView { - if ([self.originalDelegate respondsToSelector:_cmd]) { - [self.originalDelegate scrollViewDidZoom:scrollView]; - } -} - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h index 7576ce05..9735c50d 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h @@ -1,25 +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. + */ + // // UIActivityIndicatorView+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #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 2a423e9c..128cdce1 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m @@ -1,22 +1,87 @@ +/** + * 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. + */ + // // UIActivityIndicatorView+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #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 new file mode 100644 index 00000000..adeb5efe --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.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. + */ + +// +// UIBarItem+QMUI.h +// QMUIKit +// +// Created by QMUI Team on 2018/4/5. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIBarItem (QMUI) + +/** + 获取 UIBarItem(UIBarButtonItem、UITabBarItem) 内部的 view,通常对于 navigationItem 而言,需要在设置了 navigationItem 后并且在 navigationBar 可见时(例如 viewDidAppear: 及之后)获取 UIBarButtonItem.qmui_view 才有值。 + + @return 当 UIBarButtonItem 作为 navigationItem 使用时,iOS 10 及以前返回 UINavigationButton,iOS 11 及以后返回 _UIButtonBarButton;当作为 toolbarItem 使用时,iOS 10 及以前返回 UIToolbarButton,iOS 11 及以后返回 _UIButtonBarButton。对于 UITabBarItem,不管任何 iOS 版本均返回 UITabBarButton。 + + @note 可以通过 qmui_viewDidSetBlock 监听 qmui_view 值的变化,从而无需等待 viewDidAppear: 之类的时机。 + + @warning 仅对 UIBarButtonItem、UITabBarItem 有效 + */ +@property(nullable, nonatomic, weak, readonly) UIView *qmui_view; + +/** + 当 item 内的 view 生成后就会调用这个 block。 + + @note 该方法的本质是系统的 setView:/setCustomView: 被调用时就会调用,但系统在横竖屏旋转时也会再次走到 setView:(即便此时 view 的实例并没有发生变化),所以 QMUI 对这种情况做了屏蔽,以保证这个 block 对于同一个 view 实例只会被调用一次。 + + @warning 仅对 UIBarButtonItem、UITabBarItem 有效 + */ +@property(nullable, nonatomic, copy) void (^qmui_viewDidSetBlock)(__kindof UIBarItem *item, UIView * _Nullable view); + +/** + 当 item 内的 view 的 layoutSubviews 被调用后就会调用这个 block,如果某些需求需要依赖于 subviews 的位置,则使用这个 block。如果只是依赖于 item 的 view 的 frame 变化,则可以使用 qmui_viewLayoutDidChangeBlock。 + + @warning 仅对 UIBarButtonItem、UITabBarItem 有效 + */ +@property(nullable, nonatomic, copy) void (^qmui_viewDidLayoutSubviewsBlock)(__kindof UIBarItem *item, UIView * _Nullable view); + +/** + 当 item 内的 view 的 frame 发生变化时就会调用这个 block。 + + @warning 仅对 UIBarButtonItem、UITabBarItem 有效 + */ +@property(nullable, nonatomic, copy) void (^qmui_viewLayoutDidChangeBlock)(__kindof UIBarItem *item, UIView * _Nullable view); + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m new file mode 100644 index 00000000..4812e800 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m @@ -0,0 +1,122 @@ +/** + * 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. + */ + +// +// UIBarItem+QMUI.m +// QMUIKit +// +// Created by QMUI Team on 2018/4/5. +// + +#import "UIBarItem+QMUI.h" +#import "QMUICore.h" +#import "UIView+QMUI.h" + +@interface UIBarItem () + +@property(nonatomic, copy) NSString *qmuibaritem_viewDidSetBlockIdentifier; +@end + +@implementation UIBarItem (QMUI) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // -[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]; + }); + + // -[UITabBarItem setView:] + ExtendImplementationOfVoidMethodWithSingleArgument([UITabBarItem class], @selector(setView:), UIView *, ^(UITabBarItem *selfObject, UIView *firstArgv) { + [UIBarItem setView:firstArgv inBarItem:selfObject]; + }); + }); +} + +- (UIView *)qmui_view { + // UIBarItem 本身没有 view 属性,只有子类 UIBarButtonItem 和 UITabBarItem 才有 + if ([self respondsToSelector:@selector(view)]) { + return [self qmui_valueForKey:@"view"]; + } + return nil; +} + +QMUISynthesizeIdCopyProperty(qmuibaritem_viewDidSetBlockIdentifier, setQmuibaritem_viewDidSetBlockIdentifier) +QMUISynthesizeIdCopyProperty(qmui_viewDidSetBlock, setQmui_viewDidSetBlock) + +static char kAssociatedObjectKey_viewDidLayoutSubviewsBlock; +- (void)setQmui_viewDidLayoutSubviewsBlock:(void (^)(__kindof UIBarItem * _Nonnull, UIView * _Nullable))qmui_viewDidLayoutSubviewsBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_viewDidLayoutSubviewsBlock, qmui_viewDidLayoutSubviewsBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (self.qmui_view) { + __weak __typeof(self)weakSelf = self; + self.qmui_view.qmui_layoutSubviewsBlock = ^(__kindof UIView * _Nonnull view) { + if (weakSelf.qmui_viewDidLayoutSubviewsBlock) { + weakSelf.qmui_viewDidLayoutSubviewsBlock(weakSelf, view); + } + }; + } +} + +- (void (^)(__kindof UIBarItem * _Nonnull, UIView * _Nullable))qmui_viewDidLayoutSubviewsBlock { + return (void (^)(__kindof UIBarItem * _Nonnull, UIView * _Nullable))objc_getAssociatedObject(self, &kAssociatedObjectKey_viewDidLayoutSubviewsBlock); +} + +static char kAssociatedObjectKey_viewLayoutDidChangeBlock; +- (void)setQmui_viewLayoutDidChangeBlock:(void (^)(__kindof UIBarItem * _Nonnull, UIView * _Nullable))qmui_viewLayoutDidChangeBlock { + 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 ([view.superview isKindOfClass:[UIStackView class]]) { + view = self.qmui_view.superview; + } + if (view) { + __weak __typeof(self)weakSelf = self; + view.qmui_frameDidChangeBlock = ^(__kindof UIView * _Nonnull view, CGRect precedingFrame) { + if (weakSelf.qmui_viewLayoutDidChangeBlock){ + weakSelf.qmui_viewLayoutDidChangeBlock(weakSelf, weakSelf.qmui_view); + } + }; + } +} + +- (void (^)(__kindof UIBarItem * _Nonnull, UIView * _Nullable))qmui_viewLayoutDidChangeBlock { + return (void (^)(__kindof UIBarItem * _Nonnull, UIView * _Nullable))objc_getAssociatedObject(self, &kAssociatedObjectKey_viewLayoutDidChangeBlock); +} + +#pragma mark - Tools + ++ (NSString *)identifierWithView:(UIView *)view block:(id)block { + return [NSString stringWithFormat:@"%p, %p", view, block]; +} + ++ (void)setView:(UIView *)view inBarItem:(__kindof UIBarItem *)item { + if (item.qmui_viewDidSetBlock) { + item.qmui_viewDidSetBlock(item, view); + } + + if (item.qmui_viewDidLayoutSubviewsBlock) { + item.qmui_viewDidLayoutSubviewsBlock = item.qmui_viewDidLayoutSubviewsBlock;// to call setter + } + + if (item.qmui_viewLayoutDidChangeBlock) { + item.qmui_viewLayoutDidChangeBlock = item.qmui_viewLayoutDidChangeBlock;// to call setter + } +} + ++ (void)setView:(UIView *)view inBarButtonItem:(UIBarButtonItem *)item { + if (![[UIBarItem identifierWithView:view block:item.qmui_viewDidSetBlock] isEqualToString:item.qmuibaritem_viewDidSetBlockIdentifier]) { + item.qmuibaritem_viewDidSetBlockIdentifier = [UIBarItem identifierWithView:view block:item.qmui_viewDidSetBlock]; + + [self setView:view inBarItem:item]; + } +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.h index 44d876e9..deae2f70 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.h @@ -1,14 +1,23 @@ +/** + * 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. + */ + // // UIBezierPath+QMUI.h // qmui // -// Created by MoLice on 16/8/9. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/8/9. // #import #import +NS_ASSUME_NONNULL_BEGIN + @interface UIBezierPath (QMUI) /** @@ -17,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 30c3f419..afff2f02 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIBezierPath+QMUI.m @@ -1,23 +1,31 @@ +/** + * 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. + */ + // // UIBezierPath+QMUI.m // qmui // -// Created by MoLice on 16/8/9. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/8/9. // #import "UIBezierPath+QMUI.h" @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)]; @@ -31,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 994457bc..8ac38163 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.h @@ -1,16 +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. + */ + // // UIButton+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import +NS_ASSUME_NONNULL_BEGIN + @interface UIButton (QMUI) -- (instancetype)initWithImage:(UIImage *)image title:(NSString *)title; +- (instancetype)qmui_initWithImage:(nullable UIImage *)image title:(nullable NSString *)title; /** * 在UIButton的样式(如字体)设置完后,将button的text设置为一个测试字符,再调用sizeToFit,从而令button的高度适应字体 @@ -24,6 +33,16 @@ * @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 88adad24..0f81e8fe 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIButton+QMUI.m @@ -1,29 +1,90 @@ +/** + * 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. + */ + // // UIButton+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import "UIButton+QMUI.h" #import "QMUICore.h" +#import "UIImage+QMUI.h" + +@interface UIButton () + +@property(nonatomic, strong) NSMutableDictionary *> *qbt_titleAttributes; +@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_statesWithTitle, setQbt_statesWithTitle) +QMUISynthesizeIdStrongProperty(qbt_imageTintColors, setQbt_imageTintColors) +QMUISynthesizeIdStrongProperty(qbt_statesWithImageTintColor, setQbt_statesWithImageTintColor) + + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ReplaceMethod([self class], @selector(setTitle:forState:), @selector(qmui_setTitle:forState:)); - ReplaceMethod([self class], @selector(setTitleColor:forState:), @selector(qmui_setTitleColor:forState:)); + + 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]; + }; + }); + + 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]; + } + }); }); } -- (instancetype)initWithImage:(UIImage *)image title:(NSString *)title { - if (self = [super init]) { - [self setImage:image forState:UIControlStateNormal]; - [self setTitle:title forState:UIControlStateNormal]; - } +- (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; } @@ -33,89 +94,142 @@ - (void)qmui_calculateHeightAfterSetAppearance { [self setTitle:nil forState:UIControlStateNormal]; } -#pragma mark - Title Attributes +#pragma mark - TitleAttributes -- (void)qmui_setTitleAttributes:(NSDictionary *)attributes forState:(UIControlState)state { - if (!attributes) { - [self.qmui_titleAttributes removeObjectForKey:@(state)]; - [self setAttributedTitle:nil forState:state]; +- (void)qmui_setTitleAttributes:(NSDictionary *)attributes forState:(UIControlState)state { + if (!attributes && self.qbt_titleAttributes) { + [self.qbt_titleAttributes removeObjectForKey:@(state)]; return; } - if (!self.qmui_titleAttributes) { - self.qmui_titleAttributes = [NSMutableDictionary dictionary]; + [UIButton qbt_swizzleForTitleAttributesIfNeeded]; + + if (!self.qbt_titleAttributes) { + 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.qmui_titleAttributes[@(state)] = attributes; + + 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 通过设置 qmui_titleAttributes 属性而导致使用了 attributedString,则 normal 也必须使用 attributedString - if (self.qmui_titleAttributes.count && !self.qmui_titleAttributes[@(UIControlStateNormal)]) { + // 为了解决这个问题,我们要确保一旦有 normal 之外的 state 通过设置 qbt_titleAttributes 属性而导致使用了 attributedString,则 normal 也必须使用 attributedString + if (self.qbt_titleAttributes.count && !self.qbt_titleAttributes[@(UIControlStateNormal)]) { [self qmui_setTitleAttributes:@{} forState:UIControlStateNormal]; } } -- (void)qmui_setTitle:(NSString *)title forState:(UIControlState)state { - [self qmui_setTitle:title forState:state]; - if (!title || !self.qmui_titleAttributes.count) { - return; - } - - if (state == UIControlStateNormal) { - [self.qmui_titleAttributes enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) { - UIControlState state = [key unsignedIntegerValue]; - NSString *titleForState = [self titleForState:state]; - NSAttributedString *string = [[NSAttributedString alloc] initWithString:titleForState attributes:obj]; - [self setAttributedTitle:[self attributedStringWithEndKernRemoved:string] forState:state]; - }]; - return; - } - - if ([self.qmui_titleAttributes objectForKey:@(state)]) { - NSAttributedString *string = [[NSAttributedString alloc] initWithString:title attributes:self.qmui_titleAttributes[@(state)]]; - [self setAttributedTitle:[self attributedStringWithEndKernRemoved:string] forState:state]; - return; +// 如果 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]; } } -// 如果之前已经设置了此 state 下的文字颜色,则覆盖掉之前的颜色 -- (void)qmui_setTitleColor:(UIColor *)color forState:(UIControlState)state { - [self qmui_setTitleColor:color forState:state]; - NSDictionary *attributes = self.qmui_titleAttributes[@(state)]; - if (attributes) { - NSMutableDictionary *newAttributes = [NSMutableDictionary dictionaryWithDictionary:attributes]; - newAttributes[NSForegroundColorAttributeName] = color; - [self qmui_setTitleAttributes:[NSDictionary dictionaryWithDictionary:newAttributes] forState: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"]; } -// 去除最后一个字的 kern 效果 -- (NSAttributedString *)attributedStringWithEndKernRemoved:(NSAttributedString *)string { - if (!string || !string.length) { +// 去除最后一个字的 kern 效果,避免字符串尾部出现多余的空白 ++ (NSAttributedString *)qbt_attributedStringByRemovingLastKern:(NSAttributedString *)string { + if (!string.length) { return string; } - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + NSMutableAttributedString *attributedString = string.mutableCopy; [attributedString removeAttribute:NSKernAttributeName range:NSMakeRange(string.length - 1, 1)]; - return [[NSAttributedString alloc] initWithAttributedString:attributedString]; + return attributedString.copy; } -static char kAssociatedObjectKey_titleAttributes; -- (void)setQmui_titleAttributes:(NSMutableDictionary *)qmui_titleAttributes { - objc_setAssociatedObject(self, &kAssociatedObjectKey_titleAttributes, qmui_titleAttributes, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +#pragma mark - ImageTintColor + +- (void)qmui_setImageTintColor:(UIColor *)color forState:(UIControlState)state { + if (!color && self.qbt_imageTintColors) { + [self.qbt_imageTintColors removeObjectForKey:@(state)]; + return; + } + + [UIButton qbt_swizzleForImageTintColorIfNeeded]; + + if (!self.qbt_imageTintColors) { + self.qbt_imageTintColors = [[NSMutableDictionary alloc] init]; + } + self.qbt_imageTintColors[@(state)] = color; + + UIImage *stateImage = [self imageForState:state]; + if (!stateImage) return; + stateImage = [[stateImage qmui_imageWithTintColor:color] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; + [self setImage:stateImage forState:state]; } -- (NSMutableDictionary *)qmui_titleAttributes { - return (NSMutableDictionary *)objc_getAssociatedObject(self, &kAssociatedObjectKey_titleAttributes); ++ (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 28831641..1a8a8528 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.h @@ -1,14 +1,20 @@ +/** + * 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. + */ + // // UICollectionView+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import #import -#import "QMUICellHeightCache.h" @interface UICollectionView (QMUI) @@ -36,6 +42,11 @@ */ - (BOOL)qmui_itemVisibleAtIndexPath:(NSIndexPath *)indexPath; +/** + * 对系统的 indexPathsForVisibleItems 进行了排序后的结果 + */ +- (NSArray *)qmui_indexPathsForVisibleItems; + /** * 获取可视区域内第一个cell的indexPath。 * @@ -46,44 +57,3 @@ - (NSIndexPath *)qmui_indexPathForFirstVisibleCell; @end - -/// ====================== 计算动态cell高度相关 ======================= - -/** - * UICollectionView 定义了一套动态计算 cell 高度的方式。 - * 原理类似UITableView,具体请参考UITableView+QMUI。 - */ - -@interface UICollectionView (QMUIKeyedHeightCache) - -@property (nonatomic, strong, readonly) QMUICellHeightKeyCache *qmui_keyedHeightCache; - -@end - -@interface UICollectionView (QMUICellHeightIndexPathCache) - -@property (nonatomic, strong, readonly) QMUICellHeightIndexPathCache *qmui_indexPathHeightCache; - -@end - -@interface UICollectionView (QMUIIndexPathHeightCacheInvalidation) - -/// 当需要reloadData的时候,又不想使布局失效,可以调用下面这个方法。例如,在底部加载更多。 -- (void)qmui_reloadDataWithoutInvalidateIndexPathHeightCache; - -@end - -/// 以下接口可在“sizeForItemAtIndexPath”里面调用来计算高度 -/// 通过构建一个cell模拟真正显示的cell,给cell设置真实的数据,然后再调用cell的sizeThatFits:来计算高度 -/// 也就是说我们自定义的cell里面需要重写sizeThatFits:并返回正确的值 -@interface UICollectionView (QMUILayoutCell) - -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth configuration:(void (^)(id cell))configuration; - -// 通过indexPath缓存高度 -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration; - -// 通过key缓存高度 -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByKey:(id)key configuration:(void (^)(id cell))configuration; - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.m index a92f26b7..2eb00f26 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UICollectionView+QMUI.m @@ -1,16 +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. + */ + // // UICollectionView+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import "UICollectionView+QMUI.h" -#import +#import "QMUICore.h" +#import "QMUILog.h" @implementation UICollectionView (QMUI) ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // 防止 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) { + // 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) { + isIndexPathLegal = NO; + } else { + NSInteger items = [selfObject numberOfItemsInSection:indexPath.section]; + if (indexPath.item >= items) { + isIndexPathLegal = NO; + } + } + if (!isIndexPathLegal) { + QMUIAssert(NO, @"UICollectionView (QMUI)", @"%@ - target indexPath : %@ ,不合法的indexPath。\n%@", selfObject, indexPath, [NSThread callStackSymbols]); + return; + } + } + + // call super + void (*originSelectorIMP)(id, SEL, NSIndexPath *, UICollectionViewScrollPosition, BOOL); + originSelectorIMP = (void (*)(id, SEL, NSIndexPath *, UICollectionViewScrollPosition, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, indexPath, scrollPosition, animated); + }; + }); + }); +} + - (void)qmui_clearsSelection { NSArray *selectedItemIndexPaths = [self indexPathsForSelectedItems]; for (NSIndexPath *indexPath in selectedItemIndexPaths) { @@ -61,279 +104,21 @@ - (BOOL)qmui_itemVisibleAtIndexPath:(NSIndexPath *)indexPath { return NO; } +- (NSArray *)qmui_indexPathsForVisibleItems { + NSArray *visibleItems = [self indexPathsForVisibleItems]; + NSSortDescriptor *sectionSorter = [[NSSortDescriptor alloc] initWithKey:@"section" ascending:YES]; + NSSortDescriptor *rowSorter = [[NSSortDescriptor alloc] initWithKey:@"item" ascending:YES]; + visibleItems = [visibleItems sortedArrayUsingDescriptors:[NSArray arrayWithObjects:sectionSorter, rowSorter, nil]]; + return visibleItems; +} + - (NSIndexPath *)qmui_indexPathForFirstVisibleCell { - NSArray *visibleIndexPaths = [self indexPathsForVisibleItems]; + NSArray *visibleIndexPaths = [self qmui_indexPathsForVisibleItems]; if (!visibleIndexPaths || visibleIndexPaths.count <= 0) { return nil; } - NSIndexPath *minimumIndexPath = nil; - for (NSIndexPath *indexPath in visibleIndexPaths) { - if (!minimumIndexPath) { - minimumIndexPath = indexPath; - continue; - } - - if (indexPath.section < minimumIndexPath.section) { - minimumIndexPath = indexPath; - continue; - } - - if (indexPath.item < minimumIndexPath.item) { - minimumIndexPath = indexPath; - continue; - } - } - return minimumIndexPath; -} - -@end - -/// ====================== 计算动态cell高度相关 ======================= - -@implementation UICollectionView (QMUIKeyedHeightCache) - -- (QMUICellHeightKeyCache *)qmui_keyedHeightCache { - QMUICellHeightKeyCache *cache = objc_getAssociatedObject(self, _cmd); - if (!cache) { - cache = [[QMUICellHeightKeyCache alloc] init]; - objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - return cache; -} - -@end - -@implementation UICollectionView (QMUICellHeightIndexPathCache) - -- (QMUICellHeightIndexPathCache *)qmui_indexPathHeightCache { - QMUICellHeightIndexPathCache *cache = objc_getAssociatedObject(self, _cmd); - if (!cache) { - cache = [[QMUICellHeightIndexPathCache alloc] init]; - objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - return cache; -} - -@end - -@implementation UICollectionView (QMUIIndexPathHeightCacheInvalidation) - -- (void)qmui_reloadDataWithoutInvalidateIndexPathHeightCache { - [self qmui_reloadData]; -} - -+ (void)load { - SEL selectors[] = { - @selector(reloadData), - @selector(insertSections:), - @selector(deleteSections:), - @selector(reloadSections:), - @selector(moveSection:toSection:), - @selector(insertItemsAtIndexPaths:), - @selector(deleteItemsAtIndexPaths:), - @selector(reloadItemsAtIndexPaths:), - @selector(moveItemAtIndexPath:toIndexPath:) - }; - for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) { - SEL originalSelector = selectors[index]; - SEL swizzledSelector = NSSelectorFromString([@"qmui_" stringByAppendingString:NSStringFromSelector(originalSelector)]); - Method originalMethod = class_getInstanceMethod(self, originalSelector); - Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector); - method_exchangeImplementations(originalMethod, swizzledMethod); - } -} - -- (void)qmui_reloadData { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - [heightsBySection removeAllObjects]; - }]; - } - [self qmui_reloadData]; -} - -- (void)qmui_insertSections:(NSIndexSet *)sections { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) { - [self.qmui_indexPathHeightCache buildSectionsIfNeeded:section]; - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - [heightsBySection insertObject:[NSMutableArray array] atIndex:section]; - }]; - }]; - } - [self qmui_insertSections:sections]; -} - -- (void)qmui_deleteSections:(NSIndexSet *)sections { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) { - [self.qmui_indexPathHeightCache buildSectionsIfNeeded:section]; - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - [heightsBySection removeObjectAtIndex:section]; - }]; - }]; - } - [self qmui_deleteSections:sections]; -} - -- (void)qmui_reloadSections:(NSIndexSet *)sections { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [sections enumerateIndexesUsingBlock: ^(NSUInteger section, BOOL *stop) { - [self.qmui_indexPathHeightCache buildSectionsIfNeeded:section]; - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - [heightsBySection[section] removeAllObjects]; - }]; - }]; - } - [self qmui_reloadSections:sections]; -} - -- (void)qmui_moveSection:(NSInteger)section toSection:(NSInteger)newSection { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache buildSectionsIfNeeded:section]; - [self.qmui_indexPathHeightCache buildSectionsIfNeeded:newSection]; - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - [heightsBySection exchangeObjectAtIndex:section withObjectAtIndex:newSection]; - }]; - } - [self qmui_moveSection:section toSection:newSection]; -} - -- (void)qmui_insertItemsAtIndexPaths:(NSArray *)indexPaths { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:indexPaths]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - NSMutableArray *rows = heightsBySection[indexPath.section]; - [rows insertObject:@(-1) atIndex:indexPath.item]; - }]; - }]; - } - [self qmui_insertItemsAtIndexPaths:indexPaths]; -} - -- (void)qmui_deleteItemsAtIndexPaths:(NSArray *)indexPaths { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:indexPaths]; - NSMutableDictionary *mutableIndexSetsToRemove = [NSMutableDictionary dictionary]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - NSMutableIndexSet *mutableIndexSet = mutableIndexSetsToRemove[@(indexPath.section)]; - if (!mutableIndexSet) { - mutableIndexSet = [NSMutableIndexSet indexSet]; - mutableIndexSetsToRemove[@(indexPath.section)] = mutableIndexSet; - } - [mutableIndexSet addIndex:indexPath.item]; - }]; - [mutableIndexSetsToRemove enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, NSIndexSet *indexSet, BOOL *stop) { - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - NSMutableArray *rows = heightsBySection[key.integerValue]; - [rows removeObjectsAtIndexes:indexSet]; - }]; - }]; - } - [self qmui_deleteItemsAtIndexPaths:indexPaths]; -} - -- (void)qmui_reloadItemsAtIndexPaths:(NSArray *)indexPaths { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:indexPaths]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - NSMutableArray *rows = heightsBySection[indexPath.section]; - rows[indexPath.item] = @(-1); - }]; - }]; - } - [self qmui_reloadItemsAtIndexPaths:indexPaths]; -} - -- (void)qmui_moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:@[sourceIndexPath, destinationIndexPath]]; - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - if (heightsBySection.count > 0 && heightsBySection.count > sourceIndexPath.section && heightsBySection.count > destinationIndexPath.section) { - NSMutableArray *sourceRows = heightsBySection[sourceIndexPath.section]; - NSMutableArray *destinationRows = heightsBySection[destinationIndexPath.section]; - NSNumber *sourceValue = sourceRows[sourceIndexPath.item]; - NSNumber *destinationValue = destinationRows[destinationIndexPath.item]; - sourceRows[sourceIndexPath.item] = destinationValue; - destinationRows[destinationIndexPath.item] = sourceValue; - } - }]; - } - [self qmui_moveItemAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; -} - -@end - -@implementation UICollectionView (QMUILayoutCell) - -- (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"); - NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd); - if (!templateCellsByIdentifiers) { - templateCellsByIdentifiers = @{}.mutableCopy; - objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - UICollectionViewCell *templateCell = templateCellsByIdentifiers[identifier]; - if (!templateCell) { - // 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); - } - templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO; - templateCellsByIdentifiers[identifier] = templateCell; - NSLog(@"layout cell created - %@", identifier); - return templateCell; -} - -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth configuration:(void (^)(id cell))configuration { - if (!identifier || CGRectIsEmpty(self.bounds)) { - return 0; - } - UICollectionViewCell *cell = [self templateCellForReuseIdentifier:identifier cellClass:cellClass]; - [cell prepareForReuse]; - if (configuration) { configuration(cell); } - CGSize fitSize = CGSizeZero; - if (cell && itemWidth > 0) { - SEL selector = @selector(sizeThatFits:); - BOOL inherited = ![cell isMemberOfClass:[UICollectionViewCell class]]; - BOOL overrided = [cell.class instanceMethodForSelector:selector] != [UICollectionViewCell instanceMethodForSelector:selector]; - if (inherited && !overrided) { - NSAssert(NO, @"Customized cell must override '-sizeThatFits:' method if not using auto layout."); - } - fitSize = [cell sizeThatFits:CGSizeMake(itemWidth, CGFLOAT_MAX)]; - } - return ceil(fitSize.height); -} - -// 通过indexPath缓存高度 -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id cell))configuration { - if (!identifier || !indexPath || CGRectIsEmpty(self.bounds)) { - return 0; - } - if ([self.qmui_indexPathHeightCache existsHeightAtIndexPath:indexPath]) { - return [self.qmui_indexPathHeightCache heightForIndexPath:indexPath]; - } - CGFloat height = [self qmui_heightForCellWithIdentifier:identifier cellClass:cellClass itemWidth:itemWidth configuration:configuration]; - [self.qmui_indexPathHeightCache cacheHeight:height byIndexPath:indexPath]; - return height; -} - -// 通过key缓存高度 -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cellClass:(Class)cellClass itemWidth:(CGFloat)itemWidth cacheByKey:(id)key configuration:(void (^)(id cell))configuration { - if (!identifier || !key || CGRectIsEmpty(self.bounds)) { - return 0; - } - if ([self.qmui_keyedHeightCache existsHeightForKey:key]) { - return [self.qmui_keyedHeightCache heightForKey:key]; - } - CGFloat height = [self qmui_heightForCellWithIdentifier:identifier cellClass:cellClass itemWidth:itemWidth configuration:configuration]; - [self.qmui_keyedHeightCache cacheHeight:height byKey:key]; - return height; + + return visibleIndexPaths.firstObject; } @end 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 98e9ec16..ffc3fa03 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.h @@ -1,19 +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. + */ + // // UIColor+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import +#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) @@ -21,67 +32,79 @@ * * @return UIColor对象 */ -+ (UIColor *)qmui_colorWithHexString:(NSString *)hexString; ++ (nullable UIColor *)qmui_colorWithHexString:(nullable NSString *)hexString; /** * 将当前色值转换为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" */ -- (NSString *)qmui_hexString; ++ (nullable UIColor *)qmui_colorWithRGBAString:(nullable NSString *)rgbaString; /** - * 获取当前UIColor对象里的红色色值 + 将当前色值转换成“255,255,255,1.00”的字符串,如果 alpha 通道为1也会输出出来。其中 alpha 通道必定带两位小数点,其他三个通道都是整数。 + */ +@property(nonatomic, copy, readonly) NSString *qmui_RGBAString; + +/** + * 获取当前 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后返回 * * @return alpha通道为1.0,其他rgb通道与原UIColor对象一致的新UIColor对象 */ -- (UIColor *)qmui_colorWithoutAlpha; +- (nullable UIColor *)qmui_colorWithoutAlpha; /** * 计算当前color叠加了alpha之后放在指定颜色的背景上的色值 */ -- (UIColor *)qmui_colorWithAlpha:(CGFloat)alpha backgroundColor:(UIColor *)backgroundColor; +- (UIColor *)qmui_colorWithAlpha:(CGFloat)alpha backgroundColor:(nullable UIColor *)backgroundColor; /** * 计算当前color叠加了alpha之后放在白色背景上的色值 @@ -93,7 +116,7 @@ * @param toColor 目标颜色 * @param progress 变化程度,取值范围0.0f~1.0f */ -- (UIColor *)qmui_transitionToColor:(UIColor *)toColor progress:(CGFloat)progress; +- (UIColor *)qmui_transitionToColor:(nullable UIColor *)toColor progress:(CGFloat)progress; /** * 判断当前颜色是否为深色,可用于根据不同色调动态设置不同文字颜色的场景。 @@ -102,10 +125,10 @@ * * @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 */ @@ -115,12 +138,18 @@ * 判断当前颜色是否等于系统默认的 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 色值 */ -+ (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; /** * 计算两个颜色叠加之后的最终色(注意区分前景色后景色的顺序)
@@ -142,3 +171,35 @@ + (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 ea7b18f4..af1140cd 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIColor+QMUI.m @@ -1,14 +1,23 @@ +/** + * 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+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import "UIColor+QMUI.h" #import "QMUICore.h" #import "NSString+QMUI.h" +#import "NSObject+QMUI.h" +#import "NSArray+QMUI.h" @implementation UIColor (QMUI) @@ -16,20 +25,20 @@ + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 使用 [UIColor colorWithRed:green:blue:alpha:] 或 [UIColor colorWithHue:saturation:brightness:alpha:] 方法创建的颜色是 UIDeviceRGBColor 类型的而不是 UIColor 类型的 - ReplaceMethod([UIColor colorWithRed:1 green:1 blue:1 alpha:1].class, @selector(description), @selector(qmui_description)); + ExtendImplementationOfNonVoidMethodWithoutArguments([[UIColor colorWithRed:1 green:1 blue:1 alpha:1] class], @selector(description), NSString *, ^NSString *(UIColor *selfObject, NSString *originReturnValue) { + NSInteger red = selfObject.qmui_red * 255; + NSInteger green = selfObject.qmui_green * 255; + NSInteger blue = selfObject.qmui_blue * 255; + CGFloat alpha = selfObject.qmui_alpha; + NSString *description = ([NSString stringWithFormat:@"%@, RGBA(%@, %@, %@, %.2f), %@", originReturnValue, @(red), @(green), @(blue), alpha, [selfObject qmui_hexString]]); + return description; + }); }); } -- (NSString *)qmui_description { - NSInteger red = self.qmui_red * 255; - NSInteger green = self.qmui_green * 255; - NSInteger blue = self.qmui_blue * 255; - CGFloat alpha = self.qmui_alpha; - NSString *description = [NSString stringWithFormat:@"%@, RGBA(%@, %@, %@, %.2f), %@", [self qmui_description], @(red), @(green), @(blue), alpha, [self qmui_hexString]]; - return description; -} - -+ (UIColor *)qmui_colorWithHexString: (NSString *) hexString { ++ (UIColor *)qmui_colorWithHexString:(NSString *)hexString { + if (hexString.length <= 0) return nil; + NSString *colorString = [[hexString stringByReplacingOccurrencesOfString: @"#" withString: @""] uppercaseString]; CGFloat alpha, red, blue, green; switch ([colorString length]) { @@ -57,8 +66,10 @@ + (UIColor *)qmui_colorWithHexString: (NSString *) hexString { green = [self colorComponentFrom: colorString start: 4 length: 2]; blue = [self colorComponentFrom: colorString start: 6 length: 2]; break; - default: - [NSException raise:@"Invalid color value" format: @"Color value %@ is invalid. It should be a hex value of the form #RBG, #ARGB, #RRGGBB, or #AARRGGBB", hexString]; + default: { + 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; } return [UIColor colorWithRed: red green: green blue: blue alpha: alpha]; @@ -76,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; @@ -170,13 +204,14 @@ - (UIColor *)qmui_transitionToColor:(UIColor *)toColor progress:(CGFloat)progres } - (BOOL)qmui_colorIsDark { - CGFloat red = 0.0, green = 0.0, blue = 0.0, alpha = 0.0; - [self getRed:&red green:&green blue:&blue alpha:&alpha]; - - float referenceValue = 0.411; - float colorDelta = ((red * 0.299) + (green * 0.587) + (blue * 0.114)); - - return 1.0 - colorDelta > referenceValue; + 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)); + + return 1.0 - colorDelta > referenceValue; + } + return YES; } - (UIColor *)qmui_inverseColor { @@ -192,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) { @@ -247,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 b7c5aa76..4f40d142 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.h @@ -1,9 +1,16 @@ +/** + * 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. + */ + // // UIControl+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import @@ -11,22 +18,33 @@ @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) BOOL qmui_needsTakeOverTouchEvent DEPRECATED_MSG_ATTRIBUTE("已在 1.7.0 版本中废弃,请使用 qmui_automaticallyAdjustTouchHighlightedInScrollView 来代替"); - -/* - * 响应区域需要改变的大小,负值表示往外扩大,正值表示往内缩小 +/** + 当快速重复点击某个 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) UIEdgeInsets qmui_outsideEdge; +@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); @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.m index 46f22432..130c165f 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIControl+QMUI.m @@ -1,176 +1,310 @@ +/** + * 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. + */ + // // UIControl+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import "UIControl+QMUI.h" -#import #import "QMUICore.h" -static char kAssociatedObjectKey_needsTakeOverTouchEvent; -static char kAssociatedObjectKey_automaticallyAdjustTouchHighlightedInScrollView; -static char kAssociatedObjectKey_canSetHighlighted; -static char kAssociatedObjectKey_touchEndCount; -static char kAssociatedObjectKey_outsideEdge; - @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) -- (void)setQmui_needsTakeOverTouchEvent:(BOOL)qmui_needsTakeOverTouchEvent { - objc_setAssociatedObject(self, &kAssociatedObjectKey_needsTakeOverTouchEvent, [NSNumber numberWithBool:qmui_needsTakeOverTouchEvent], OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} +QMUISynthesizeBOOLProperty(qmuictl_canSetHighlighted, setQmuictl_canSetHighlighted) +QMUISynthesizeNSIntegerProperty(qmuictl_touchEndCount, setQmuictl_touchEndCount) -- (BOOL)qmui_needsTakeOverTouchEvent { - return (BOOL)[objc_getAssociatedObject(self, &kAssociatedObjectKey_needsTakeOverTouchEvent) boolValue]; -} +#pragma mark - Automatically Adjust Touch Highlighted In ScrollView +static char kAssociatedObjectKey_automaticallyAdjustTouchHighlightedInScrollView; - (void)setQmui_automaticallyAdjustTouchHighlightedInScrollView:(BOOL)qmui_automaticallyAdjustTouchHighlightedInScrollView { - objc_setAssociatedObject(self, &kAssociatedObjectKey_automaticallyAdjustTouchHighlightedInScrollView, [NSNumber numberWithBool:qmui_automaticallyAdjustTouchHighlightedInScrollView], OBJC_ASSOCIATION_RETAIN_NONATOMIC); + 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 + 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) { + + if (selfObject.qmui_automaticallyAdjustTouchHighlightedInScrollView) { + 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]; + [weakSelf sendActionsForAllTouchEventsIfCan]; + if (weakSelf.highlighted) { + [weakSelf setHighlighted:NO]; + } + }); + } else { + [selfObject setHighlighted:NO]; + } + 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) = ^{ + void (*originSelectorIMP)(id, SEL, NSSet *, UIEvent *); + originSelectorIMP = (void (*)(id, SEL, NSSet *, UIEvent *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, touches, event); + }; + + if (selfObject.qmui_automaticallyAdjustTouchHighlightedInScrollView) { + selfObject.qmuictl_canSetHighlighted = NO; + callSuperBlock(); + if (selfObject.highlighted) { + [selfObject setHighlighted:NO]; + } + return; + } + callSuperBlock(); + }; + }); + } oncePerIdentifier:@"UIControl automaticallyAdjustTouchHighlightedInScrollView"]; + } } - (BOOL)qmui_automaticallyAdjustTouchHighlightedInScrollView { - return (BOOL)[objc_getAssociatedObject(self, &kAssociatedObjectKey_automaticallyAdjustTouchHighlightedInScrollView) boolValue]; + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_automaticallyAdjustTouchHighlightedInScrollView)) boolValue]; } -- (void)setCanSetHighlighted:(BOOL)canSetHighlighted { - objc_setAssociatedObject(self, &kAssociatedObjectKey_canSetHighlighted, [NSNumber numberWithBool:canSetHighlighted], OBJC_ASSOCIATION_RETAIN_NONATOMIC); +// 这段代码需要以一个独立的方法存在,因为一旦有坑,外面可以直接通过runtime调用这个方法 +// 但,不要开放到.h文件里,理论上外面不应该用到它 +- (void)sendActionsForAllTouchEventsIfCan { + self.qmuictl_touchEndCount += 1; + if (self.qmuictl_touchEndCount == 1) { + [self sendActionsForControlEvents:UIControlEventAllTouchEvents]; + } } -- (BOOL)canSetHighlighted { - return (BOOL)[objc_getAssociatedObject(self, &kAssociatedObjectKey_canSetHighlighted) boolValue]; -} +#pragma mark - Prevents Repeated TouchUpInside Event -- (void)setTouchEndCount:(NSInteger)touchEndCount { - objc_setAssociatedObject(self, &kAssociatedObjectKey_touchEndCount, @(touchEndCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +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"]; + } } -- (NSInteger)touchEndCount { - return [objc_getAssociatedObject(self, &kAssociatedObjectKey_touchEndCount) integerValue]; +- (BOOL)qmui_preventsRepeatedTouchUpInsideEvent { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_preventsRepeatedTouchUpInsideEvent)) boolValue]; } -- (void)setQmui_outsideEdge:(UIEdgeInsets)qmui_outsideEdge { - objc_setAssociatedObject(self, &kAssociatedObjectKey_outsideEdge, [NSValue valueWithUIEdgeInsets:qmui_outsideEdge], OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} +#pragma mark - Highlighted Block -- (UIEdgeInsets)qmui_outsideEdge { - return [objc_getAssociatedObject(self, &kAssociatedObjectKey_outsideEdge) UIEdgeInsetsValue]; +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)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - - Class clz = [self class]; - - SEL beginSelector = @selector(touchesBegan:withEvent:); - SEL swizzled_beginSelector = @selector(qmui_touchesBegan:withEvent:); - - SEL moveSelector = @selector(touchesMoved:withEvent:); - SEL swizzled_moveSelector = @selector(qmui_touchesMoved:withEvent:); - - SEL endSelector = @selector(touchesEnded:withEvent:); - SEL swizzled_endSelector = @selector(qmui_touchesEnded:withEvent:); - - SEL cancelSelector = @selector(touchesCancelled:withEvent:); - SEL swizzled_cancelSelector = @selector(qmui_touchesCancelled:withEvent:); - - SEL pointInsideSelector = @selector(pointInside:withEvent:); - SEL swizzled_pointInsideSelector = @selector(qmui_pointInside:withEvent:); - - ReplaceMethod(clz, beginSelector, swizzled_beginSelector); - ReplaceMethod(clz, moveSelector, swizzled_moveSelector); - ReplaceMethod(clz, endSelector, swizzled_endSelector); - ReplaceMethod(clz, cancelSelector, swizzled_cancelSelector); - ReplaceMethod(clz, pointInsideSelector, swizzled_pointInsideSelector); - - }); +- (void (^)(BOOL))qmui_setHighlightedBlock { + return (void (^)(BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_setHighlightedBlock); } -BeginIgnoreDeprecatedWarning -- (void)qmui_touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { - self.touchEndCount = 0; - if (self.qmui_automaticallyAdjustTouchHighlightedInScrollView || self.qmui_needsTakeOverTouchEvent) { - self.canSetHighlighted = YES; - [self qmui_touchesBegan:touches withEvent:event]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - if (self.canSetHighlighted) { - [self setHighlighted:YES]; - } - }); - } else { - [self qmui_touchesBegan:touches withEvent:event]; +#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)qmui_touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { - if (self.qmui_automaticallyAdjustTouchHighlightedInScrollView || self.qmui_needsTakeOverTouchEvent) { - self.canSetHighlighted = NO; - [self qmui_touchesMoved:touches withEvent:event]; - } else { - [self qmui_touchesMoved:touches withEvent:event]; - } +- (void (^)(BOOL))qmui_setSelectedBlock { + return (void (^)(BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_setSelectedBlock); } -- (void)qmui_touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { - if (self.qmui_automaticallyAdjustTouchHighlightedInScrollView || self.qmui_needsTakeOverTouchEvent) { - self.canSetHighlighted = NO; - if (self.touchInside) { - [self setHighlighted:YES]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.02 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - // 如果延迟时间太长,会导致快速点击两次,事件会触发两次 - // 对于 3D Touch 的机器,如果点击按钮的时候在按钮上停留事件稍微长一点点,那么 touchesEnded 会被调用两次 - // 把 super touchEnded 放到延迟里调用会导致长按无法触发点击,先这么改,再想想怎么办。// [self qmui_touchesEnded:touches withEvent:event]; - [self sendActionsForAllTouchEventsIfCan]; - if (self.highlighted) { - [self setHighlighted:NO]; - } +#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); + } + }; }); - } else { - [self setHighlighted:NO]; - } - } else { - [self qmui_touchesEnded:touches withEvent:event]; + } oncePerIdentifier:@"UIControl setEnabled:"]; } } -- (void)qmui_touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { - if (self.qmui_automaticallyAdjustTouchHighlightedInScrollView || self.qmui_needsTakeOverTouchEvent) { - self.canSetHighlighted = NO; - [self qmui_touchesCancelled:touches withEvent:event]; - if (self.highlighted) { - [self setHighlighted:NO]; - } +- (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]; } else { - [self qmui_touchesCancelled:touches withEvent:event]; + [self addTarget:self action:action forControlEvents:UIControlEventTouchUpInside]; } + objc_setAssociatedObject(self, &kAssociatedObjectKey_tapBlock, qmui_tapBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); } -EndIgnoreDeprecatedWarning -- (BOOL)qmui_pointInside:(CGPoint)point withEvent:(UIEvent *)event { - if (([event type] != UIEventTypeTouches)) { - return [self qmui_pointInside:point withEvent:event]; - } - UIEdgeInsets qmui_outsideEdge = self.qmui_outsideEdge; - CGRect boundsInsetOutsideEdge = CGRectMake(CGRectGetMinX(self.bounds) + qmui_outsideEdge.left, CGRectGetMinY(self.bounds) + qmui_outsideEdge.top, CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(qmui_outsideEdge), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(qmui_outsideEdge)); - return CGRectContainsPoint(boundsInsetOutsideEdge, point); +- (void (^)(__kindof UIControl *))qmui_tapBlock { + return (void (^)(__kindof UIControl *))objc_getAssociatedObject(self, &kAssociatedObjectKey_tapBlock); } -// 这段代码需要以一个独立的方法存在,因为一旦有坑,外面可以直接通过runtime调用这个方法 -// 但,不要开放到.h文件里,理论上外面不应该用到它 -- (void)sendActionsForAllTouchEventsIfCan { - self.touchEndCount += 1; - if (self.touchEndCount == 1) { - [self sendActionsForControlEvents:UIControlEventAllTouchEvents]; +- (void)qmui_handleTouchUpInside:(__kindof UIControl *)sender { + if (self.qmui_tapBlock) { + self.qmui_tapBlock(self); } } diff --git a/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.h index 3b01cdf3..25a587e2 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.h @@ -1,17 +1,43 @@ +/** + * 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. + */ + // // UIFont+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import +#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 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) @@ -22,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 90bbaeb0..3c4f0b72 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIFont+QMUI.m @@ -1,9 +1,16 @@ +/** + * 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. + */ + // // UIFont+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import "UIFont+QMUI.h" @@ -12,46 +19,42 @@ @implementation UIFont (QMUI) + (UIFont *)qmui_lightSystemFontOfSize:(CGFloat)fontSize { - return [UIFont fontWithName:IOS_VERSION >= 9.0 ? @".SFUIText-Light" : @"HelveticaNeue-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 = IOS_VERSION < 9.0 ? @"HelveticaNeue" : @".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 ([self.class respondsToSelector:@selector(systemFontOfSize:weight:)]) { - 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 new file mode 100644 index 00000000..2fb029fc --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.h @@ -0,0 +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. + */ + +// +// UIGestureRecognizer+QMUI.h +// qmui +// +// Created by QMUI Team on 2017/8/21. +// + +#import + +@interface UIGestureRecognizer (QMUI) + +/// 获取当前手势直接作用到的 view(注意与 view 属性区分开:view 属性表示手势被添加到哪个 view 上,qmui_targetView 则是 view 属性里的某个 subview) +@property(nullable, nonatomic, weak, readonly) UIView *qmui_targetView; +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.m new file mode 100644 index 00000000..203eee65 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.m @@ -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. + */ + +// +// UIGestureRecognizer+QMUI.m +// qmui +// +// Created by QMUI Team on 2017/8/21. +// + +#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]; + return targetView; +} + +@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.h index e49795e8..06af72f7 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.h @@ -1,9 +1,16 @@ +/** + * 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+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import @@ -11,15 +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 按钮图片 @@ -35,8 +41,49 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { QMUIImageBorderPositionRight = 1 << 3, }; +typedef NS_ENUM(NSInteger, QMUIImageResizingMode) { + QMUIImageResizingModeScaleToFill = 0, // 将图片缩放到给定的大小,不考虑宽高比例 + QMUIImageResizingModeScaleAspectFit = 10, // 默认的缩放方式,将图片保持宽高比例不变的情况下缩放到不超过给定的大小(但缩放后的大小不一定与给定大小相等),不会产生空白也不会产生裁剪 + QMUIImageResizingModeScaleAspectFill = 20, // 将图片保持宽高比例不变的情况下缩放到不超过给定的大小(但缩放后的大小不一定与给定大小相等),若有内容超出则会被裁剪。若裁剪则上下居中裁剪。 + QMUIImageResizingModeScaleAspectFillTop, // 将图片保持宽高比例不变的情况下缩放到不超过给定的大小(但缩放后的大小不一定与给定大小相等),若有内容超出则会被裁剪。若裁剪则水平居中、垂直居上裁剪。 + QMUIImageResizingModeScaleAspectFillBottom // 将图片保持宽高比例不变的情况下缩放到不超过给定的大小(但缩放后的大小不一定与给定大小相等),若有内容超出则会被裁剪。若裁剪则水平居中、垂直居下裁剪。 +}; + +typedef NS_ENUM(NSInteger, QMUIImageGradientType) { + QMUIImageGradientTypeHorizontal, + QMUIImageGradientTypeVertical, + QMUIImageGradientTypeTopLeftToBottomRight, + QMUIImageGradientTypeTopRightToBottomLeft, + QMUIImageGradientTypeRadial, +}; + @interface UIImage (QMUI) +/** + 用于绘制一张图并以 UIImage 的形式返回 + + @param size 要绘制的图片的 size,宽或高均不能为 0 + @param opaque 图片是否不透明,YES 表示不透明,NO 表示半透明 + @param scale 图片的倍数,0 表示取当前屏幕的倍数 + @param actionBlock 实际的图片绘制操作,在这里只管绘制就行,不用手动生成 image + @return 返回绘制完的图片 + */ ++ (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; + +/** + * 判断一张图是否不存在 alpha 通道,注意 “不存在 alpha 通道” 不等价于 “不透明”。一张不透明的图有可能是存在 alpha 通道但 alpha 值为 1。 + */ +- (BOOL)qmui_opaque; + /** * 获取当前图片的均色,原理是将图片绘制到1px*1px的矩形内,再从当前区域取色,得到图片的均色。 * @link http://www.bobbygeorgescu.com/2011/08/finding-average-color-of-uiimage/ @/link @@ -50,7 +97,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 已经置灰的图片 */ -- (UIImage *)qmui_grayImage; +- (nullable UIImage *)qmui_grayImage; /** * 设置一张图片的透明度 @@ -59,12 +106,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 设置了透明度之后的图片 */ -- (UIImage *)qmui_imageWithAlpha:(CGFloat)alpha; - -/** - * 判断一张图是否不存在 alpha 通道,注意 “不存在 alpha 通道” 不等价于 “不透明”。一张不透明的图有可能是存在 alpha 通道但 alpha 值为 1。 - */ -- (BOOL)qmui_opaque; +- (nullable UIImage *)qmui_imageWithAlpha:(CGFloat)alpha; /** * 保持当前图片的形状不变,使用指定的颜色去重新渲染它,生成一张新图片并返回 @@ -73,7 +115,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 与当前图片形状一致但颜色与参数tintColor相同的新图片 */ -- (UIImage *)qmui_imageWithTintColor:(UIColor *)tintColor; +- (nullable UIImage *)qmui_imageWithTintColor:(nullable UIColor *)tintColor; /** * 以 CIColorBlendMode 的模式为当前图片叠加一个颜色,生成一张新图片并返回,在叠加过程中会保留图片内的纹理。 @@ -84,7 +126,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @warning 这个方法可能比较慢,会卡住主线程,建议异步使用 */ -- (UIImage *)qmui_imageWithBlendColor:(UIColor *)blendColor; +- (nullable UIImage *)qmui_imageWithBlendColor:(nullable UIColor *)blendColor; /** * 在当前图片的基础上叠加一张图片,并指定绘制叠加图片的起始位置 @@ -96,14 +138,14 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 返回一张与原图大小一致的图片,所叠加的图片若超出原图大小,则超出部分被截掉 */ -- (UIImage *)qmui_imageWithImageAbove:(UIImage *)image atPoint:(CGPoint)point; +- (nullable UIImage *)qmui_imageWithImageAbove:(UIImage *)image atPoint:(CGPoint)point; /** * 在当前图片的上下左右增加一些空白(不支持负值),通常用于调节NSAttributedString里的图片与文字的间距 * @param extension 要拓展的大小 * @return 拓展后的图片 */ -- (UIImage *)qmui_imageWithSpacingExtensionInsets:(UIEdgeInsets)extension; +- (nullable UIImage *)qmui_imageWithSpacingExtensionInsets:(UIEdgeInsets)extension; /** * 切割出在指定位置中的图片 @@ -112,36 +154,52 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 切割后的新图片 */ -- (UIImage *)qmui_imageWithClippedRect:(CGRect)rect; +- (nullable UIImage *)qmui_imageWithClippedRect:(CGRect)rect; + /** - * 将原图按 UIViewContentModeScaleAspectFit 的方式进行缩放,并返回缩放后的图片,处理完的图片的 scale 保持与原图一致。 - * @param size 缩放后的图片尺寸不超过这个尺寸 + * 切割出在指定圆角的图片 + * + * @param cornerRadius 要切割的圆角值 + * + * @return 切割后的新图片 + */ +- (nullable UIImage *)qmui_imageWithClippedCornerRadius:(CGFloat)cornerRadius; + +/** + * 同上,可以设置 scale + */ + +- (nullable UIImage *)qmui_imageWithClippedCornerRadius:(CGFloat)cornerRadius scale:(CGFloat)scale; + +/** + * 将原图以 QMUIImageResizingModeScaleAspectFit 的策略缩放,使其缩放后的大小不超过指定的大小,并返回缩放后的图片。缩放后的图片的倍数保持与原图一致。 + * @param size 在这个约束的 size 内进行缩放后的大小,处理后返回的图片的 size 会根据 resizingMode 不同而不同,但必定不会超过 size。 * * @return 处理完的图片 - * @see qmui_imageWithScaleToSize:contentMode:scale: + * @see qmui_imageResizedInLimitedSize:resizingMode:scale: */ -- (UIImage *)qmui_imageWithScaleToSize:(CGSize)size; +- (nullable UIImage *)qmui_imageResizedInLimitedSize:(CGSize)size; /** - * 将原图按指定的 UIViewContentMode 缩放到指定的大小,返回处理完的图片,处理完的图片的 scale 保持与原图一致 - * @param size 在这个约束的 size 内进行缩放后的大小,处理后返回的图片的 size 会根据 contentMode 不同而不同 - * @param contentMode 希望使用的缩放模式,目前仅支持 UIViewContentModeScaleToFill、UIViewContentModeScaleAspectFill、UIViewContentModeScaleAspectFit(默认) + * 将原图按指定的 QMUIImageResizingMode 缩放,使其缩放后的大小不超过指定的大小,并返回缩放后的图片,缩放后的图片的倍数保持与原图一致。 + * @param size 在这个约束的 size 内进行缩放后的大小,处理后返回的图片的 size 会根据 resizingMode 不同而不同,但必定不会超过 size。 + * @param resizingMode 希望使用的缩放模式 * * @return 处理完的图片 - * @see qmui_imageWithScaleToSize:contentMode:scale: + * @see qmui_imageResizedInLimitedSize:resizingMode:scale: */ -- (UIImage *)qmui_imageWithScaleToSize:(CGSize)size contentMode:(UIViewContentMode)contentMode; +- (nullable UIImage *)qmui_imageResizedInLimitedSize:(CGSize)size resizingMode:(QMUIImageResizingMode)resizingMode; /** - * 将原图按指定的 UIViewContentMode 缩放到指定的大小,返回处理完的图片 - * @param size 在这个约束的 size 内进行缩放后的大小,处理后返回的图片的 size 会根据 contentMode 不同而不同 - * @param contentMode 希望使用的缩放模式,目前仅支持 UIViewContentModeScaleToFill、UIViewContentModeScaleAspectFill、UIViewContentModeScaleAspectFit(默认) - * @param scale 处理后返回的图片的 scale + * 将原图按指定的 QMUIImageResizingMode 缩放,使其缩放后的大小不超过指定的大小,并返回缩放后的图片。 + * @param size 在这个约束的 size 内进行缩放后的大小,处理后返回的图片的 size 会根据 resizingMode 不同而不同,但必定不会超过 size。 + * @param resizingMode 希望使用的缩放模式 + * @param scale 用于指定缩放后的图片的倍数 * * @return 处理完的图片 */ -- (UIImage *)qmui_imageWithScaleToSize:(CGSize)size contentMode:(UIViewContentMode)contentMode scale:(CGFloat)scale; +- (nullable UIImage *)qmui_imageResizedInLimitedSize:(CGSize)size resizingMode:(QMUIImageResizingMode)resizingMode scale:(CGFloat)scale; /** * 将原图进行旋转,只能选择上下左右四个方向 @@ -150,7 +208,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 处理完的图片 */ -- (UIImage *)qmui_imageWithOrientation:(UIImageOrientation)direction; +- (nullable UIImage *)qmui_imageWithOrientation:(UIImageOrientation)direction; /** * 为图片加上一个border,border的路径为path @@ -161,7 +219,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * @return 带border的UIImage * @warning 注意通过`path.lineWidth`设置边框大小,同时注意路径要考虑像素对齐(`path.lineWidth / 2.0`) */ -- (UIImage *)qmui_imageWithBorderColor:(UIColor *)borderColor path:(UIBezierPath *)path; +- (nullable UIImage *)qmui_imageWithBorderColor:(nullable UIColor *)borderColor path:(nullable UIBezierPath *)path; /** * 为图片加上一个border,border的路径为borderColor、cornerRadius和borderWidth所创建的path @@ -174,8 +232,8 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 带border的UIImage */ -- (UIImage *)qmui_imageWithBorderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth cornerRadius:(CGFloat)cornerRadius dashedLengths:(const CGFloat *)dashedLengths; -- (UIImage *)qmui_imageWithBorderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth cornerRadius:(CGFloat)cornerRadius; +- (nullable UIImage *)qmui_imageWithBorderColor:(nullable UIColor *)borderColor borderWidth:(CGFloat)borderWidth cornerRadius:(CGFloat)cornerRadius dashedLengths:(nullable const CGFloat *)dashedLengths; +- (nullable UIImage *)qmui_imageWithBorderColor:(nullable UIColor *)borderColor borderWidth:(CGFloat)borderWidth cornerRadius:(CGFloat)cornerRadius; /** @@ -187,7 +245,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 带border的UIImage */ -- (UIImage *)qmui_imageWithBorderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth borderPosition:(QMUIImageBorderPosition)borderPosition; +- (nullable UIImage *)qmui_imageWithBorderColor:(nullable UIColor *)borderColor borderWidth:(CGFloat)borderWidth borderPosition:(QMUIImageBorderPosition)borderPosition; /** * 返回一个被mask的图片 @@ -197,7 +255,43 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 被mask的图片 */ -- (UIImage *)qmui_imageWithMaskImage:(UIImage *)maskImage usingMaskImageMode:(BOOL)usingMaskImageMode; +- (nullable UIImage *)qmui_imageWithMaskImage:(UIImage *)maskImage usingMaskImageMode:(BOOL)usingMaskImageMode; + +/** + 将 data 转换成 animated UIImage(如果非 animated 则转换成普通 UIImage),image 倍数为 1(与系统的 [UIImage imageWithData:] 接口一致) + + @param data 图片文件的 data + @return 转换成的 UIImage + */ ++ (nullable UIImage *)qmui_animatedImageWithData:(NSData *)data; + +/** + 将 data 转换成 animated UIImage(如果非 animated 则转换成普通 UIImage) + + @param data 图片文件的 data + @param scale 图片的倍数,0 表示获取当前设备的屏幕倍数 + @return 转换成的 UIImage + @see http://www.jianshu.com/p/767af9c690a3 + @see https://github.com/rs/SDWebImage + */ ++ (nullable UIImage *)qmui_animatedImageWithData:(NSData *)data scale:(CGFloat)scale; + +/** + 在 mainBundle 里找到对应名字的图片, 注意图片 scale 为 1,与系统的 [UIImage imageWithData:] 接口一致,若需要修改倍数,请使用 -qmui_animatedImageNamed:scale: + + @param name 图片名,可指定后缀,若不写后缀,默认为“gif”。不写后缀的情况下会先找“gif”后缀的图片,不存在再找无后缀的文件,仍不存在则返回 nil + @return 转换成的 UIImage + */ ++ (nullable UIImage *)qmui_animatedImageNamed:(NSString *)name; + +/** + 在 mainBundle 里找到对应名字的图片 + + @param name 图片名,可指定后缀,若不写后缀,默认为“gif”。不写后缀的情况下会先找“gif”后缀的图片,不存在再找无后缀的文件,仍不存在则返回 nil + @param scale 图片的倍数,0 表示获取当前设备的屏幕倍数 + @return 转换成的 UIImage + */ ++ (nullable UIImage *)qmui_animatedImageNamed:(NSString *)name scale:(CGFloat)scale; /** * 创建一个size为(4, 4)的纯色的UIImage @@ -206,7 +300,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 纯色的UIImage */ -+ (UIImage *)qmui_imageWithColor:(UIColor *)color; ++ (nullable UIImage *)qmui_imageWithColor:(nullable UIColor *)color; /** * 创建一个纯色的UIImage @@ -217,7 +311,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 纯色的UIImage */ -+ (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadius:(CGFloat)cornerRadius; ++ (nullable UIImage *)qmui_imageWithColor:(nullable UIColor *)color size:(CGSize)size cornerRadius:(CGFloat)cornerRadius; /** * 创建一个纯色的UIImage,支持为四个角设置不同的圆角 @@ -225,7 +319,17 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * @param size 图片的大小 * @param cornerRadius 四个角的圆角值的数组,长度必须为4,顺序分别为[左上角、左下角、右下角、右上角] */ -+ (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadiusArray:(NSArray *)cornerRadius; ++ (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 @@ -236,7 +340,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 带border的UIImage */ -+ (UIImage *)qmui_imageWithStrokeColor:(UIColor *)strokeColor size:(CGSize)size path:(UIBezierPath *)path addClip:(BOOL)addClip; ++ (nullable UIImage *)qmui_imageWithStrokeColor:(nullable UIColor *)strokeColor size:(CGSize)size path:(nullable UIBezierPath *)path addClip:(BOOL)addClip; /** * 创建一个带边框路径,没有背景色的路径图片,border的路径为strokeColor、cornerRadius和lineWidth所创建的path @@ -247,7 +351,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 带border的UIImage */ -+ (UIImage *)qmui_imageWithStrokeColor:(UIColor *)strokeColor size:(CGSize)size lineWidth:(CGFloat)lineWidth cornerRadius:(CGFloat)cornerRadius; ++ (nullable UIImage *)qmui_imageWithStrokeColor:(nullable UIColor *)strokeColor size:(CGSize)size lineWidth:(CGFloat)lineWidth cornerRadius:(CGFloat)cornerRadius; /** * 创建一个带边框路径,没有背景色的路径图片(可以是任意一条边,也可以是多条组合;只能创建矩形的border,不能添加圆角) @@ -259,14 +363,14 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * * @return 带路径,没有背景色的UIImage */ -+ (UIImage *)qmui_imageWithStrokeColor:(UIColor *)strokeColor size:(CGSize)size lineWidth:(CGFloat)lineWidth borderPosition:(QMUIImageBorderPosition)borderPosition; ++ (nullable UIImage *)qmui_imageWithStrokeColor:(nullable UIColor *)strokeColor size:(CGSize)size lineWidth:(CGFloat)lineWidth borderPosition:(QMUIImageBorderPosition)borderPosition; /** * 创建一个指定大小和颜色的形状图片 * @param shape 图片形状 * @param size 图片大小 * @param tintColor 图片颜色 */ -+ (UIImage *)qmui_imageWithShape:(QMUIImageShape)shape size:(CGSize)size tintColor:(UIColor *)tintColor; ++ (nullable UIImage *)qmui_imageWithShape:(QMUIImageShape)shape size:(CGSize)size tintColor:(nullable UIColor *)tintColor; /** * 创建一个指定大小和颜色的形状图片 @@ -275,12 +379,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { * @param lineWidth 路径大小,不会影响最终size * @param tintColor 图片颜色 */ -+ (UIImage *)qmui_imageWithShape:(QMUIImageShape)shape size:(CGSize)size lineWidth:(CGFloat)lineWidth tintColor:(UIColor *)tintColor; - -/** - * 将文字渲染成图片,最终图片和文字一样大 - */ -+ (UIImage *)qmui_imageWithAttributedString:(NSAttributedString *)attributedString; ++ (nullable UIImage *)qmui_imageWithShape:(QMUIImageShape)shape size:(CGSize)size lineWidth:(CGFloat)lineWidth tintColor:(nullable UIColor *)tintColor; /** 对传进来的 `UIView` 截图,生成一个 `UIImage` 并返回。注意这里使用的是 view.layer 来渲染图片内容。 @@ -291,7 +390,7 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { @warning UIView 的 transform 并不会在截图里生效 */ -+ (UIImage *)qmui_imageWithView:(UIView *)view; ++ (nullable UIImage *)qmui_imageWithView:(UIView *)view; /** 对传进来的 `UIView` 截图,生成一个 `UIImage` 并返回。注意这里使用的是 iOS 7的系统截图接口。 @@ -303,6 +402,8 @@ typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { @warning UIView 的 transform 并不会在截图里生效 */ -+ (UIImage *)qmui_imageWithView:(UIView *)view afterScreenUpdates:(BOOL)afterUpdates; ++ (nullable UIImage *)qmui_imageWithView:(UIView *)view afterScreenUpdates:(BOOL)afterUpdates; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.m index abe7b83e..5e54ab01 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIImage+QMUI.m @@ -1,15 +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. + */ + // // UIImage+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import "UIImage+QMUI.h" #import "QMUICore.h" #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 @@ -22,19 +34,105 @@ @implementation UIImage (QMUI) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ReplaceMethod([self class], @selector(description), @selector(qmui_description)); + + ExtendImplementationOfNonVoidMethodWithoutArguments([UIImage class], @selector(description), NSString *, ^NSString *(UIImage *selfObject, NSString *originReturnValue) { + return ([NSString stringWithFormat:@"%@, scale = %@", originReturnValue, @(selfObject.scale)]); + }); + + OverrideImplementation([UIImage class], @selector(resizableImageWithCapInsets:resizingMode:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UIImage *selfObject, UIEdgeInsets capInsets, UIImageResizingMode resizingMode) { + + 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 + UIImage *(*originSelectorIMP)(id, SEL, UIEdgeInsets, UIImageResizingMode); + originSelectorIMP = (UIImage *(*)(id, SEL, UIEdgeInsets, UIImageResizingMode))originalIMPProvider(); + UIImage *result = originSelectorIMP(selfObject, originCMD, capInsets, resizingMode); + + 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; + }; + }); }); } -- (NSString *)qmui_description { - return [NSString stringWithFormat:@"%@, scale = %@", [self qmui_description], @(self.scale)]; ++ (UIImage *)qmui_imageWithSize:(CGSize)size opaque:(BOOL)opaque scale:(CGFloat)scale actions:(void (^)(CGContextRef contextRef))actionBlock { + if (!actionBlock || CGSizeIsEmpty(size)) { + return nil; + } + 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; +} + +- (BOOL)qmui_opaque { + CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(self.CGImage); + BOOL opaque = alphaInfo == kCGImageAlphaNoneSkipLast + || alphaInfo == kCGImageAlphaNoneSkipFirst + || alphaInfo == kCGImageAlphaNone; + return opaque; } - (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); @@ -53,16 +151,15 @@ - (UIColor *)qmui_averageColor { - (UIImage *)qmui_grayImage { // CGBitmapContextCreate 是无倍数的,所以要自己换算成1倍 - NSInteger width = self.size.width * self.scale; - NSInteger height = self.size.height * self.scale; + CGSize size = self.qmui_sizeInPixel; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); - CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, colorSpace, kCGBitmapByteOrderDefault); - CGContextInspectContext(context); + CGContextRef context = CGBitmapContextCreate(NULL, size.width, size.height, 8, 0, colorSpace, kCGBitmapByteOrderDefault); + CGContextInspectContext(context, nil); CGColorSpaceRelease(colorSpace); if (context == NULL) { return nil; } - CGRect imageRect = CGRectMake(0, 0, width, height); + CGRect imageRect = CGRectMakeWithSize(size); CGContextDrawImage(context, imageRect, self.CGImage); UIImage *grayImage = nil; @@ -70,7 +167,7 @@ - (UIImage *)qmui_grayImage { if (self.qmui_opaque) { grayImage = [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation]; } else { - CGContextRef alphaContext = CGBitmapContextCreate(NULL, width, height, 8, 0, nil, kCGImageAlphaOnly); + CGContextRef alphaContext = CGBitmapContextCreate(NULL, size.width, size.height, 8, 0, nil, kCGImageAlphaOnly); CGContextDrawImage(alphaContext, imageRect, self.CGImage); CGImageRef mask = CGBitmapContextCreateImage(alphaContext); CGImageRef maskedGrayImageRef = CGImageCreateWithMask(imageRef, mask); @@ -80,10 +177,9 @@ - (UIImage *)qmui_grayImage { CGContextRelease(alphaContext); // 用 CGBitmapContextCreateImage 方式创建出来的图片,CGImageAlphaInfo 总是为 CGImageAlphaInfoNone,导致 qmui_opaque 与原图不一致,所以这里再做多一步 - UIGraphicsBeginImageContextWithOptions(grayImage.size, NO, grayImage.scale); - [grayImage drawInRect:imageRect]; - grayImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + grayImage = [UIImage qmui_imageWithSize:grayImage.size opaque:NO scale:grayImage.scale actions:^(CGContextRef contextRef) { + [grayImage drawInRect:CGRectMakeWithSize(grayImage.size)]; + }]; } CGContextRelease(context); @@ -92,39 +188,35 @@ - (UIImage *)qmui_grayImage { } - (UIImage *)qmui_imageWithAlpha:(CGFloat)alpha { - UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - CGRect drawingRect = CGRectMake(0, 0, self.size.width, self.size.height); - [self drawInRect:drawingRect blendMode:kCGBlendModeNormal alpha:alpha]; - UIImage *imageOut = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return imageOut; -} - -- (BOOL)qmui_opaque { - CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(self.CGImage); - BOOL opaque = alphaInfo == kCGImageAlphaNoneSkipLast - || alphaInfo == kCGImageAlphaNoneSkipFirst - || alphaInfo == kCGImageAlphaNone; - return opaque; + return [UIImage qmui_imageWithSize:self.size opaque:NO scale:self.scale actions:^(CGContextRef contextRef) { + [self drawInRect:CGRectMakeWithSize(self.size) blendMode:kCGBlendModeNormal alpha:alpha]; + }]; } - (UIImage *)qmui_imageWithTintColor:(UIColor *)tintColor { - UIImage *imageIn = self; - CGRect rect = CGRectMake(0, 0, imageIn.size.width, imageIn.size.height); - UIGraphicsBeginImageContextWithOptions(imageIn.size, self.qmui_opaque, imageIn.scale); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - CGContextTranslateCTM(context, 0, imageIn.size.height); - CGContextScaleCTM(context, 1.0, -1.0); - CGContextSetBlendMode(context, kCGBlendModeNormal); - CGContextClipToMask(context, rect, imageIn.CGImage); - CGContextSetFillColorWithColor(context, tintColor.CGColor); - CGContextFillRect(context, rect); - UIImage *imageOut = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return imageOut; + // 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); + 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 { @@ -141,23 +233,17 @@ - (UIImage *)qmui_imageWithBlendColor:(UIColor *)blendColor { } - (UIImage *)qmui_imageWithImageAbove:(UIImage *)image atPoint:(CGPoint)point { - UIImage *imageIn = self; - UIImage *imageOut = nil; - UIGraphicsBeginImageContextWithOptions(imageIn.size, self.qmui_opaque, imageIn.scale); - [imageIn drawInRect:CGRectMakeWithSize(imageIn.size)]; - [image drawAtPoint:point]; - imageOut = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return imageOut; + return [UIImage qmui_imageWithSize:self.size opaque:self.qmui_opaque scale:self.scale actions:^(CGContextRef contextRef) { + [self drawInRect:CGRectMakeWithSize(self.size)]; + [image drawAtPoint:point]; + }]; } - (UIImage *)qmui_imageWithSpacingExtensionInsets:(UIEdgeInsets)extension { CGSize contextSize = CGSizeMake(self.size.width + UIEdgeInsetsGetHorizontalValue(extension), self.size.height + UIEdgeInsetsGetVerticalValue(extension)); - UIGraphicsBeginImageContextWithOptions(contextSize, self.qmui_opaque, self.scale); - [self drawAtPoint:CGPointMake(extension.left, extension.top)]; - UIImage *finalImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return finalImage; + return [UIImage qmui_imageWithSize:contextSize opaque:self.qmui_opaque scale:self.scale actions:^(CGContextRef contextRef) { + [self drawAtPoint:CGPointMake(extension.left, extension.top)]; + }]; } - (UIImage *)qmui_imageWithClippedRect:(CGRect)rect { @@ -175,43 +261,75 @@ - (UIImage *)qmui_imageWithClippedRect:(CGRect)rect { return imageOut; } -- (UIImage *)qmui_imageWithScaleToSize:(CGSize)size { - return [self qmui_imageWithScaleToSize:size contentMode:UIViewContentModeScaleAspectFit]; +- (UIImage *)qmui_imageWithClippedCornerRadius:(CGFloat)cornerRadius { + return [self qmui_imageWithClippedCornerRadius:cornerRadius scale:self.scale]; } -- (UIImage *)qmui_imageWithScaleToSize:(CGSize)size contentMode:(UIViewContentMode)contentMode { - return [self qmui_imageWithScaleToSize:size contentMode:contentMode scale:self.scale]; +- (UIImage *)qmui_imageWithClippedCornerRadius:(CGFloat)cornerRadius scale:(CGFloat)scale { + if (cornerRadius <= 0) { + return self; + } + return [UIImage qmui_imageWithSize:self.size opaque:NO scale:scale actions:^(CGContextRef contextRef) { + [[UIBezierPath bezierPathWithRoundedRect:CGRectMakeWithSize(self.size) cornerRadius:cornerRadius] addClip]; + [self drawInRect:CGRectMakeWithSize(self.size)]; + }]; +} + +- (UIImage *)qmui_imageResizedInLimitedSize:(CGSize)size { + return [self qmui_imageResizedInLimitedSize:size resizingMode:QMUIImageResizingModeScaleAspectFit]; } -- (UIImage *)qmui_imageWithScaleToSize:(CGSize)size contentMode:(UIViewContentMode)contentMode scale:(CGFloat)scale { +- (UIImage *)qmui_imageResizedInLimitedSize:(CGSize)size resizingMode:(QMUIImageResizingMode)resizingMode { + return [self qmui_imageResizedInLimitedSize:size resizingMode:resizingMode scale:self.scale]; +} + +- (UIImage *)qmui_imageResizedInLimitedSize:(CGSize)size resizingMode:(QMUIImageResizingMode)resizingMode scale:(CGFloat)scale { size = CGSizeFlatSpecificScale(size, scale); - CGContextInspectSize(size); CGSize imageSize = self.size; - CGRect drawingRect = CGRectZero; + CGRect drawingRect = CGRectZero;// 图片绘制的 rect + CGSize contextSize = CGSizeZero;// 画布的大小 - if (contentMode == UIViewContentModeScaleToFill) { - drawingRect = CGRectMakeWithSize(size); - } else { + if (CGSizeEqualToSize(size, imageSize) && scale == self.scale) { + return self; + } + + if (resizingMode >= QMUIImageResizingModeScaleAspectFit && resizingMode <= QMUIImageResizingModeScaleAspectFillBottom) { CGFloat horizontalRatio = size.width / imageSize.width; CGFloat verticalRatio = size.height / imageSize.height; CGFloat ratio = 0; - if (contentMode == UIViewContentModeScaleAspectFill) { - ratio = fmaxf(horizontalRatio, verticalRatio); + if (resizingMode >= QMUIImageResizingModeScaleAspectFill && resizingMode < (QMUIImageResizingModeScaleAspectFill + 10)) { + ratio = MAX(horizontalRatio, verticalRatio); + } else { + // 默认按 QMUIImageResizingModeScaleAspectFit + ratio = MIN(horizontalRatio, verticalRatio); + } + CGSize resizedSize = CGSizeMake(flatSpecificScale(imageSize.width * ratio, scale), flatSpecificScale(imageSize.height * ratio, scale)); + contextSize = CGSizeMake(MIN(size.width, resizedSize.width), MIN(size.height, resizedSize.height)); + drawingRect.origin.x = CGFloatGetCenter(contextSize.width, resizedSize.width); + + CGFloat originY = 0; + if (resizingMode % 10 == 1) { + // toTop + originY = 0; + } else if (resizingMode % 10 == 2) { + // toBottom + originY = contextSize.height - resizedSize.height; } else { - // 默认按 UIViewContentModeScaleAspectFit - ratio = fminf(horizontalRatio, verticalRatio); + // default is Center + originY = CGFloatGetCenter(contextSize.height, resizedSize.height); } - drawingRect.size.width = flatSpecificScale(imageSize.width * ratio, scale); - drawingRect.size.height = flatSpecificScale(imageSize.height * ratio, scale); + drawingRect.origin.y = originY; + + drawingRect.size = resizedSize; + } else { + // 默认按照 QMUIImageResizingModeScaleToFill + drawingRect = CGRectMakeWithSize(size); + contextSize = size; } - UIGraphicsBeginImageContextWithOptions(drawingRect.size, self.qmui_opaque, scale); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - [self drawInRect:drawingRect]; - UIImage *imageOut = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return imageOut; + return [UIImage qmui_imageWithSize:contextSize opaque:self.qmui_opaque scale:scale actions:^(CGContextRef contextRef) { + [self drawInRect:drawingRect]; + }]; } - (UIImage *)qmui_imageWithOrientation:(UIImageOrientation)orientation { @@ -226,50 +344,44 @@ - (UIImage *)qmui_imageWithOrientation:(UIImageOrientation)orientation { contextSize = CGSizeFlatSpecificScale(contextSize, self.scale); - UIGraphicsBeginImageContextWithOptions(contextSize, NO, self.scale); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - - // 画布的原点在左上角,旋转后可能图片就飞到画布外了,所以旋转前先把图片摆到特定位置再旋转,图片刚好就落在画布里 - switch (orientation) { - case UIImageOrientationUp: - // 上 - break; - case UIImageOrientationDown: - // 下 - CGContextTranslateCTM(context, contextSize.width, contextSize.height); - CGContextRotateCTM(context, AngleWithDegrees(180)); - break; - case UIImageOrientationLeft: - // 左 - CGContextTranslateCTM(context, 0, contextSize.height); - CGContextRotateCTM(context, AngleWithDegrees(-90)); - break; - case UIImageOrientationRight: - // 右 - CGContextTranslateCTM(context, contextSize.width, 0); - CGContextRotateCTM(context, AngleWithDegrees(90)); - break; - case UIImageOrientationUpMirrored: - case UIImageOrientationDownMirrored: - // 向上、向下翻转是一样的 - CGContextTranslateCTM(context, 0, contextSize.height); - CGContextScaleCTM(context, 1, -1); - break; - case UIImageOrientationLeftMirrored: - case UIImageOrientationRightMirrored: - // 向左、向右翻转是一样的 - CGContextTranslateCTM(context, contextSize.width, 0); - CGContextScaleCTM(context, -1, 1); - break; - } - - // 在前面画布的旋转、移动的结果上绘制自身即可,这里不用考虑旋转带来的宽高置换的问题 - [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)]; - - UIImage *imageOut = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return imageOut; + return [UIImage qmui_imageWithSize:contextSize opaque:NO scale:self.scale actions:^(CGContextRef contextRef) { + // 画布的原点在左上角,旋转后可能图片就飞到画布外了,所以旋转前先把图片摆到特定位置再旋转,图片刚好就落在画布里 + switch (orientation) { + case UIImageOrientationUp: + // 上 + break; + case UIImageOrientationDown: + // 下 + CGContextTranslateCTM(contextRef, contextSize.width, contextSize.height); + CGContextRotateCTM(contextRef, AngleWithDegrees(180)); + break; + case UIImageOrientationLeft: + // 左 + CGContextTranslateCTM(contextRef, 0, contextSize.height); + CGContextRotateCTM(contextRef, AngleWithDegrees(-90)); + break; + case UIImageOrientationRight: + // 右 + CGContextTranslateCTM(contextRef, contextSize.width, 0); + CGContextRotateCTM(contextRef, AngleWithDegrees(90)); + break; + case UIImageOrientationUpMirrored: + case UIImageOrientationDownMirrored: + // 向上、向下翻转是一样的 + CGContextTranslateCTM(contextRef, 0, contextSize.height); + CGContextScaleCTM(contextRef, 1, -1); + break; + case UIImageOrientationLeftMirrored: + case UIImageOrientationRightMirrored: + // 向左、向右翻转是一样的 + CGContextTranslateCTM(contextRef, contextSize.width, 0); + CGContextScaleCTM(contextRef, -1, 1); + break; + } + + // 在前面画布的旋转、移动的结果上绘制自身即可,这里不用考虑旋转带来的宽高置换的问题 + [self drawInRect:CGRectMakeWithSize(self.size)]; + }]; } - (UIImage *)qmui_imageWithBorderColor:(UIColor *)borderColor path:(UIBezierPath *)path { @@ -277,18 +389,11 @@ - (UIImage *)qmui_imageWithBorderColor:(UIColor *)borderColor path:(UIBezierPath return self; } - UIImage *oldImage = self; - UIImage *resultImage; - CGRect rect = CGRectMake(0, 0, oldImage.size.width, oldImage.size.height); - UIGraphicsBeginImageContextWithOptions(oldImage.size, self.qmui_opaque, oldImage.scale); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - [oldImage drawInRect:rect]; - CGContextSetStrokeColorWithColor(context, borderColor.CGColor); - [path stroke]; - resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return resultImage; + return [UIImage qmui_imageWithSize:self.size opaque:self.qmui_opaque scale:self.scale actions:^(CGContextRef contextRef) { + [self drawInRect:CGRectMakeWithSize(self.size)]; + CGContextSetStrokeColorWithColor(contextRef, borderColor.CGColor); + [path stroke]; + }]; } - (UIImage *)qmui_imageWithBorderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth cornerRadius:(CGFloat)cornerRadius { @@ -370,19 +475,78 @@ - (UIImage *)qmui_imageWithMaskImage:(UIImage *)maskImage usingMaskImageMode:(BO return returnImage; } ++ (UIImage *)qmui_animatedImageWithData:(NSData *)data { + return [self qmui_animatedImageWithData:data scale:1]; +} + ++ (UIImage *)qmui_animatedImageWithData:(NSData *)data scale:(CGFloat)scale { + // http://www.jianshu.com/p/767af9c690a3 + // https://github.com/rs/SDWebImage + if (!data) { + return nil; + } + 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 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 orientation:UIImageOrientationUp]; + [images addObject:frameImage]; + CGImageRelease(image); + } + if (!duration) { + duration = (1.0f / 10.0f) * count; + } + animatedImage = [UIImage animatedImageWithImages:images duration:duration]; + } + CFRelease(source); + return animatedImage; +} + ++ (float)qmui_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source { + float frameDuration = 0.1f; + CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil); + NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties; + NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary]; + NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime]; + if (delayTimeUnclampedProp) { + frameDuration = [delayTimeUnclampedProp floatValue]; + } else { + NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime]; + if (delayTimeProp) { + frameDuration = [delayTimeProp floatValue]; + } + } + CFRelease(cfFrameProperties); + return frameDuration; +} + ++ (UIImage *)qmui_animatedImageNamed:(NSString *)name { + return [UIImage qmui_animatedImageNamed:name scale:1]; +} + ++ (UIImage *)qmui_animatedImageNamed:(NSString *)name scale:(CGFloat)scale { + NSString *type = name.pathExtension.lowercaseString; + type = type.length > 0 ? type : @"gif"; + NSString *path = [[NSBundle mainBundle] pathForResource:name.stringByDeletingPathExtension ofType:type]; + NSData *data = [NSData dataWithContentsOfFile:path]; + return [UIImage qmui_animatedImageWithData:data scale:scale]; +} + + (UIImage *)qmui_imageWithStrokeColor:(UIColor *)strokeColor size:(CGSize)size path:(UIBezierPath *)path addClip:(BOOL)addClip { size = CGSizeFlatted(size); - CGContextInspectSize(size); - UIImage *resultImage = nil; - UIGraphicsBeginImageContextWithOptions(size, NO, 0); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - CGContextSetStrokeColorWithColor(context, strokeColor.CGColor); - if (addClip) [path addClip]; - [path stroke]; - resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return resultImage; + return [UIImage qmui_imageWithSize:size opaque:NO scale:0 actions:^(CGContextRef contextRef) { + CGContextSetStrokeColorWithColor(contextRef, strokeColor.CGColor); + if (addClip) [path addClip]; + [path stroke]; + }]; } + (UIImage *)qmui_imageWithStrokeColor:(UIColor *)strokeColor size:(CGSize)size lineWidth:(CGFloat)lineWidth cornerRadius:(CGFloat)cornerRadius { @@ -390,7 +554,7 @@ + (UIImage *)qmui_imageWithStrokeColor:(UIColor *)strokeColor size:(CGSize)size // 往里面缩一半的lineWidth,应为stroke绘制线的时候是往两边绘制的 // 如果cornerRadius为0的时候使用bezierPathWithRoundedRect:cornerRadius:会有问题,左上角老是会多出一点,所以区分开 UIBezierPath *path; - CGRect rect = CGRectInset(CGRectMake(0, 0, size.width, size.height), lineWidth / 2, lineWidth / 2); + CGRect rect = CGRectInset(CGRectMakeWithSize(size), lineWidth / 2, lineWidth / 2); if (cornerRadius > 0) { path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius]; } else { @@ -437,147 +601,193 @@ + (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadius size = CGSizeFlatted(size); CGContextInspectSize(size); - UIImage *resultImage = nil; color = color ? color : UIColorClear; - BOOL opaque = (cornerRadius == 0.0 && [color qmui_alpha] == 1.0); - UIGraphicsBeginImageContextWithOptions(size, opaque, 0); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextSetFillColorWithColor(context, color.CGColor); - - if (cornerRadius > 0) { - UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMakeWithSize(size) cornerRadius:cornerRadius]; - [path addClip]; - [path fill]; - } else { - CGContextFillRect(context, CGRectMakeWithSize(size)); + UIImage *result = [UIImage qmui_imageWithSize:size opaque:opaque scale:0 actions:^(CGContextRef contextRef) { + CGContextSetFillColorWithColor(contextRef, color.CGColor); + + if (cornerRadius > 0) { + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMakeWithSize(size) cornerRadius:cornerRadius]; + [path addClip]; + [path fill]; + } else { + 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 切换时无法刷新。"); } - - resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return resultImage; + return result; } + (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadiusArray:(NSArray *)cornerRadius { size = CGSizeFlatted(size); CGContextInspectSize(size); - - UIGraphicsBeginImageContextWithOptions(size, NO, 0); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - - color = color ? color : UIColorWhite; - CGContextSetFillColorWithColor(context, color.CGColor); - - UIBezierPath *path = [UIBezierPath qmui_bezierPathWithRoundedRect:CGRectMakeWithSize(size) cornerRadiusArray:cornerRadius lineWidth:0]; - [path addClip]; - [path fill]; - - UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return resultImage; + 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); + + UIBezierPath *path = [UIBezierPath qmui_bezierPathWithRoundedRect:CGRectMakeWithSize(size) cornerRadiusArray:cornerRadius lineWidth:0]; + [path addClip]; + [path fill]; + }]; +} + ++ (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); - UIImage *resultImage = nil; tintColor = tintColor ? : [UIColor colorWithRed:1 green:1 blue:1 alpha:1]; - - UIGraphicsBeginImageContextWithOptions(size, NO, 0); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - UIBezierPath *path = nil; - BOOL drawByStroke = NO; - CGFloat drawOffset = lineWidth / 2; - switch (shape) { - case QMUIImageShapeOval: { - path = [UIBezierPath bezierPathWithOvalInRect:CGRectMakeWithSize(size)]; + return [UIImage qmui_imageWithSize:size opaque:NO scale:0 actions:^(CGContextRef contextRef) { + UIBezierPath *path = nil; + BOOL drawByStroke = NO; + CGFloat drawOffset = lineWidth / 2; + switch (shape) { + case QMUIImageShapeOval: { + path = [UIBezierPath bezierPathWithOvalInRect:CGRectMakeWithSize(size)]; + } + break; + case QMUIImageShapeTriangle: { + path = [UIBezierPath bezierPath]; + [path moveToPoint:CGPointMake(0, size.height)]; + [path addLineToPoint:CGPointMake(size.width / 2, 0)]; + [path addLineToPoint:CGPointMake(size.width, size.height)]; + [path closePath]; + } + break; + case QMUIImageShapeNavBack: { + drawByStroke = YES; + path = [UIBezierPath bezierPath]; + path.lineWidth = lineWidth; + [path moveToPoint:CGPointMake(size.width - drawOffset, drawOffset)]; + [path addLineToPoint:CGPointMake(0 + drawOffset, size.height / 2.0)]; + [path addLineToPoint:CGPointMake(size.width - drawOffset, size.height - drawOffset)]; + } + break; + case QMUIImageShapeDisclosureIndicator: { + drawByStroke = YES; + path = [UIBezierPath bezierPath]; + path.lineWidth = lineWidth; + [path moveToPoint:CGPointMake(drawOffset, drawOffset)]; + [path addLineToPoint:CGPointMake(size.width - drawOffset, size.height / 2)]; + [path addLineToPoint:CGPointMake(drawOffset, size.height - drawOffset)]; + } + break; + case QMUIImageShapeCheckmark: { + CGFloat lineAngle = M_PI_4; + path = [UIBezierPath bezierPath]; + [path moveToPoint:CGPointMake(0, size.height / 2)]; + [path addLineToPoint:CGPointMake(size.width / 3, size.height)]; + [path addLineToPoint:CGPointMake(size.width, lineWidth * sin(lineAngle))]; + [path addLineToPoint:CGPointMake(size.width - lineWidth * cos(lineAngle), 0)]; + [path addLineToPoint:CGPointMake(size.width / 3, size.height - lineWidth / sin(lineAngle))]; + [path addLineToPoint:CGPointMake(lineWidth * sin(lineAngle), size.height / 2 - lineWidth * sin(lineAngle))]; + [path closePath]; + } + break; + case QMUIImageShapeDetailButtonImage: { + drawByStroke = YES; + path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(CGRectMakeWithSize(size), drawOffset, drawOffset)]; + path.lineWidth = lineWidth; + } + break; + case QMUIImageShapeNavClose: { + drawByStroke = YES; + path = [UIBezierPath bezierPath]; + [path moveToPoint:CGPointMake(0, 0)]; + [path addLineToPoint:CGPointMake(size.width, size.height)]; + [path closePath]; + [path moveToPoint:CGPointMake(size.width, 0)]; + [path addLineToPoint:CGPointMake(0, size.height)]; + [path closePath]; + path.lineWidth = lineWidth; + path.lineCapStyle = kCGLineCapRound; + } + break; + default: + break; } - break; - case QMUIImageShapeTriangle: { - path = [UIBezierPath bezierPath]; - [path moveToPoint:CGPointMake(0, size.height)]; - [path addLineToPoint:CGPointMake(size.width / 2, 0)]; - [path addLineToPoint:CGPointMake(size.width, size.height)]; - [path closePath]; - } - break; - case QMUIImageShapeNavBack: { - drawByStroke = YES; - path = [UIBezierPath bezierPath]; - path.lineWidth = lineWidth; - [path moveToPoint:CGPointMake(size.width - drawOffset, drawOffset)]; - [path addLineToPoint:CGPointMake(0 + drawOffset, size.height / 2.0)]; - [path addLineToPoint:CGPointMake(size.width - drawOffset, size.height - drawOffset)]; - } - break; - case QMUIImageShapeDisclosureIndicator: { - drawByStroke = YES; - path = [UIBezierPath bezierPath]; - path.lineWidth = lineWidth; - [path moveToPoint:CGPointMake(drawOffset, drawOffset)]; - [path addLineToPoint:CGPointMake(size.width - drawOffset, size.height / 2)]; - [path addLineToPoint:CGPointMake(drawOffset, size.height - drawOffset)]; - } - break; - case QMUIImageShapeCheckmark: { - CGFloat lineAngle = M_PI_4; - path = [UIBezierPath bezierPath]; - [path moveToPoint:CGPointMake(0, size.height / 2)]; - [path addLineToPoint:CGPointMake(size.width / 3, size.height)]; - [path addLineToPoint:CGPointMake(size.width, lineWidth * sin(lineAngle))]; - [path addLineToPoint:CGPointMake(size.width - lineWidth * cos(lineAngle), 0)]; - [path addLineToPoint:CGPointMake(size.width / 3, size.height - lineWidth / sin(lineAngle))]; - [path addLineToPoint:CGPointMake(lineWidth * sin(lineAngle), size.height / 2 - lineWidth * sin(lineAngle))]; - [path closePath]; - } - break; - case QMUIImageShapeDetailButtonImage: { - drawByStroke = YES; - path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(CGRectMakeWithSize(size), drawOffset, drawOffset)]; - path.lineWidth = lineWidth; + + if (drawByStroke) { + CGContextSetStrokeColorWithColor(contextRef, tintColor.CGColor); + [path stroke]; + } else { + CGContextSetFillColorWithColor(contextRef, tintColor.CGColor); + [path fill]; } - break; - case QMUIImageShapeNavClose: { - drawByStroke = YES; - path = [UIBezierPath bezierPath]; - [path moveToPoint:CGPointMake(0, 0)]; - [path addLineToPoint:CGPointMake(size.width, size.height)]; - [path closePath]; - [path moveToPoint:CGPointMake(size.width, 0)]; - [path addLineToPoint:CGPointMake(0, size.height)]; - [path closePath]; - path.lineWidth = lineWidth; - path.lineCapStyle = kCGLineCapRound; + + if (shape == QMUIImageShapeDetailButtonImage) { + CGFloat fontPointSize = flat(size.height * 0.8); + UIFont *font = [UIFont fontWithName:@"Georgia" size:fontPointSize]; + NSAttributedString *string = [[NSAttributedString alloc] initWithString:@"i" attributes:@{NSFontAttributeName: font, NSForegroundColorAttributeName: tintColor}]; + CGSize stringSize = [string boundingRectWithSize:size options:NSStringDrawingUsesFontLeading context:nil].size; + [string drawAtPoint:CGPointMake(CGFloatGetCenter(size.width, stringSize.width), CGFloatGetCenter(size.height, stringSize.height))]; } - break; - default: - break; - } - - if (drawByStroke) { - CGContextSetStrokeColorWithColor(context, tintColor.CGColor); - [path stroke]; - } else { - CGContextSetFillColorWithColor(context, tintColor.CGColor); - [path fill]; - } - - if (shape == QMUIImageShapeDetailButtonImage) { - CGFloat fontPointSize = flat(size.height * 0.8); - UIFont *font = [UIFont fontWithName:@"Georgia" size:fontPointSize]; - NSAttributedString *string = [[NSAttributedString alloc] initWithString:@"i" attributes:@{NSFontAttributeName: font, NSForegroundColorAttributeName: tintColor}]; - CGSize stringSize = [string boundingRectWithSize:size options:NSStringDrawingUsesFontLeading context:nil].size; - [string drawAtPoint:CGPointMake(CGFloatGetCenter(size.width, stringSize.width), CGFloatGetCenter(size.height, stringSize.height))]; - } - - resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return resultImage; + }]; } + (UIImage *)qmui_imageWithShape:(QMUIImageShape)shape size:(CGSize)size tintColor:(UIColor *)tintColor { @@ -604,38 +814,19 @@ + (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:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin context:nil].size; - stringSize = CGSizeCeil(stringSize); - UIGraphicsBeginImageContextWithOptions(stringSize, NO, 0); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - [attributedString drawInRect:CGRectMake(0, 0, stringSize.width, stringSize.height)]; - UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return resultImage; -} - + (UIImage *)qmui_imageWithView:(UIView *)view { CGContextInspectSize(view.bounds.size); - UIImage *resultImage = nil; - UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextInspectContext(context); - [view.layer renderInContext:context]; - resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return resultImage; + return [UIImage qmui_imageWithSize:view.bounds.size opaque:NO scale:0 actions:^(CGContextRef contextRef) { + [view.layer renderInContext:contextRef]; + }]; } + (UIImage *)qmui_imageWithView:(UIView *)view afterScreenUpdates:(BOOL)afterUpdates { // iOS 7 截图新方式,性能好会好一点,不过不一定适用,因为这个方法的使用条件是:界面要已经render完,否则截到得图将会是empty。 - UIImage *resultImage = nil; - UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0); - [view drawViewHierarchyInRect:CGRectMakeWithSize(view.bounds.size) afterScreenUpdates:afterUpdates]; - resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return resultImage; + CGContextInspectSize(view.bounds.size); + return [UIImage qmui_imageWithSize:view.bounds.size opaque:NO scale:0 actions:^(CGContextRef contextRef) { + [view drawViewHierarchyInRect:CGRectMakeWithSize(view.bounds.size) afterScreenUpdates:afterUpdates]; + }]; } @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.h index 08554ea3..32343901 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.h @@ -1,9 +1,16 @@ +/** + * 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. + */ + // // UIImageView+QMUI.h // qmui // -// Created by MoLice on 16/8/9. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/8/9. // #import @@ -11,6 +18,16 @@ @interface UIImageView (QMUI) +/** + 暂停/恢复当前 UIImageView 上的 animation images(包括通过 animationImages 设置的图片数组,以及通过 [UIImage animatedImage] 系列方法创建的动图)的播放,默认为 NO。 + */ +@property(nonatomic, assign) BOOL qmui_pause; + +/** + 是否要用 QMUI 提供的高性能方式去渲染由 [UIImage animatedImage] 创建的 UIImage,(系统原生的方式在 UIImageView 被放在 UIScrollView 内时会卡顿),默认为 NO。 + */ +@property(nonatomic, assign) BOOL qmui_smoothAnimation; + /** * 把 UIImageView 的宽高调整为能保持 image 宽高比例不变的同时又不超过给定的 `limitSize` 大小的最大frame * diff --git a/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.m index a6b398ec..b79fe9af 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIImageView+QMUI.m @@ -1,16 +1,207 @@ +/** + * 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. + */ + // // UIImageView+QMUI.m // qmui // -// Created by MoLice on 16/8/9. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/8/9. // #import "UIImageView+QMUI.h" #import "QMUICore.h" +#import "CALayer+QMUI.h" +#import "UIView+QMUI.h" + +@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; +@end @implementation UIImageView (QMUI) +QMUISynthesizeIdStrongProperty(qimgv_animatedImageLayer, setQimgv_animatedImageLayer) +QMUISynthesizeIdStrongProperty(qimgv_displayLink, setQimgv_displayLink) +QMUISynthesizeIdStrongProperty(qimgv_animatedImage, setQimgv_animatedImage) +QMUISynthesizeNSIntegerProperty(qimgv_currentAnimatedImageIndex, setQimgv_currentAnimatedImageIndex) + +- (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) { + + // call super + void (^callSuperBlock)(UIImage *) = ^void(UIImage *aImage) { + void (*originSelectorIMP)(id, SEL, UIImage *); + originSelectorIMP = (void (*)(id, SEL, UIImage *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, aImage); + }; + + 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); + } + }; + }); + + OverrideImplementation([UIImageView class], @selector(image), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UIImageView *selfObject) { + if (selfObject.qimgv_animatedImage) { + return selfObject.qimgv_animatedImage; + } + + // call super + UIImage *(*originSelectorIMP)(id, SEL); + originSelectorIMP = (UIImage *(*)(id, SEL))originalIMPProvider(); + UIImage *result = originSelectorIMP(selfObject, originCMD); + + return result; + }; + }); + + ExtendImplementationOfVoidMethodWithoutArguments([UIImageView class], @selector(layoutSubviews), ^(UIImageView *selfObject) { + if (selfObject.qimgv_animatedImageLayer) { + selfObject.qimgv_animatedImageLayer.frame = selfObject.bounds; + } + }); + + ExtendImplementationOfVoidMethodWithoutArguments([UIImageView class], @selector(didMoveToWindow), ^(UIImageView *selfObject) { + [selfObject qimgv_updateAnimationStateAutomatically]; + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UIImageView class], @selector(setHidden:), BOOL, ^(UIImageView *selfObject, BOOL hidden) { + [selfObject qimgv_updateAnimationStateAutomatically]; + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UIImageView class], @selector(setAlpha:), CGFloat, ^(UIImageView *selfObject, CGFloat alpha) { + [selfObject qimgv_updateAnimationStateAutomatically]; + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UIImageView class], @selector(setFrame:), CGRect, ^(UIImageView *selfObject, CGRect frame) { + [selfObject qimgv_updateAnimationStateAutomatically]; + }); + + OverrideImplementation([UIImageView class], @selector(sizeThatFits:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGSize(UIImageView *selfObject, CGSize size) { + + if (selfObject.qimgv_animatedImage) { + return selfObject.qimgv_animatedImage.size; + } + + // call super + CGSize (*originSelectorIMP)(id, SEL, CGSize); + originSelectorIMP = (CGSize (*)(id, SEL, CGSize))originalIMPProvider(); + CGSize result = originSelectorIMP(selfObject, originCMD, size); + return result; + }; + }); + + 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; + self.qimgv_displayLink.preferredFramesPerSecond = preferredFramesPerSecond; + self.qimgv_currentAnimatedImageIndex = -1; + self.qimgv_animatedImageLayer.contents = (__bridge id)self.qimgv_animatedImage.images.firstObject.CGImage;// 对于那种一开始就 pause 的图,displayLayer: 不会被调用,所以看不到图,为了避免这种情况,手动先把第一帧显示出来 + } + + self.qimgv_displayLink.paused = self.qmui_pause; + + return YES; +} + +- (void)qimgv_stopAnimating { + if (self.qimgv_displayLink) { + [self.qimgv_displayLink invalidate]; + self.qimgv_displayLink = nil; + } + if (self.qimgv_animatedImageLayer) { + [self.qimgv_animatedImageLayer removeFromSuperlayer]; + self.qimgv_animatedImageLayer = nil; + } +} + +- (void)qimgv_updateAnimationStateAutomatically { + if (self.qimgv_animatedImage) { + if (![self qimgv_requestToStartAnimation]) { + [self qimgv_stopAnimating]; + } + } +} + +- (BOOL)qimgv_canStartAnimation { + return self.qmui_visible && !CGRectIsEmpty(self.frame); +} + +- (void)handleDisplayLink:(CADisplayLink *)displayLink { + self.qimgv_currentAnimatedImageIndex = self.qimgv_currentAnimatedImageIndex < self.qimgv_animatedImage.images.count - 1 ? (self.qimgv_currentAnimatedImageIndex + 1) : 0; + 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) { + self.image = self.image;// 交给 setImage 那边把动画清理掉 + } +} + +- (BOOL)qmui_smoothAnimation { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_smoothAnimation)) boolValue]; +} + +static char kAssociatedObjectKey_pause; +- (void)setQmui_pause:(BOOL)qmui_pause { + objc_setAssociatedObject(self, &kAssociatedObjectKey_pause, @(qmui_pause), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (self.animationImages || self.image.images) { + self.qimgv_animatedImageLayer.qmui_pause = qmui_pause; + } + if (self.qimgv_displayLink) { + self.qimgv_displayLink.paused = qmui_pause; + } +} + +- (BOOL)qmui_pause { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_pause)) boolValue]; +} + - (void)qmui_sizeToFitKeepingImageAspectRatioInSize:(CGSize)limitSize { if (!self.image) { return; @@ -24,7 +215,7 @@ - (void)qmui_sizeToFitKeepingImageAspectRatioInSize:(CGSize)limitSize { } CGFloat horizontalRatio = limitSize.width / currentSize.width; CGFloat verticalRatio = limitSize.height / currentSize.height; - CGFloat ratio = fminf(horizontalRatio, verticalRatio); + CGFloat ratio = fmin(horizontalRatio, verticalRatio); CGRect frame = self.frame; frame.size.width = flat(currentSize.width * ratio); frame.size.height = flat(currentSize.height * ratio); diff --git a/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.h new file mode 100644 index 00000000..241e720a --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.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. + */ + +// +// UIInterface+QMUI.h +// QMUIKit +// +// Created by QMUI Team on 2018/12/20. +// + +#import +#import "QMUIHelper.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QMUIHelper (QMUI_Interface) + +/** + * 内部使用,记录手动旋转方向前的设备方向,当值不为 UIDeviceOrientationUnknown 时表示设备方向有经过了手动调整。默认值为 UIDeviceOrientationUnknown。 + */ +@property(nonatomic, assign) UIDeviceOrientation lastOrientationChangedByHelper; + +/// 将一个 UIInterfaceOrientationMask 转换成对应的 UIDeviceOrientation ++ (UIDeviceOrientation)deviceOrientationWithInterfaceOrientationMask:(UIInterfaceOrientationMask)mask; + +/// 判断一个 UIInterfaceOrientationMask 是否包含某个给定的 UIDeviceOrientation 方向 ++ (BOOL)interfaceOrientationMask:(UIInterfaceOrientationMask)mask containsDeviceOrientation:(UIDeviceOrientation)deviceOrientation; + +/// 判断一个 UIInterfaceOrientationMask 是否包含某个给定的 UIInterfaceOrientation 方向 ++ (BOOL)interfaceOrientationMask:(UIInterfaceOrientationMask)mask containsInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation; + +/// 根据指定的旋转方向计算出对应的旋转角度 ++ (CGFloat)angleForTransformWithInterfaceOrientation:(UIInterfaceOrientation)orientation; + +/// 根据当前设备的旋转方向计算出对应的CGAffineTransform ++ (CGAffineTransform)transformForCurrentInterfaceOrientation; + +/// 根据指定的旋转方向计算出对应的CGAffineTransform ++ (CGAffineTransform)transformWithInterfaceOrientation:(UIInterfaceOrientation)orientation; + +/// 给 QMUIHelper instance 通知用 +- (void)handleDeviceOrientationNotification:(NSNotification *)notification; + +@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 new file mode 100644 index 00000000..18b01795 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIInterface+QMUI.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. + */ + +// +// UIInterface+QMUI.m +// QMUIKit +// +// Created by QMUI Team on 2018/12/20. +// + +#import "UIInterface+QMUI.h" +#import "QMUICore.h" + +@implementation QMUIHelper (QMUI_Interface) + +QMUISynthesizeNSIntegerProperty(lastOrientationChangedByHelper, setLastOrientationChangedByHelper) + +- (void)handleDeviceOrientationNotification:(NSNotification *)notification { + QMUILogInfo(@"Interface (QMUI)", @"device orientation did change to %@", @(((UIDevice *)([notification.object isKindOfClass:UIDevice.class] ? notification.object : UIDevice.currentDevice)).orientation)); + + // 如果是由 setValue:forKey: 方式修改方向而走到这个 notification 的话,理论上是不需要重置为 Unknown 的,但因为在 UIViewController (QMUI) 那边会再次记录旋转前的值,所以这里就算重置也无所谓 + [QMUIHelper sharedInstance].lastOrientationChangedByHelper = UIDeviceOrientationUnknown; +} + ++ (UIDeviceOrientation)deviceOrientationWithInterfaceOrientationMask:(UIInterfaceOrientationMask)mask { + 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; + } + if ((mask & UIInterfaceOrientationMaskLandscape) == UIInterfaceOrientationMaskLandscape) { + return [UIDevice currentDevice].orientation == UIDeviceOrientationLandscapeLeft ? UIDeviceOrientationLandscapeLeft : UIDeviceOrientationLandscapeRight; + } + if ((mask & UIInterfaceOrientationMaskLandscapeLeft) == UIInterfaceOrientationMaskLandscapeLeft) { + return UIDeviceOrientationLandscapeRight; + } + if ((mask & UIInterfaceOrientationMaskLandscapeRight) == UIInterfaceOrientationMaskLandscapeRight) { + return UIDeviceOrientationLandscapeLeft; + } + if ((mask & UIInterfaceOrientationMaskPortraitUpsideDown) == UIInterfaceOrientationMaskPortraitUpsideDown) { + return UIDeviceOrientationPortraitUpsideDown; + } + return [UIDevice currentDevice].orientation; +} + ++ (BOOL)interfaceOrientationMask:(UIInterfaceOrientationMask)mask containsDeviceOrientation:(UIDeviceOrientation)deviceOrientation { + if (deviceOrientation == UIDeviceOrientationUnknown) { + return YES;// YES 表示不用额外处理 + } + + if ((mask & UIInterfaceOrientationMaskAll) == UIInterfaceOrientationMaskAll) { + return YES; + } + if ((mask & UIInterfaceOrientationMaskAllButUpsideDown) == UIInterfaceOrientationMaskAllButUpsideDown) { + return UIInterfaceOrientationPortraitUpsideDown != deviceOrientation; + } + if ((mask & UIInterfaceOrientationMaskPortrait) == UIInterfaceOrientationMaskPortrait) { + return UIInterfaceOrientationPortrait == deviceOrientation; + } + if ((mask & UIInterfaceOrientationMaskLandscape) == UIInterfaceOrientationMaskLandscape) { + return UIInterfaceOrientationLandscapeLeft == deviceOrientation || UIInterfaceOrientationLandscapeRight == deviceOrientation; + } + if ((mask & UIInterfaceOrientationMaskLandscapeLeft) == UIInterfaceOrientationMaskLandscapeLeft) { + return UIInterfaceOrientationLandscapeLeft == deviceOrientation; + } + if ((mask & UIInterfaceOrientationMaskLandscapeRight) == UIInterfaceOrientationMaskLandscapeRight) { + return UIInterfaceOrientationLandscapeRight == deviceOrientation; + } + if ((mask & UIInterfaceOrientationMaskPortraitUpsideDown) == UIInterfaceOrientationMaskPortraitUpsideDown) { + return UIInterfaceOrientationPortraitUpsideDown == deviceOrientation; + } + + return YES; +} + ++ (BOOL)interfaceOrientationMask:(UIInterfaceOrientationMask)mask containsInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + return [self interfaceOrientationMask:mask containsDeviceOrientation:(UIDeviceOrientation)interfaceOrientation]; +} + ++ (CGFloat)angleForTransformWithInterfaceOrientation:(UIInterfaceOrientation)orientation { + CGFloat angle; + switch (orientation) + { + case UIInterfaceOrientationPortraitUpsideDown: + angle = M_PI; + break; + case UIInterfaceOrientationLandscapeLeft: + angle = -M_PI_2; + break; + case UIInterfaceOrientationLandscapeRight: + angle = M_PI_2; + break; + default: + angle = 0.0; + break; + } + return angle; +} + ++ (CGAffineTransform)transformForCurrentInterfaceOrientation { + return [QMUIHelper transformWithInterfaceOrientation:UIApplication.sharedApplication.statusBarOrientation]; +} + ++ (CGAffineTransform)transformWithInterfaceOrientation:(UIInterfaceOrientation)orientation { + CGFloat angle = [QMUIHelper angleForTransformWithInterfaceOrientation:orientation]; + 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 e376efe4..17995ffc 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.h @@ -1,16 +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. + */ + // // UILabel+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import +NS_ASSUME_NONNULL_BEGIN + +extern const CGFloat QMUILineHeightIdentity; + @interface UILabel (QMUI) -- (instancetype)initWithFont:(UIFont *)font textColor:(UIColor *)textColor; +- (instancetype)qmui_initWithFont:(nullable UIFont *)font textColor:(nullable UIColor *)textColor; /** * @brief 在需要特殊样式时,可通过此属性直接给整个 label 添加 NSAttributeName 系列样式,然后 setText 即可,无需使用繁琐的 attributedText @@ -28,19 +39,38 @@ * 唯一例外的极端情况是:先用方法2将文字设成红色,再用方法1将文字设成蓝色,最后再 setText,这时虽然代码执行顺序靠后的是方法1,但最终生效的会是方法2,为了避免这种极端情况的困扰,建议不要同时使用方法1和方法2去设置同一种样式。 * */ -@property(nonatomic, copy) NSDictionary *qmui_textAttributes; +@property(nullable, nonatomic, copy) NSDictionary *qmui_textAttributes; /** - * 设置当前整段文字的行高 + * Setter 设置当前整段文字的行高 * @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: * + * ----------------------------------- + * + * Getter 获取整段文字的行高 + * @note 如果通过 setQmui_lineHeight 设置行高,会优先返回该值。 + * @note 如果通过 NSParagraphStyleAttributeName 设置了行高,同时 range 是整段文字,则会返回 paraStyle.maximumLineHeight。 + * @note 如果通过 setText 设置文本,会返回 font.lineHeight。 + * @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; -- (void)setQmui_lineHeight:(CGFloat)qmui_lineHeight; +/** + 获取当前 font.xHeight 的中心点在 label.bounds.size.height 里的y值(代表x这种矮的字符的中心点位置),从而令业务在试图将文本垂直居中时可以基于该属性的返回值去计算。 + @warning 仅对单行文本有意义,对 label 的高度没有要求,但 label 不应该调整过 baselineOffset + */ +@property(nonatomic, assign, readonly) CGFloat qmui_centerOfXHeight; /** * 将目标UILabel的样式属性设置到当前UILabel上 @@ -63,3 +93,20 @@ - (void)qmui_avoidBlendedLayersIfShowingChineseWithBackgroundColor:(UIColor *)color; @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 c6d20a89..8cbd04b9 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UILabel+QMUI.m @@ -1,43 +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. + */ + // // UILabel+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import "UILabel+QMUI.h" #import "QMUICore.h" -#import "QMUILabel.h" #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, ^{ - ReplaceMethod([self class], @selector(setText:), @selector(qmui_setText:)); - ReplaceMethod([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.qmui_lineHeight <= 0) { - [self qmui_setText:text]; + if (!self.qmui_textAttributes.count && ![self _hasSetQmuiLineHeight]) { + [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 { - if (!text) { - [self qmui_setAttributedText:text]; +- (void)qmuilb_setAttributedText:(NSAttributedString *)text { + if (!text || (!self.qmui_textAttributes.count && ![self _hasSetQmuiLineHeight])) { + [self qmuilb_setAttributedText:text]; return; } NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text.string attributes:self.qmui_textAttributes]; @@ -45,12 +80,12 @@ - (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; // 在现有样式基础上增加 qmui_textAttributes 样式。换句话说,如果这个方法里有样式冲突,则以 qmui_textAttributes 为准 -- (void)setQmui_textAttributes:(NSDictionary *)qmui_textAttributes { +- (void)setQmui_textAttributes:(NSDictionary *)qmui_textAttributes { NSDictionary *prevTextAttributes = self.qmui_textAttributes; if ([prevTextAttributes isEqualToDictionary:qmui_textAttributes]) { return; @@ -68,16 +103,16 @@ - (void)setQmui_textAttributes:(NSDictionary *)qmui_textAttribut if (prevTextAttributes) { // 找出现在 attributedText 中哪些 attrs 是通过上次的 qmui_textAttributes 设置的 NSMutableArray *willRemovedAttributes = [NSMutableArray array]; - [string enumerateAttributesInRange:NSMakeRange(0, string.length) options:0 usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { + [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 设置的 if (!NSEqualRanges(range, fullRange)) { return; } - [attrs enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull attr, id _Nonnull value, BOOL * _Nonnull stop) { + [attrs enumerateKeysAndObjectsUsingBlock:^(NSAttributedStringKey _Nonnull attr, id _Nonnull value, BOOL * _Nonnull stop) { if (prevTextAttributes[attr] == value) { [willRemovedAttributes addObject:attr]; } @@ -93,7 +128,7 @@ - (void)setQmui_textAttributes:(NSDictionary *)qmui_textAttribut [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 { @@ -102,7 +137,7 @@ - (NSDictionary *)qmui_textAttributes { // 去除最后一个字的 kern 效果,并且在有必要的情况下应用 qmui_setLineHeight: 设置的行高 - (NSAttributedString *)attributedStringWithKernAndLineHeightAdjusted:(NSAttributedString *)string { - if (!string || !string.length) { + if (!string.length) { return string; } NSMutableAttributedString *attributedString = nil; @@ -119,10 +154,7 @@ - (NSAttributedString *)attributedStringWithKernAndLineHeightAdjusted:(NSAttribu } // 判断是否应该应用上通过 qmui_setLineHeight: 设置的行高 - __block BOOL shouldAdjustLineHeight = YES; - if (self.qmui_lineHeight <= 0) { - shouldAdjustLineHeight = NO; - } + __block BOOL shouldAdjustLineHeight = [self _hasSetQmuiLineHeight]; [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, attributedString.length) options:0 usingBlock:^(NSParagraphStyle *style, NSRange range, BOOL * _Nonnull stop) { // 如果用户已经通过传入 NSParagraphStyle 对文字整个 range 设置了行高,则这里不应该再次调整行高 if (NSEqualRanges(range, NSMakeRange(0, attributedString.length))) { @@ -135,28 +167,106 @@ - (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_COPY_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 - [self setAttributedText:self.attributedText]; + // 注意:这里需要刷新一下 qmui_textAttributes 对 text 的样式,否则刚进行设置的 lineHeight 就会无法设置。 + NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:self.attributedText.string attributes:self.qmui_textAttributes]; + attributedString = [[self attributedStringWithKernAndLineHeightAdjusted:attributedString] mutableCopy]; + [self setAttributedText:attributedString]; } - (CGFloat)qmui_lineHeight { - return [objc_getAssociatedObject(self, &kAssociatedObjectKey_lineHeight) floatValue]; + if ([self _hasSetQmuiLineHeight]) { + return [(NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_lineHeight) qmui_CGFloatValue]; + } else if (self.attributedText.length) { + __block NSMutableAttributedString *string = [self.attributedText mutableCopy]; + __block CGFloat result = 0; + [string enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(NSParagraphStyle *style, NSRange range, BOOL * _Nonnull stop) { + // 如果用户已经通过传入 NSParagraphStyle 对文字整个 range 设置了行高,则这里不应该再次调整行高 + if (NSEqualRanges(range, NSMakeRange(0, string.length))) { + if (style && (style.maximumLineHeight || style.minimumLineHeight)) { + result = style.maximumLineHeight; + *stop = YES; + } + } + }]; + + 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; } -- (instancetype)initWithFont:(UIFont *)font textColor:(UIColor *)textColor { - if (self = [super init]) { - self.font = font; - self.textColor = textColor; +- (BOOL)_hasSetQmuiLineHeight { + return !!objc_getAssociatedObject(self, &kAssociatedObjectKey_lineHeight); +} + +- (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; } - return self; + 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 { @@ -165,8 +275,10 @@ - (void)qmui_setTheSameAppearanceAsLabel:(UILabel *)label { self.backgroundColor = label.backgroundColor; self.lineBreakMode = label.lineBreakMode; self.textAlignment = label.textAlignment; - if ([self respondsToSelector:@selector(contentEdgeInsets)] && [label respondsToSelector:@selector(contentEdgeInsets)]) { - ((QMUILabel *)self).contentEdgeInsets = ((QMUILabel *)label).contentEdgeInsets; + if ([self respondsToSelector:@selector(setContentEdgeInsets:)] && [label respondsToSelector:@selector(contentEdgeInsets)]) { + UIEdgeInsets contentEdgeInsets; + [label qmui_performSelector:@selector(contentEdgeInsets) withPrimitiveReturnValue:&contentEdgeInsets]; + [self qmui_performSelector:@selector(setContentEdgeInsets:) withArguments:&contentEdgeInsets, nil]; } } @@ -177,11 +289,70 @@ - (void)qmui_calculateHeightAfterSetAppearance { } - (void)qmui_avoidBlendedLayersIfShowingChineseWithBackgroundColor:(UIColor *)color { - self.opaque = YES;// 本来默认就是YES,这里还是明确写一下,表意清晰 + self.opaque = YES;// 本来默认就是YES,这里还是明确写一下 self.backgroundColor = color; - if (IOS_VERSION >= 8.0) { - self.clipsToBounds = YES;// 只clip不适用cornerRadius就不会触发offscreen render + self.clipsToBounds = YES;// 只 clip 不使用 cornerRadius就不会触发offscreen render +} + +@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/QMUIKit/UIKitExtensions/UIMenuController+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIMenuController+QMUI.h new file mode 100644 index 00000000..9b36eaad --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIMenuController+QMUI.h @@ -0,0 +1,23 @@ +/** + * 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.h +// QMUIKit +// +// Created by 陈志宏 on 2019/7/21. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@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 new file mode 100644 index 00000000..7226ec6b --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+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. + */ + +// +// UINavigationBar+QMUI.h +// QMUIKit +// +// Created by QMUI Team on 2018/O/8. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UINavigationBar (QMUI) + +/** + UINavigationBar 在 iOS 11 下所有的 item 都会由 contentView 管理,只要在 UINavigationController init 完成后就能拿到 qmui_contentView 的值 + */ +@property(nonatomic, strong, readonly, nullable) UIView *qmui_contentView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m new file mode 100644 index 00000000..8d82a777 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m @@ -0,0 +1,377 @@ +/** + * 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+QMUI.m +// QMUIKit +// +// Created by QMUI Team on 2018/O/8. +// + +#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) + ++ (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 + }); +} + +- (UIView *)qmui_contentView { + return [self valueForKeyPath:@"visualProvider.contentView"]; +} + +- (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/UINavigationBar+Transition.h b/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+Transition.h deleted file mode 100644 index dbd5b0f6..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+Transition.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// QMUINavigationBar+Transition.h -// qmui -// -// Created by bang on 11/25/16. -// Copyright © 2016 QMUI Team. All rights reserved. -// - -#import - -@interface UINavigationBar (Transition) - -/// 用来模仿真的navBar,配合 UINavigationController+NavigationBarTransition 在转场过程中存在的一条假navBar -@property (nonatomic, strong) UINavigationBar *transitionNavigationBar; - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+Transition.m b/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+Transition.m deleted file mode 100644 index f8e9278b..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/UINavigationBar+Transition.m +++ /dev/null @@ -1,58 +0,0 @@ -// -// QMUINavigationBar+Transition.m -// qmui -// -// Created by bang on 11/25/16. -// Copyright © 2016 QMUI Team. All rights reserved. -// - -#import "UINavigationBar+Transition.h" -#import "QMUICore.h" - -@implementation UINavigationBar (Transition) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - Class cls = [self class]; - ReplaceMethod(cls, @selector(setShadowImage:), @selector(NavigationBarTransition_setShadowImage:)); - ReplaceMethod(cls, @selector(setBarTintColor:), @selector(NavigationBarTransition_setBarTintColor:)); - ReplaceMethod(cls, @selector(setBackgroundImage:forBarMetrics:), @selector(NavigationBarTransition_setBackgroundImage:forBarMetrics:)); - - }); -} - -- (void)NavigationBarTransition_setShadowImage:(UIImage *)image { - [self NavigationBarTransition_setShadowImage:image]; - if (self.transitionNavigationBar) { - self.transitionNavigationBar.shadowImage = image; - } -} - - -- (void)NavigationBarTransition_setBarTintColor:(UIColor *)tintColor { - [self NavigationBarTransition_setBarTintColor:tintColor]; - if (self.transitionNavigationBar) { - self.transitionNavigationBar.barTintColor = self.barTintColor; - } -} - -- (void)NavigationBarTransition_setBackgroundImage:(UIImage *)backgroundImage forBarMetrics:(UIBarMetrics)barMetrics { - [self NavigationBarTransition_setBackgroundImage:backgroundImage forBarMetrics:barMetrics]; - if (self.transitionNavigationBar) { - [self.transitionNavigationBar setBackgroundImage:backgroundImage forBarMetrics:barMetrics]; - } -} - -static char transitionNavigationBarKey; - -- (UINavigationBar *)transitionNavigationBar { - return objc_getAssociatedObject(self, &transitionNavigationBarKey); -} - -- (void)setTransitionNavigationBar:(UINavigationBar *)transitionNavigationBar { - objc_setAssociatedObject(self, &transitionNavigationBarKey, transitionNavigationBar, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+NavigationBarTransition.h b/QMUI/QMUIKit/UIKitExtensions/UINavigationController+NavigationBarTransition.h deleted file mode 100644 index 39149df5..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+NavigationBarTransition.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// UINavigationController+NavigationBarTransition.h -// qmui -// -// Created by QQMail on 16/2/22. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import -#import - -/** - * 因为系统的UINavigationController只有一个navBar,所以会导致在切换controller的时候,如果两个controller的navBar状态不一致(包括backgroundImgae、shadowImage、barTintColor等等),就会导致在刚要切换的瞬间,navBar的状态都立马变成下一个controller所设置的样式了,为了解决这种情况,QMUI给出了一个方案,有四个方法可以决定你在转场的时候要不要使用自定义的navBar来模仿真实的navBar。 - */ -@interface UINavigationController (NavigationBarTransition) - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+NavigationBarTransition.m b/QMUI/QMUIKit/UIKitExtensions/UINavigationController+NavigationBarTransition.m deleted file mode 100644 index a58d5633..00000000 --- a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+NavigationBarTransition.m +++ /dev/null @@ -1,552 +0,0 @@ -// -// UINavigationController+NavigationBarTransition.m -// qmui -// -// Created by QQMail on 16/2/22. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "UINavigationController+NavigationBarTransition.h" -#import "QMUINavigationController.h" -#import "QMUICore.h" -#import "UINavigationController+QMUI.h" -#import "UIImage+QMUI.h" -#import "UIViewController+QMUI.h" -#import "UINavigationBar+Transition.h" -#import "QMUICommonViewController.h" -#import "QMUINavigationTitleView.h" - -@interface _QMUITransitionNavigationBar : UINavigationBar - -@end - -@implementation _QMUITransitionNavigationBar - -- (void)layoutSubviews { - [super layoutSubviews]; - if (IOS_VERSION >= 11.0) { - // iOS 11 以前,自己 init 的 navigationBar,它的 backgroundView 默认会一直保持与 navigationBar 的高度相等,但 iOS 11 Beta1 里,自己 init 的 navigationBar.backgroundView.height 默认一直是 44,所以才加上这个兼容 - UIView *backgroundView = [self valueForKey:@"backgroundView"]; - backgroundView.frame = self.bounds; - } -} - -@end - -/** - * 为了响应NavigationBarTransition分类的功能,UIViewController需要做一些相应的支持。 - * @see UINavigationController+NavigationBarTransition.h - */ -@interface UIViewController (NavigationBarTransition) - -/// 用来模仿真的navBar的,在转场过程中存在的一条假navBar -@property (nonatomic, strong) _QMUITransitionNavigationBar *transitionNavigationBar; - -/// 是否要把真的navBar隐藏 -@property (nonatomic, assign) BOOL prefersNavigationBarBackgroundViewHidden; - -/// 原始的clipsToBounds -@property(nonatomic, assign) BOOL originClipsToBounds; - -/// 原始containerView的背景色 -@property(nonatomic, strong) UIColor *originContainerViewBackgroundColor; - -/// 添加假的navBar -- (void)addTransitionNavigationBarIfNeeded; - -/// .m文件里自己赋值和使用。因为有些特殊情况下viewDidAppear之后,有可能还会调用到viewWillLayoutSubviews,导致原始的navBar隐藏,所以用这个属性做个保护。 -@property (nonatomic, assign) BOOL lockTransitionNavigationBar; - -@end - - -@implementation UIViewController (NavigationBarTransition) - -#pragma mark - 主流程 - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - Class cls = [self class]; - ReplaceMethod(cls, @selector(viewWillLayoutSubviews), @selector(NavigationBarTransition_viewWillLayoutSubviews)); - ReplaceMethod(cls, @selector(viewWillAppear:), @selector(NavigationBarTransition_viewWillAppear:)); - ReplaceMethod(cls, @selector(viewDidAppear:), @selector(NavigationBarTransition_viewDidAppear:)); - ReplaceMethod(cls, @selector(viewDidDisappear:), @selector(NavigationBarTransition_viewDidDisappear:)); - }); -} - -- (void)NavigationBarTransition_viewWillAppear:(BOOL)animated { - // 放在最前面,留一个时机给业务可以覆盖 - [self renderNavigationStyleInViewController:self animated:animated]; - [self NavigationBarTransition_viewWillAppear:animated]; -} - -- (void)NavigationBarTransition_viewDidAppear:(BOOL)animated { - if (self.transitionNavigationBar) { - [UIViewController replaceStyleForNavigationBar:self.transitionNavigationBar withNavigationBar:self.navigationController.navigationBar]; - [self removeTransitionNavigationBar]; - self.lockTransitionNavigationBar = YES; - - id transitionCoordinator = self.transitionCoordinator; - [transitionCoordinator containerView].backgroundColor = self.originContainerViewBackgroundColor; - self.view.clipsToBounds = self.originClipsToBounds; - } - self.prefersNavigationBarBackgroundViewHidden = NO; - [self NavigationBarTransition_viewDidAppear:animated]; -} - -- (void)NavigationBarTransition_viewDidDisappear:(BOOL)animated { - if (self.transitionNavigationBar) { - [self removeTransitionNavigationBar]; - self.lockTransitionNavigationBar = NO; - - self.view.clipsToBounds = self.originClipsToBounds; - } - [self NavigationBarTransition_viewDidDisappear:animated]; -} - -- (void)NavigationBarTransition_viewWillLayoutSubviews { - - id transitionCoordinator = self.transitionCoordinator; - UIViewController *fromViewController = [transitionCoordinator viewControllerForKey:UITransitionContextFromViewControllerKey]; - UIViewController *toViewController = [transitionCoordinator viewControllerForKey:UITransitionContextToViewControllerKey]; - - BOOL isCurrentToViewController = (self == self.navigationController.viewControllers.lastObject && self == toViewController); - BOOL isPushingViewContrller = [self.navigationController.viewControllers containsObject:fromViewController]; - - if (isCurrentToViewController && !self.lockTransitionNavigationBar) { - - BOOL shouldCustomNavigationBarTransition = NO; - - if (!self.transitionNavigationBar) { - - if (isPushingViewContrller) { - if ([toViewController canCustomNavigationBarTransitionWhenPushAppearing] || - [fromViewController canCustomNavigationBarTransitionWhenPushDisappearing]) { - shouldCustomNavigationBarTransition = YES; - } - } else { - if ([toViewController canCustomNavigationBarTransitionWhenPopAppearing] || - [fromViewController canCustomNavigationBarTransitionWhenPopDisappearing]) { - shouldCustomNavigationBarTransition = YES; - } - } - - if (shouldCustomNavigationBarTransition) { - if (self.navigationController.navigationBar.translucent) { - // 如果原生bar是半透明的,需要给containerView加个背景色,否则有可能会看到下面的默认黑色背景色 - toViewController.originContainerViewBackgroundColor = [transitionCoordinator containerView].backgroundColor; - [transitionCoordinator containerView].backgroundColor = [self containerViewBackgroundColor]; - } - fromViewController.originClipsToBounds = fromViewController.view.clipsToBounds; - toViewController.originClipsToBounds = toViewController.view.clipsToBounds; - fromViewController.view.clipsToBounds = NO; - toViewController.view.clipsToBounds = NO; - [self addTransitionNavigationBarIfNeeded]; - [self resizeTransitionNavigationBarFrame]; - self.navigationController.navigationBar.transitionNavigationBar = self.transitionNavigationBar; - self.prefersNavigationBarBackgroundViewHidden = YES; - } - } - } - - [self NavigationBarTransition_viewWillLayoutSubviews]; -} - -- (void)addTransitionNavigationBarIfNeeded { - - if (!self.view.window || !self.navigationController.navigationBar) { - return; - } - - UINavigationBar *originBar = self.navigationController.navigationBar; - _QMUITransitionNavigationBar *customBar = [[_QMUITransitionNavigationBar alloc] init]; - - if (customBar.barStyle != originBar.barStyle) { - customBar.barStyle = originBar.barStyle; - } - if (customBar.translucent != originBar.translucent) { - customBar.translucent = originBar.translucent; - } - if (![customBar.barTintColor isEqual:originBar.barTintColor]) { - customBar.barTintColor = originBar.barTintColor; - } - UIImage *backgroundImage = [originBar backgroundImageForBarMetrics:UIBarMetricsDefault]; - if (backgroundImage && CGSizeEqualToSize(backgroundImage.size, CGSizeZero)) { - // 假设这里的图片时通过`[UIImage new]`这种形式创建的,那么会navBar会奇怪地显示为系统默认navBar的样式。不知道为什么 navController 设置自己的 navBar 为 [UIImage new] 却没事,所以这里做个保护。 - backgroundImage = [UIImage qmui_imageWithColor:UIColorClear]; - } - [customBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault]; - [customBar setShadowImage:originBar.shadowImage]; - - self.transitionNavigationBar = customBar; - [self resizeTransitionNavigationBarFrame]; - - if (!self.navigationController.navigationBarHidden) { - [self.view addSubview:self.transitionNavigationBar]; - } -} - -- (void)removeTransitionNavigationBar { - if (!self.transitionNavigationBar) { - return; - } - [self.transitionNavigationBar removeFromSuperview]; - self.transitionNavigationBar = nil; -} - -- (void)resizeTransitionNavigationBarFrame { - if (!self.view.window) { - return; - } - UIView *backgroundView = [self.navigationController.navigationBar valueForKey:@"backgroundView"]; - CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.view]; - self.transitionNavigationBar.frame = rect; -} - -#pragma mark - 工具方法 - -// 根据当前的viewController,统一处理导航栏底部的分隔线、状态栏的颜色 -- (void)renderNavigationStyleInViewController:(UIViewController *)viewController animated:(BOOL)animated { - - // 针对一个 container view controller 里面包含了若干个 view controller,这总情况里面的 view controller 也会相应这个 render 方法,这样就会覆盖 container view controller 的设置,所以应该规避这种情况。 - if (viewController != viewController.navigationController.topViewController) { - return; - } - - if ([[viewController class] conformsToProtocol:@protocol(QMUINavigationControllerDelegate)]) { - UIViewController *vc = (UIViewController *)viewController; - - // 控制界面的状态栏颜色 - if ([vc shouldSetStatusBarStyleLight]) { - if ([[UIApplication sharedApplication] statusBarStyle] < UIStatusBarStyleLightContent) { - [QMUIHelper renderStatusBarStyleLight]; - } - } else { - if ([[UIApplication sharedApplication] statusBarStyle] >= UIStatusBarStyleLightContent) { - [QMUIHelper renderStatusBarStyleDark]; - } - } - - // 显示/隐藏 导航栏 - 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]; - } - } - } - - // 不能直接return,否则当navBar再次出现的时候可能样式不正确,这里说的再次出现有可能不是由QMUI控制的,而是自己手动触发 - // if (viewController.navigationController.isNavigationBarHidden) return; - - // 导航栏的背景 - if ([vc respondsToSelector:@selector(navigationBarBackgroundImage)]) { - UIImage *backgroundImage = [vc navigationBarBackgroundImage]; - [viewController.navigationController.navigationBar setBackgroundImage:backgroundImage forBarMetrics:UIBarMetricsDefault]; - } else { - [viewController.navigationController.navigationBar setBackgroundImage:NavBarBackgroundImage forBarMetrics:UIBarMetricsDefault]; - } - - // 导航栏底部的分隔线 - if ([vc respondsToSelector:@selector(navigationBarShadowImage)]) { - UIImage *shadowImage = [vc navigationBarShadowImage]; - [viewController.navigationController.navigationBar setShadowImage:shadowImage]; - } else { - [viewController.navigationController.navigationBar setShadowImage:NavBarShadowImage]; - } - - // 导航栏上控件的主题色 - if ([vc respondsToSelector:@selector(navigationBarTintColor)]) { - UIColor *tintColor = [vc navigationBarTintColor]; - viewController.navigationController.navigationBar.tintColor = tintColor; - } else { - viewController.navigationController.navigationBar.tintColor = NavBarTintColor; - } - - // 导航栏title的颜色 - if ([vc isKindOfClass:[QMUICommonViewController class]]) { - QMUICommonViewController *qmuiVC = (QMUICommonViewController *)vc; - if ([qmuiVC respondsToSelector:@selector(titleViewTintColor)]) { - UIColor *tintColor = [qmuiVC titleViewTintColor]; - qmuiVC.titleView.tintColor = tintColor; - } else { - qmuiVC.titleView.tintColor = NavBarTitleColor; - } - } - } -} - -+ (void)replaceStyleForNavigationBar:(UINavigationBar *)navbarA withNavigationBar:(UINavigationBar *)navbarB { - navbarB.barStyle = navbarA.barStyle; - navbarB.barTintColor = navbarA.barTintColor; - [navbarB setBackgroundImage:[navbarA backgroundImageForBarMetrics:UIBarMetricsDefault] forBarMetrics:UIBarMetricsDefault]; - [navbarB setShadowImage:navbarA.shadowImage]; -} - -// 该 viewController 是否实现自定义 navBar 动画的协议 - -- (BOOL)respondCustomNavigationBarTransitionWhenPushAppearing { - BOOL respondPushAppearing = NO; - if ([self qmui_respondQMUINavigationControllerDelegate]) { - UIViewController *vc = (UIViewController *)self; - if ([vc respondsToSelector:@selector(shouldCustomNavigationBarTransitionWhenPushAppearing)]) { - respondPushAppearing = YES; - } - } - return respondPushAppearing; -} - -- (BOOL)respondCustomNavigationBarTransitionWhenPushDisappearing { - BOOL respondPushDisappearing = NO; - if ([self qmui_respondQMUINavigationControllerDelegate]) { - UIViewController *vc = (UIViewController *)self; - if ([vc respondsToSelector:@selector(shouldCustomNavigationBarTransitionWhenPushDisappearing)]) { - respondPushDisappearing = YES; - } - } - return respondPushDisappearing; -} - -- (BOOL)respondCustomNavigationBarTransitionWhenPopAppearing { - BOOL respondPopAppearing = NO; - if ([self qmui_respondQMUINavigationControllerDelegate]) { - UIViewController *vc = (UIViewController *)self; - if ([vc respondsToSelector:@selector(shouldCustomNavigationBarTransitionWhenPopAppearing)]) { - respondPopAppearing = YES; - } - } - return respondPopAppearing; -} - -- (BOOL)respondCustomNavigationBarTransitionWhenPopDisappearing { - BOOL respondPopDisappearing = NO; - if ([self qmui_respondQMUINavigationControllerDelegate]) { - UIViewController *vc = (UIViewController *)self; - if ([vc respondsToSelector:@selector(shouldCustomNavigationBarTransitionWhenPopDisappearing)]) { - respondPopDisappearing = YES; - } - } - return respondPopDisappearing; -} - -- (BOOL)respondCustomNavigationBarTransitionIfBarHiddenable { - BOOL respondIfBarHiddenable = NO; - if ([self qmui_respondQMUINavigationControllerDelegate]) { - UIViewController *vc = (UIViewController *)self; - if ([vc respondsToSelector:@selector(shouldCustomNavigationBarTransitionIfBarHiddenable)]) { - respondIfBarHiddenable = YES; - } - } - return respondIfBarHiddenable; -} - -- (BOOL)respondCustomNavigationBarTransitionWithBarHiddenState { - BOOL respondWithBarHidden = NO; - if ([self qmui_respondQMUINavigationControllerDelegate]) { - UIViewController *vc = (UIViewController *)self; - if ([vc respondsToSelector:@selector(preferredNavigationBarHidden)]) { - respondWithBarHidden = YES; - } - } - return respondWithBarHidden; -} - -// 该 viewController 实现自定义 navBar 动画的协议的返回值 - -- (BOOL)canCustomNavigationBarTransitionWhenPushAppearing { - if ([self respondCustomNavigationBarTransitionWhenPushAppearing]) { - UIViewController *vc = (UIViewController *)self; - return [vc shouldCustomNavigationBarTransitionWhenPushAppearing]; - } - return NO; -} - -- (BOOL)canCustomNavigationBarTransitionWhenPushDisappearing { - if ([self respondCustomNavigationBarTransitionWhenPushDisappearing]) { - UIViewController *vc = (UIViewController *)self; - return [vc shouldCustomNavigationBarTransitionWhenPushDisappearing]; - } - return NO; -} - -- (BOOL)canCustomNavigationBarTransitionWhenPopAppearing { - if ([self respondCustomNavigationBarTransitionWhenPopAppearing]) { - UIViewController *vc = (UIViewController *)self; - return [vc shouldCustomNavigationBarTransitionWhenPopAppearing]; - } - return NO; -} - -- (BOOL)canCustomNavigationBarTransitionWhenPopDisappearing { - if ([self respondCustomNavigationBarTransitionWhenPopDisappearing]) { - UIViewController *vc = (UIViewController *)self; - return [vc shouldCustomNavigationBarTransitionWhenPopDisappearing]; - } - return NO; -} - -- (BOOL)canCustomNavigationBarTransitionIfBarHiddenable { - if ([self respondCustomNavigationBarTransitionIfBarHiddenable]) { - UIViewController *vc = (UIViewController *)self; - return [vc shouldCustomNavigationBarTransitionIfBarHiddenable]; - } - return NO; -} - -- (BOOL)hideNavigationBarWhenTransitioning { - if ([self respondCustomNavigationBarTransitionWithBarHiddenState]) { - UIViewController *vc = (UIViewController *)self; - BOOL hidden = [vc preferredNavigationBarHidden]; - return hidden; - } - return NO; -} - -- (UIColor *)containerViewBackgroundColor { - UIColor *backgroundColor = UIColorWhite; - if ([self qmui_respondQMUINavigationControllerDelegate]) { - UIViewController *vc = (UIViewController *)self; - if ([vc respondsToSelector:@selector(containerViewBackgroundColorWhenTransitioning)]) { - backgroundColor = [vc containerViewBackgroundColorWhenTransitioning]; - } - } - return backgroundColor; -} - -#pragma mark - Setter / Getter - -- (BOOL)lockTransitionNavigationBar { - return [objc_getAssociatedObject(self, _cmd) boolValue]; -} - -- (void)setLockTransitionNavigationBar:(BOOL)lockTransitionNavigationBar { - objc_setAssociatedObject(self, @selector(lockTransitionNavigationBar), [[NSNumber alloc] initWithBool:lockTransitionNavigationBar], OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (UINavigationBar *)transitionNavigationBar { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setTransitionNavigationBar:(UINavigationBar *)transitionNavigationBar { - objc_setAssociatedObject(self, @selector(transitionNavigationBar), transitionNavigationBar, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (BOOL)prefersNavigationBarBackgroundViewHidden { - return [objc_getAssociatedObject(self, _cmd) boolValue]; -} - -- (void)setPrefersNavigationBarBackgroundViewHidden:(BOOL)prefersNavigationBarBackgroundViewHidden { - [[self.navigationController.navigationBar valueForKey:@"backgroundView"] setHidden:prefersNavigationBarBackgroundViewHidden]; - objc_setAssociatedObject(self, @selector(prefersNavigationBarBackgroundViewHidden), [[NSNumber alloc] initWithBool:prefersNavigationBarBackgroundViewHidden], OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (BOOL)originClipsToBounds { - return [objc_getAssociatedObject(self, _cmd) boolValue]; -} - -- (void)setOriginClipsToBounds:(BOOL)originClipsToBounds { - objc_setAssociatedObject(self, @selector(originClipsToBounds), [[NSNumber alloc] initWithBool:originClipsToBounds], OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (UIColor *)originContainerViewBackgroundColor { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setOriginContainerViewBackgroundColor:(UIColor *)originContainerViewBackgroundColor { - objc_setAssociatedObject(self, @selector(originContainerViewBackgroundColor), originContainerViewBackgroundColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -@end - - -@implementation UINavigationController (NavigationBarTransition) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - Class cls = [self class]; - ReplaceMethod(cls, @selector(pushViewController:animated:), @selector(NavigationBarTransition_pushViewController:animated:)); - ReplaceMethod(cls, @selector(popViewControllerAnimated:), @selector(NavigationBarTransition_popViewControllerAnimated:)); - ReplaceMethod(cls, @selector(popToViewController:animated:), @selector(NavigationBarTransition_popToViewController:animated:)); - ReplaceMethod(cls, @selector(popToRootViewControllerAnimated:), @selector(NavigationBarTransition_popToRootViewControllerAnimated:)); - }); -} - -- (void)NavigationBarTransition_pushViewController:(UIViewController *)viewController animated:(BOOL)animated { - UIViewController *disappearingViewController = self.viewControllers.lastObject; - if (!disappearingViewController) { - return [self NavigationBarTransition_pushViewController:viewController animated:animated]; - } - - BOOL shouldCustomNavigationBarTransition = NO; - if ([disappearingViewController canCustomNavigationBarTransitionWhenPushDisappearing]) { - shouldCustomNavigationBarTransition = YES; - } - if (!shouldCustomNavigationBarTransition && [viewController canCustomNavigationBarTransitionWhenPushAppearing]) { - shouldCustomNavigationBarTransition = YES; - } - if (shouldCustomNavigationBarTransition) { - [disappearingViewController addTransitionNavigationBarIfNeeded]; - disappearingViewController.prefersNavigationBarBackgroundViewHidden = YES; - } - - return [self NavigationBarTransition_pushViewController:viewController animated:animated]; -} - -- (UIViewController *)NavigationBarTransition_popViewControllerAnimated:(BOOL)animated { - UIViewController *disappearingViewController = self.viewControllers.lastObject; - UIViewController *appearingViewController = self.viewControllers.count >= 2 ? self.viewControllers[self.viewControllers.count - 2] : nil; - if (!disappearingViewController) { - return [self NavigationBarTransition_popViewControllerAnimated:animated]; - } - [self handlePopViewControllerNavigationBarTransitionWithDisappearViewController:disappearingViewController appearViewController:appearingViewController]; - - return [self NavigationBarTransition_popViewControllerAnimated:animated]; -} - -- (NSArray *)NavigationBarTransition_popToViewController:(UIViewController *)viewController animated:(BOOL)animated { - UIViewController *disappearingViewController = self.viewControllers.lastObject; - UIViewController *appearingViewController = viewController; - NSArray *poppedViewControllers = [self NavigationBarTransition_popToViewController:viewController animated:animated]; - if (poppedViewControllers) { - [self handlePopViewControllerNavigationBarTransitionWithDisappearViewController:disappearingViewController appearViewController:appearingViewController]; - } - return poppedViewControllers; -} - -- (NSArray *)NavigationBarTransition_popToRootViewControllerAnimated:(BOOL)animated { - NSArray *poppedViewControllers = [self NavigationBarTransition_popToRootViewControllerAnimated:animated]; - if (self.viewControllers.count > 1) { - UIViewController *disappearingViewController = self.viewControllers.lastObject; - UIViewController *appearingViewController = self.viewControllers.firstObject; - if (poppedViewControllers) { - [self handlePopViewControllerNavigationBarTransitionWithDisappearViewController:disappearingViewController appearViewController:appearingViewController]; - } - } - return poppedViewControllers; -} - -- (void)handlePopViewControllerNavigationBarTransitionWithDisappearViewController:(UIViewController *)disappearViewController appearViewController:(UIViewController *)appearViewController { - BOOL shouldCustomNavigationBarTransition = NO; - if ([disappearViewController canCustomNavigationBarTransitionWhenPopDisappearing]) { - shouldCustomNavigationBarTransition = YES; - } - if (appearViewController && !shouldCustomNavigationBarTransition && [appearViewController canCustomNavigationBarTransitionWhenPopAppearing]) { - shouldCustomNavigationBarTransition = YES; - } - 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; - } -} - -@end diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h index 3358a052..1a80c2d0 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h @@ -1,18 +1,88 @@ +/** + * 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. + */ + // // UINavigationController+QMUI.h // qmui // -// Created by QQMail on 16/1/12. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/1/12. // #import #import +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; +- (NSArray *)qmui_popToViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^_Nullable)(void))completion; +- (NSArray *)qmui_popToRootViewControllerAnimated:(BOOL)animated completion:(void (^_Nullable)(void))completion; @end @@ -25,11 +95,11 @@ @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; @@ -43,3 +113,5 @@ @interface UIViewController (BackBarButtonSupport) @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m index aebc67e9..f5a15064 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m @@ -1,107 +1,607 @@ +/** + * 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. + */ + // // UINavigationController+QMUI.m // qmui // -// Created by QQMail on 16/1/12. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/1/12. // #import "UINavigationController+QMUI.h" #import "QMUICore.h" +#import "QMUILog.h" +#import "UIViewController+QMUI.h" -@interface UINavigationController (BackButtonHandlerProtocol) +@interface _QMUINavigationInteractiveGestureDelegator : NSObject -// `UINavigationControllerBackButtonHandlerProtocol`的`canPopViewController`功能里面,当 A canPop = NO,B canPop = YES,那么从 B 手势返回到 A,也会触发需求 A 的 `canPopViewController` 方法,这是因为手势返回会去询问`gestureRecognizerShouldBegin:`和`qmui_navigationBar:shouldPopItem:`,而这两个方法里面的 self.topViewController 是不同的对象,所以导致这个问题。所以通过 tmp_topViewController 来记录 self.topViewController 从而保证两个地方的值是相等的。 +@property(nonatomic, weak, readonly) UINavigationController *parentViewController; +- (instancetype)initWithParentViewController:(UINavigationController *)parentViewController; +@end -- (nullable UIViewController *)tmp_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 (BackButtonHandlerProtocol) +@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) { + 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(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 + 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); + }; + + 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; + + [selfObject setQmui_navigationAction:QMUINavigationActionWillPush animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + + if (shouldInvokeAppearanceMethod) { + [disappearingViewControllers.lastObject beginAppearanceTransition:NO animated:animated]; + [appearingViewController beginAppearanceTransition:YES animated:animated]; + } + + callSuperBlock(); + + [selfObject setQmui_navigationAction:QMUINavigationActionDidPush animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + + [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) { + + // 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 qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { + if (transitionCompletion) { + transitionCompletion(); + } + }]; + } + + 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]; -- (UIViewController *)tmp_topViewController { - return objc_getAssociatedObject(self, _cmd); + [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]; + }]; + }; + }); + }); } -- (void)setTmp_topViewController:(UIViewController *)viewController { - objc_setAssociatedObject(self, @selector(tmp_topViewController), viewController, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +- (void)qmui_didInitialize { } -@end +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]; +} -@implementation UINavigationController (QMUI) +- (void)qmui_addNavigationActionDidChangeBlock:(QMUINavigationActionDidChangeBlock)block { + if (!self.qmuinc_navigationActionDidChangeBlocks) { + self.qmuinc_navigationActionDidChangeBlocks = NSMutableArray.new; + } + [self.qmuinc_navigationActionDidChangeBlocks addObject:block]; +} -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - ReplaceMethod([self class], @selector(viewDidLoad), @selector(qmui_viewDidLoad)); - ReplaceMethod([self class], @selector(navigationBar:shouldPopItem:), @selector(qmui_navigationBar:shouldPopItem:)); - }); +- (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 { + // 要先进行转场操作才能产生 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 { + // 要先进行转场操作才能产生 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; } -static char originGestureDelegateKey; -- (void)qmui_viewDidLoad { - [self qmui_viewDidLoad]; - objc_setAssociatedObject(self, &originGestureDelegateKey, self.interactivePopGestureRecognizer.delegate, OBJC_ASSOCIATION_ASSIGN); - self.interactivePopGestureRecognizer.delegate = (id)self; +- (NSArray *)qmui_popToViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^)(void))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 { + // 要先进行转场操作才能产生 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; } -- (BOOL)qmui_navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item { - - // 如果nav的vc栈中有两个vc,第一个是root,第二个是second。这是second页面如果点击系统的返回按钮,topViewController获取的栈顶vc是second,而如果是直接代码写的pop操作,则获取的栈顶vc是root。也就是说只要代码写了pop操作,则系统会直接将顶层vc也就是second出栈,然后才回调的,所以这时我们获取到的顶层vc就是root了。然而不管哪种方式,参数中的item都是second的item。 - BOOL isPopedByCoding = item != [self topViewController].navigationItem; - - // !isPopedByCoding 要放在前面,这样当 !isPopedByCoding 不满足的时候就不会去询问 canPopViewController 了,可以避免额外调用 canPopViewController 里面的逻辑导致 - BOOL canPopViewController = !isPopedByCoding && [self canPopViewController:self.tmp_topViewController ?: [self topViewController]]; - - if (canPopViewController || isPopedByCoding) { - self.tmp_topViewController = nil; - return [self qmui_navigationBar:navigationBar shouldPopItem:item]; - } else { - [self resetSubviewsInNavBar:navigationBar]; - self.tmp_topViewController = nil; +- (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 NO; + return self; } -- (void)resetSubviewsInNavBar:(UINavigationBar *)navBar { - // Workaround for >= iOS7.1. Thanks to @boliva - http://stackoverflow.com/posts/comments/34452906 - for(UIView *subview in [navBar subviews]) { - if(subview.alpha < 1.0) { - [UIView animateWithDuration:.25 animations:^{ - subview.alpha = 1.0; - }]; +#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 (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; } @@ -113,29 +613,24 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { - if (gestureRecognizer == self.interactivePopGestureRecognizer) { - idoriginGestureDelegate = objc_getAssociatedObject(self, &originGestureDelegateKey); - if ([originGestureDelegate respondsToSelector:@selector(gestureRecognizer:shouldReceiveTouch:)]) { - // 先判断要不要强制开启手势返回 - UIViewController *viewController = [self topViewController]; - if (self.viewControllers.count > 1 && - self.interactivePopGestureRecognizer.enabled && - [viewController respondsToSelector:@selector(forceEnableInteractivePopGestureRecognizer)] && - [viewController forceEnableInteractivePopGestureRecognizer]) { + if (gestureRecognizer == self.parentViewController.interactivePopGestureRecognizer) { + idoriginGestureDelegate = self.parentViewController.qmui_interactivePopGestureRecognizerDelegate; + if ([originGestureDelegate respondsToSelector:_cmd]) { + BOOL originalValue = [originGestureDelegate gestureRecognizer:gestureRecognizer shouldReceiveTouch:touch]; + if (!originalValue && [self.parentViewController shouldForceEnableInteractivePopGestureRecognizer]) { return YES; } - // 调用默认的实现 - return [originGestureDelegate gestureRecognizer:gestureRecognizer shouldReceiveTouch:touch]; + return originalValue; } } return YES; } - (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; @@ -143,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; @@ -155,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 81dbd551..c4005762 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.h @@ -1,13 +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. + */ + // // UIScrollView+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import +typedef NS_ENUM(NSInteger, QMUIScrollPosition) { + QMUIScrollPositionNone, // 滚动到临近的区域(可能是 Top 也可能是 Bottom) + QMUIScrollPositionTop, // 滚动到可视区域最顶部 + QMUIScrollPositionMiddle, // 滚动到可视区域中间 + QMUIScrollPositionBottom, // 滚动到可视区域底部 +}; + @interface UIScrollView (QMUI) /// 判断UIScrollView是否已经处于顶部(当UIScrollView内容不够多不可滚动时,也认为是在顶部) @@ -16,6 +30,15 @@ /// 判断UIScrollView是否已经处于底部(当UIScrollView内容不够多不可滚动时,也认为是在底部) @property(nonatomic, assign, readonly) BOOL qmui_alreadyAtBottom; +/// UIScrollView 的真正 inset,在 iOS11 以后需要用到 adjustedContentInset 而在 iOS11 以前只需要用 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内容是否足够滚动 * @warning 避免与scrollEnabled混淆 @@ -37,6 +60,11 @@ /// 等同于[self qmui_scrollToTopAnimated:NO] - (void)qmui_scrollToTop; +/** + 滚到列表顶部,但如果 contentInset.top 与上一次相同则不会执行滚动操作,通常用于 UIScrollViewContentInsetAdjustmentNever 的 scrollView 设置完业务的 contentInset 后将列表滚到顶部。 + */ +- (void)qmui_scrollToTopUponContentInsetTopChange; + /** * 如果当前的scrollView可滚动,则将其滚动到最底部 * @param animated 是否用动画表现 @@ -47,7 +75,18 @@ /// 等同于[self qmui_scrollToBottomAnimated:NO] - (void)qmui_scrollToBottom; -// 立即停止滚动,用于那种手指已经离开屏幕但列表还在滚动的情况。 +/// 将 scroll 坐标系内的指定 rect 滚动到指定位置。 +- (void)qmui_scrollToRect:(CGRect)rect atPosition:(QMUIScrollPosition)scrollPosition animated:(BOOL)animated; + +/// 立即停止滚动,用于那种手指已经离开屏幕但列表还在滚动的情况。 - (void)qmui_stopDeceleratingIfNeeded; +/** + 以动画的形式修改 contentInset + + @param contentInset 要修改为的 contentInset + @param animated 是否要使用动画修改 + */ +- (void)qmui_setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated; + @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.m index 31ad998d..9ea0a5a0 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIScrollView+QMUI.m @@ -1,33 +1,75 @@ +/** + * 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. + */ + // // UIScrollView+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import "UIScrollView+QMUI.h" #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, ^{ - ReplaceMethod([self class], @selector(description), @selector(qmui_description)); + + 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; + } + }; + }); + } }); } -- (NSString *)qmui_description { - return [NSString stringWithFormat:@"%@, contentInset = %@", [self qmui_description], NSStringFromUIEdgeInsets(self.contentInset)]; -} - - (BOOL)qmui_alreadyAtTop { - if (!self.qmui_canScroll) { - return YES; - } - - if (self.contentOffset.y == -self.contentInset.top) { + if (CGFloatEqualToFloat(self.contentOffset.y, -self.adjustedContentInset.top)) { return YES; } @@ -39,26 +81,45 @@ - (BOOL)qmui_alreadyAtBottom { return YES; } - if (self.contentOffset.y == self.contentSize.height + self.contentInset.bottom - CGRectGetHeight(self.bounds)) { + if (CGFloatEqualToFloat(self.contentOffset.y, self.contentSize.height + self.adjustedContentInset.bottom - CGRectGetHeight(self.bounds))) { return YES; } return NO; } +- (UIEdgeInsets)qmui_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 { // 没有高度就不用算了,肯定不可滚动,这里只是做个保护 if (CGSizeIsEmpty(self.bounds.size)) { return NO; } - BOOL canVerticalScroll = self.contentSize.height + UIEdgeInsetsGetVerticalValue(self.contentInset) > CGRectGetHeight(self.bounds); - BOOL canHorizontalScoll = self.contentSize.width + UIEdgeInsetsGetHorizontalValue(self.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.contentInset.left, -self.contentInset.top) animated:animated]; + [self setContentOffset:CGPointMake(-self.adjustedContentInset.left, -self.adjustedContentInset.top) animated:animated]; } } @@ -70,9 +131,16 @@ - (void)qmui_scrollToTop { [self qmui_scrollToTopAnimated:NO]; } +- (void)qmui_scrollToTopUponContentInsetTopChange { + if (self.qmuiscroll_lastInsetTopWhenScrollToTop != self.contentInset.top) { + [self qmui_scrollToTop]; + self.qmuiscroll_lastInsetTopWhenScrollToTop = self.contentInset.top; + } +} + - (void)qmui_scrollToBottomAnimated:(BOOL)animated { if ([self qmui_canScroll]) { - [self setContentOffset:CGPointMake(self.contentOffset.x, self.contentSize.height + self.contentInset.bottom - CGRectGetHeight(self.bounds)) animated:animated]; + [self setContentOffset:CGPointMake(self.contentOffset.x, self.contentSize.height + self.adjustedContentInset.bottom - CGRectGetHeight(self.bounds)) animated:animated]; } } @@ -86,4 +154,30 @@ - (void)qmui_stopDeceleratingIfNeeded { } } +- (void)qmui_setContentInset:(UIEdgeInsets)contentInset animated:(BOOL)animated { + [UIView qmui_animateWithAnimated:animated duration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + self.contentInset = contentInset; + } 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 5bdacc6e..53c8cf46 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.h @@ -1,23 +1,116 @@ +/** + * 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. + */ + // // UISearchBar+QMUI.h // qmui // -// Created by MoLice on 16/5/26. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/5/26. // #import #import +NS_ASSUME_NONNULL_BEGIN + +/** + 提供更丰富的接口来修改 UISearchBar 的样式,注意大部分接口都同时支持配置表和 UIAppearance,如果有使用配置表并且该项的值不为 nil,则以配置表的值为准。 + */ @interface UISearchBar (QMUI) -@property(nonatomic, strong) UIColor *qmui_placeholderColor; -@property(nonatomic, strong) UIColor *qmui_textColor; -@property(nonatomic, strong) UIFont *qmui_font; +/** + 获取与 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; + +/// 输入框的文字颜色 +@property(nullable, nonatomic, strong) UIColor *qmui_textColor UI_APPEARANCE_SELECTOR; + +/// 输入框的文字字体,会同时影响 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; + +/// 支持根据 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 的取消按钮是在需要的时候才会生成(具体时机可以看 .m 内的 +load 方法) +@property(nullable, nonatomic, weak, readonly) UIButton *qmui_cancelButton; + +/// 取消按钮的字体,由于系统的 cancelButton 是懒加载的,所以当不存在 cancelButton 时该值为 nil +@property(nullable, nonatomic, strong) UIFont *qmui_cancelButtonFont UI_APPEARANCE_SELECTOR; -/// 获取 searchBar 内的输入框 -@property(nonatomic, weak, readonly) UITextField *qmui_textField; +/// 取消按钮相对于系统原有布局位置的上下左右的偏移。 +@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 fbe9c048..4e50a0b5 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m @@ -1,38 +1,299 @@ +/** + * 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. + */ + // // UISearchBar+QMUI.m // qmui // -// Created by MoLice on 16/5/26. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/5/26. // #import "UISearchBar+QMUI.h" #import "QMUICore.h" #import "UIImage+QMUI.h" +#import "UIView+QMUI.h" +#import "UIViewController+QMUI.h" + +@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) +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, ^{ - ReplaceMethod([self class], @selector(setPlaceholder:), @selector(qmui_setPlaceholder:)); + + 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) { + NSMutableAttributedString *string = selfObject.searchTextField.attributedPlaceholder.mutableCopy; + if (selfObject.qmui_placeholderColor) { + [string addAttribute:NSForegroundColorAttributeName value:selfObject.qmui_placeholderColor range:NSMakeRange(0, string.length)]; + } + if (selfObject.qmui_font) { + [string addAttribute:NSFontAttributeName value:selfObject.qmui_font range:NSMakeRange(0, string.length)]; + } + // 默认移除文字阴影 + [string removeAttribute:NSShadowAttributeName range:NSMakeRange(0, string.length)]; + selfObject.searchTextField.attributedPlaceholder = string.copy; + } + })); + + // 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(); + } + }; + + }); + + 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) { + + // 修复 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]; + + }); + + 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 (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, frame); + + }; + }); + + // [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); + } + + // 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)qmui_setPlaceholder:(NSString *)placeholder { - [self qmui_setPlaceholder:placeholder]; - if (self.qmui_placeholderColor || self.qmui_font) { - NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init]; - if (self.qmui_placeholderColor) { - attributes[NSForegroundColorAttributeName] = self.qmui_placeholderColor; - } - if (self.qmui_font) { - attributes[NSFontAttributeName] = self.qmui_font; - } - self.qmui_textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:placeholder attributes:attributes]; +- (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); @@ -49,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 { @@ -63,72 +324,580 @@ - (void)setQmui_font:(UIFont *)qmui_font { // 触发 setPlaceholder 里更新 placeholder 样式的逻辑 self.placeholder = self.placeholder; } + + // 更新输入框的文字样式 + 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 qmui_valueForKey:@"cancelButton"]; + return cancelButton; +} + +static char kAssociatedObjectKey_cancelButtonFont; +- (void)setQmui_cancelButtonFont:(UIFont *)qmui_cancelButtonFont { + objc_setAssociatedObject(self, &kAssociatedObjectKey_cancelButtonFont, qmui_cancelButtonFont, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self.qmui_cancelButton.titleLabel.font = qmui_cancelButtonFont; +} + +- (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 { + UISegmentedControl *segmentedControl = [self qmui_valueForKey:@"scopeBar"]; + return segmentedControl; +} + +- (BOOL)qmui_isActive { + return (self.qmui_searchController.isBeingPresented || self.qmui_searchController.isActive); +} + +- (UISearchController *)qmui_searchController { + return [self qmui_valueForKey:@"_searchController"]; +} + +- (UIView *)qmui_backgroundView { + BeginIgnorePerformSelectorLeaksWarning + UIView *backgroundView = [self performSelector:NSSelectorFromString(@"_backgroundView")]; + EndIgnorePerformSelectorLeaksWarning + return backgroundView; } - (void)qmui_styledAsQMUISearchBar { - // 搜索框的字号及 placeholder 的字号 - UIFont *font = SearchBarFont; - if (font) { - self.qmui_font = font; + if (!QMUICMIActivated) { + return; } + // 搜索框的字号及 placeholder 的字号 + self.qmui_font = SearchBarFont; + // 搜索框的文字颜色 - UIColor *textColor = SearchBarTextColor; - if (textColor) { - self.qmui_textColor = SearchBarTextColor; - } - + self.qmui_textColor = SearchBarTextColor; + // placeholder 的文字颜色 - UIColor *placeholderColor = SearchBarPlaceholderColor; - if (placeholderColor) { - self.qmui_placeholderColor = SearchBarPlaceholderColor; - } - + self.qmui_placeholderColor = SearchBarPlaceholderColor; + self.placeholder = @"搜索"; self.autocorrectionType = UITextAutocorrectionTypeNo; self.autocapitalizationType = UITextAutocapitalizationTypeNone; - self.searchTextPositionAdjustment = UIOffsetMake(5, 0); - + // 设置搜索icon UIImage *searchIconImage = SearchBarSearchIconImage; if (searchIconImage) { - if (!CGSizeEqualToSize(searchIconImage.size, CGSizeMake(13, 13))) { - QMUILog(@"搜索框放大镜图片(SearchBarSearchIconImage)的大小最好为 (13, 13),否则会失真,目前的大小为 %@", NSStringFromCGSize(searchIconImage.size)); + if (!CGSizeEqualToSize(searchIconImage.size, CGSizeMake(14, 14))) { + NSLog(@"搜索框放大镜图片(SearchBarSearchIconImage)的大小最好为 (14, 14),否则会失真,目前的大小为 %@", NSStringFromCGSize(searchIconImage.size)); } [self setImage:searchIconImage forSearchBarIcon:UISearchBarIconSearch state:UIControlStateNormal]; } - + // 设置搜索右边的清除按钮的icon UIImage *clearIconImage = SearchBarClearIconImage; if (clearIconImage) { [self setImage:clearIconImage forSearchBarIcon:UISearchBarIconClear state:UIControlStateNormal]; } - + // 设置SearchBar上的按钮颜色 self.tintColor = SearchBarTintColor; - + // 输入框背景图 - UIColor *textFieldBackgroundColor = SearchBarTextFieldBackground; - if (textFieldBackgroundColor) { - [self setSearchFieldBackgroundImage:[[[UIImage qmui_imageWithColor:SearchBarTextFieldBackground size:CGSizeMake(60, 28) cornerRadius:SearchBarTextFieldCornerRadius] qmui_imageWithBorderColor:SearchBarTextFieldBorderColor borderWidth:PixelOne cornerRadius:SearchBarTextFieldCornerRadius] resizableImageWithCapInsets:UIEdgeInsetsMake(14, 14, 14, 14)] forState:UIControlStateNormal]; + UIImage *searchFieldBackgroundImage = SearchBarTextFieldBackgroundImage; + if (searchFieldBackgroundImage) { + [self setSearchFieldBackgroundImage:searchFieldBackgroundImage forState:UIControlStateNormal]; + } + + // 输入框边框 + UIColor *textFieldBorderColor = SearchBarTextFieldBorderColor; + if (textFieldBorderColor) { + self.searchTextField.layer.borderWidth = PixelOne; + self.searchTextField.layer.borderColor = textFieldBorderColor.CGColor; } // 整条bar的背景 - // iOS7及以后不用barTintColor设置背景是因为这么做的话会出现上下border,去不掉,所以iOS6和7都改为用backgroundImage实现 - UIColor *barTintColor = SearchBarBarTintColor; - if (barTintColor) { - UIImage *backgroundImage = [[[UIImage qmui_imageWithColor:SearchBarBarTintColor size:CGSizeMake(10, 10) cornerRadius:0] qmui_imageWithBorderColor:SearchBarBottomBorderColor borderWidth:PixelOne borderPosition:QMUIImageBorderPositionBottom] resizableImageWithCapInsets:UIEdgeInsetsMake(1, 1, 1, 1)]; + // 为了让 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); + + 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]; + + [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]; + } + } + } 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); + + 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 c8172a03..6615e4c7 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.h @@ -1,13 +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. + */ + // // UITabBar+QMUI.h // qmui // -// Created by MoLice on 2017/2/14. -// Copyright © 2017年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2017/2/14. // #import +#import "QMUIBarProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 -@interface UITabBar (QMUI) +UIKIT_EXTERN API_AVAILABLE(ios(13.0), tvos(13.0)) @interface UITabBarAppearance (QMUI) +/** + 同时设置 stackedLayoutAppearance、inlineLayoutAppearance、compactInlineLayoutAppearance 三个状态下的 itemAppearance + */ +- (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 653ced46..f45c428c 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UITabBar+QMUI.m @@ -1,16 +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. + */ + // // UITabBar+QMUI.m // qmui // -// Created by MoLice on 2017/2/14. -// Copyright © 2017年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2017/2/14. // #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 () @@ -21,32 +35,216 @@ @interface UITabBar () @implementation UITabBar (QMUI) +QMUISynthesizeBOOLProperty(canItemRespondDoubleTouch, setCanItemRespondDoubleTouch) +QMUISynthesizeNSIntegerProperty(lastTouchedTabBarItemViewIndex, setLastTouchedTabBarItemViewIndex) +QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouchCount) + + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ReplaceMethod([self class], @selector(setItems:animated:), @selector(qmui_setItems:animated:)); - ReplaceMethod([self class], @selector(setSelectedItem:), @selector(qmui_setSelectedItem:)); + + // -[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 (*originSelectorIMP)(id, SEL, UITabBarItem *); + originSelectorIMP = (void (*)(id, SEL, UITabBarItem *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, selectedItem); + + NSInteger newerSelectedIndex = [selfObject.items indexOfObject:selectedItem]; + // 只有双击当前正在显示的界面的 tabBarItem,才能正常触发双击事件 + selfObject.canItemRespondDoubleTouch = olderSelectedIndex == newerSelectedIndex; + }; + }); + + // 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, 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]; + } + }; + }); + + // 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) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + [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; + } + 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)qmui_setItems:(NSArray *)items animated:(BOOL)animated { - [self qmui_setItems:items animated:animated]; - - for (UITabBarItem *item in items) { - UIControl *itemView = item.qmui_barButton; - [itemView addTarget:self action:@selector(handleTabBarItemViewEvent:) forControlEvents:UIControlEventTouchUpInside]; - } -} - -- (void)qmui_setSelectedItem:(UITabBarItem *)selectedItem { - NSInteger olderSelectedIndex = self.selectedItem ? [self.items indexOfObject:self.selectedItem] : -1; - [self qmui_setSelectedItem:selectedItem]; - NSInteger newerSelectedIndex = [self.items indexOfObject:selectedItem]; - // 只有双击当前正在显示的界面的 tabBarItem,才能正常触发双击事件 - self.canItemRespondDoubleTouch = olderSelectedIndex == newerSelectedIndex; -} - -- (void)handleTabBarItemViewEvent:(UIControl *)itemView { +- (void)qmuitb_handleTabBarItemViewEvent:(UIControl *)itemView { if (!self.canItemRespondDoubleTouch) { return; @@ -58,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]; @@ -68,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; } @@ -80,42 +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; } -#pragma mark - Swizzle Property Getter/Setter - -static char kAssociatedObjectKey_canItemRespondDoubleTouch; -- (void)setCanItemRespondDoubleTouch:(BOOL)canItemRespondDoubleTouch { - objc_setAssociatedObject(self, &kAssociatedObjectKey_canItemRespondDoubleTouch, @(canItemRespondDoubleTouch), OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (BOOL)canItemRespondDoubleTouch { - return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_canItemRespondDoubleTouch)) boolValue]; -} - -static char kAssociatedObjectKey_lastTouchedTabBarItemViewIndex; -- (void)setLastTouchedTabBarItemViewIndex:(NSInteger)lastTouchedTabBarItemViewIndex { - objc_setAssociatedObject(self, &kAssociatedObjectKey_lastTouchedTabBarItemViewIndex, @(lastTouchedTabBarItemViewIndex), OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (NSInteger)lastTouchedTabBarItemViewIndex { - return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_lastTouchedTabBarItemViewIndex)) integerValue]; -} +@end -static char kAssociatedObjectKey_tabBarItemViewTouchCount; -- (void)setTabBarItemViewTouchCount:(NSInteger)tabBarItemViewTouchCount { - objc_setAssociatedObject(self, &kAssociatedObjectKey_tabBarItemViewTouchCount, @(tabBarItemViewTouchCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} +@implementation UITabBarAppearance (QMUI) -- (NSInteger)tabBarItemViewTouchCount { - return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_tabBarItemViewTouchCount)) integerValue]; +- (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 88a752a9..47fc81c0 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.h @@ -1,31 +1,37 @@ +/** + * 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. + */ + // // UITabBarItem+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #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); - -/** - * 获取一个UITabBarItem内的按钮,里面包含imageView、label等子View - */ -- (UIControl *)qmui_barButton; +@property(nonatomic, copy, nullable) void (^qmui_doubleTapBlock)(UITabBarItem *tabBarItem, NSInteger index); /** * 获取一个UITabBarItem内显示图标的UIImageView,如果找不到则返回nil - * @warning 需要对nil的返回值做保护 */ -- (UIImageView *)qmui_imageView; +- (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 22583717..c3cd55dd 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m @@ -1,51 +1,36 @@ +/** + * 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. + */ + // // UITabBarItem+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import "UITabBarItem+QMUI.h" #import "QMUICore.h" +#import "UIBarItem+QMUI.h" @implementation UITabBarItem (QMUI) -- (UIControl *)qmui_barButton { - return [self valueForKey:@"view"]; -} +QMUISynthesizeIdCopyProperty(qmui_doubleTapBlock, setQmui_doubleTapBlock) - (UIImageView *)qmui_imageView { - UIControl *barButton = [self qmui_barButton]; + return [self.class qmui_imageViewInTabBarButton:self.qmui_view]; +} + ++ (UIImageView *)qmui_imageViewInTabBarButton:(UIView *)tabBarButton { - if (!barButton) { + if (!tabBarButton) { return nil; } - - for (UIView *subview in barButton.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; -} - -static char kAssociatedObjectKey_doubleTapBlock; -- (void)setQmui_doubleTapBlock:(void (^)(UITabBarItem *, NSInteger))qmui_doubleTapBlock { - objc_setAssociatedObject(self, &kAssociatedObjectKey_doubleTapBlock, qmui_doubleTapBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); -} - -- (void (^)(UITabBarItem *, NSInteger))qmui_doubleTapBlock { - return (void (^)(UITabBarItem *, NSInteger))objc_getAssociatedObject(self, &kAssociatedObjectKey_doubleTapBlock); + return [tabBarButton qmui_valueForKey:@"_imageView"]; } @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.h index 50836bb6..7c83ce4f 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.h @@ -1,23 +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. + */ + // // UITableView+QMUI.h // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // -#import "QMUICellHeightCache.h" -#import "QMUITableView.h" +#import +#import -typedef NS_ENUM(NSInteger, QMUITableViewCellPosition) { - QMUITableViewCellPositionNone = -1, // 初始化用 - QMUITableViewCellPositionFirstInSection, - QMUITableViewCellPositionMiddleInSection, - QMUITableViewCellPositionLastInSection, - QMUITableViewCellPositionSingleInSection, - QMUITableViewCellPositionNormal, +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, // 默认 + QMUITableViewCellPositionFirstInSection = 1 << 0, + QMUITableViewCellPositionMiddleInSection = 1 << 1, + QMUITableViewCellPositionLastInSection = 1 << 2, + QMUITableViewCellPositionSingleInSection = QMUITableViewCellPositionFirstInSection | QMUITableViewCellPositionLastInSection, }; +/** + * 这个分类提供额外的功能包括: + * 1. 将给定的 UITableView 格式化为 QMUITableView 风格的样式,以统一为配置表里的值 + * 2. 计算给定的某个 view 处于哪个 indexPath 的 cell 上 + * 3. 计算给定的某个 view 处于哪个 sectionHeader 上 + * 4. 获取所有可视范围内的 sectionHeader 的 index + * 5. 获取正处于 pinned 状态(也即悬停在顶部)的 sectionHeader 的 index + * 6. 判断某个给定的 sectionHeader 是否处于 pinned 状态 + * 7. 判断某个给定的 cell indexPath 是否处于可视范围内 + * 8. 计算给定的 cell 的 indexPath 所对应的 QMUITableViewCellPosition + * 9. 清除当前列表的所有 selection(选中的背景灰色) + * 10. 判断列表当前内容是否足够滚动 + * 11. 让某个 row 滚动到指定的位置(系统默认只可以将 row 滚动到 Top/Middle/Bottom) + * 12. 在将 searchBar 作为 tableHeaderView 的情况下,获取列表真实的 contentSize(系统为了实现列表内容不足一屏时依然可以将 searchBar 滚动到 navigationBar 下,在这种情况下会强制增大 contentSize) + * 13. 在将 searchBar 作为 tableHeaderView 的情况下,判断列表内容是否足够多到可滚动 + */ @interface UITableView (QMUI) /// 将当前tableView按照QMUI统一定义的宏来渲染外观 @@ -31,35 +58,51 @@ typedef NS_ENUM(NSInteger, QMUITableViewCellPosition) { * @param view 要计算的 UIView * @return view 所在的 indexPath,若不存在则返回 nil */ -- (NSIndexPath *)qmui_indexPathForRowAtView:(UIView *)view; +- (nullable NSIndexPath *)qmui_indexPathForRowAtView:(nullable UIView *)view; /** * 计算某个 view 处于当前 tableView 里的哪个 sectionHeaderView 内 * @param view 要计算的 UIView * @return view 所在的 sectionHeaderView 的 section,若不存在则返回 -1 */ -- (NSInteger)qmui_indexForSectionHeaderAtView:(UIView *)view; +- (NSInteger)qmui_indexForSectionHeaderAtView:(nullable UIView *)view; + +/// 获取可视范围内的所有 sectionHeader 的 index,注意 contentInset 所在的区域被视为“不可视”。 +@property(nonatomic, readonly, nullable) NSArray *qmui_indexForVisibleSectionHeaders; + +/// 获取正处于 pinned(悬停在顶部)状态的 sectionHeader 的序号,注意如果某个 section 的 numberOfRows 为 0,则这个 section 天然无法被 pinned。 +@property(nonatomic, readonly) NSInteger qmui_indexOfPinnedSectionHeader; + +/** + * 判断给定的 section 的 header 是否处于 pinned 状态 + * @param section 给定的 section 的序号 + * @note 当列表往上滚动的过程中,header1 处于将要离开 pinned 状态、header2 即将进入 pinned 状态的这个过程,header1 和 header2 均不处于 pinned 状态 + */ +- (BOOL)qmui_isHeaderPinnedForSection:(NSInteger)section; + +/// 判断当前 indexPath 的 item 是否为可视的 item +- (BOOL)qmui_cellVisibleAtIndexPath:(nullable NSIndexPath *)indexPath; /** * 根据给定的indexPath,配合dataSource得到对应的cell在当前section中所处的位置 * @param indexPath cell所在的indexPath * @return 给定indexPath对应的cell在当前section中所处的位置 */ -- (QMUITableViewCellPosition)qmui_positionForRowAtIndexPath:(NSIndexPath *)indexPath; +- (QMUITableViewCellPosition)qmui_positionForRowAtIndexPath:(nullable NSIndexPath *)indexPath; -/// 判断当前 indexPath 的 item 是否为可视的 item -- (BOOL)qmui_cellVisibleAtIndexPath:(NSIndexPath *)indexPath; - -// 取消选择状态 +/// 取消选择状态 - (void)qmui_clearsSelection; /** * 将指定的row滚到指定的位置(row的顶边缘和指定位置重叠),并对一些特殊情况做保护(例如列表内容不够一屏、要滚动的row是最后一条等) * @param offsetY 目标row要滚到的y值,这个y值是相对于tableView的frame而言的 - * @param indexPath 要滚动的目标indexPath,请自行保证indexPath是合法的 + * @param indexPath 要滚动的目标indexPath,如果该 indexPath 不合法则该方法不会有任何效果 * @param animated 是否需要动画 */ -- (void)qmui_scrollToRowFittingOffsetY:(CGFloat)offsetY atIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated; +- (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`替代。 @@ -73,73 +116,33 @@ typedef NS_ENUM(NSInteger, QMUITableViewCellPosition) { */ - (BOOL)qmui_canScroll; -@end - - -/// ====================== 动态计算 cell 高度相关 ======================= - /** - * UITableView 定义了一套动态计算 cell 高度的方式: - * - * 其思路是参考开源代码:https://github.com/forkingdog/UITableView-FDTemplateLayoutCell。 - * - * 1. cell 必须实现 sizeThatFits: 方法,在里面计算自身的高度并返回 - * 2. 初始化一个 QMUITableView,并为其指定一个 QMUITableViewDataSource - * 3. 实现 qmui_tableView:cellWithIdentifier: 方法,在里面为不同的 identifier 创建不同的 cell 实例 - * 4. 在 tableView:cellForRowAtIndexPath: 里使用 qmui_tableView:cellWithIdentifier: 获取 cell - * 5. 在 tableView:heightForRowAtIndexPath: 里使用 UITableView (QMUILayoutCell) 提供的几种方法得到 cell 的高度 - * - * 这套方式的好处是 tableView 能直接操作 cell 的实例,cell 无需增加额外的专门用于获取 cell 高度的方法。并且这套方式支持基本的高度缓存(可按 key 缓存或按 indexPath 缓存),若使用了缓存,请注意在适当的时机去更新缓存(例如某个 cell 的内容发生变化,可能 cell 的高度也会变化,则需要更新这个 cell 已被缓存起来的高度)。 - * - * 使用这套方式额外的消耗是每个 identifier 都会生成一个多余的 cell 实例(专用于高度计算),但大部分情况下一个生成一个 cell 实例并不会带来过多的负担,所以一般不用担心这个问题。 - */ - -@interface UITableView (QMUILayoutCell) + 等同于 UITableView 自 iOS 11 开始新增的同名方法,但兼容 iOS 11 以下的系统使用。 -/** - * 通过 qmui_tableView:cellWithIdentifier: 得到 identifier 对应的 cell 实例,并在 configuration 里对 cell 进行渲染后,得到 cell 的高度。 - * @param identifier cell 的 identifier - * @param configuration 用于渲染 cell 的block,一般与 tableView:cellForRowAtIndexPath: 里渲染 cell 的代码一样 + @param updates insert/delete/reload/move calls + @param completion completion callback */ -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(__kindof UITableViewCell *cell))configuration; +- (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,没必要提供该兼容性之的接口了,后续会删除。"); -/** - * 通过 qmui_tableView:cellWithIdentifier: 得到 identifier 对应的 cell 实例,并在 configuration 里对 cell 进行渲染后,得到 cell 的高度。 - * - * 以 indexPath 为单位进行缓存,相同的 indexPath 高度将不会重复计算,若需刷新高度,请参考 QMUICellHeightIndexPathCache - * - * @param identifier cell 的 identifier - * @param configuration 用于渲染 cell 的block,一般与 tableView:cellForRowAtIndexPath: 里渲染 cell 的代码一样 - */ -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UITableViewCell *cell))configuration; +@end /** - * 通过 qmui_tableView:cellWithIdentifier: 得到 identifier 对应的 cell 实例,并在 configuration 里对 cell 进行渲染后,得到 cell 的高度。 - * - * 以自定义的 key 为单位进行缓存,相同的 key 高度将不会重复计算,若需刷新高度,请参考 QMUICellHeightKeyCache - * - * @param identifier cell 的 identifier - * @param configuration 用于渲染 cell 的block,一般与 tableView:cellForRowAtIndexPath: 里渲染 cell 的代码一样 + 系统在 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 及以下系统,那可忽略这个限制。 */ -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id)key configuration:(void (^)(__kindof UITableViewCell *cell))configuration; - -@end +@interface UITableView (QMUI_InsetGrouped) -@interface UITableView (QMUIKeyedHeightCache) +/// 当使用 UITableViewStyleInsetGrouped 时可通过这个属性修改 cell 的圆角值,默认值为 10,也即 iOS 13 系统默认表现。如果要为不同 indexPath 指定不同圆角值,可在 -[UITableViewDelegate tableView:willDisplayCell:forRowAtIndexPath:] 内修改 cell.layer.cornerRadius 的值。 +@property(nonatomic, assign) CGFloat qmui_insetGroupedCornerRadius UI_APPEARANCE_SELECTOR; -@property (nonatomic, strong, readonly) QMUICellHeightKeyCache *qmui_keyedHeightCache; +/// 当使用 UITableViewStyleInsetGrouped 时可通过这个属性修改列表的左右缩进值,默认值为 20,也即 iOS 13 系统默认表现。 +@property(nonatomic, assign) CGFloat qmui_insetGroupedHorizontalInset UI_APPEARANCE_SELECTOR; @end -@interface UITableView (QMUICellHeightIndexPathCache) - -@property (nonatomic, strong, readonly) QMUICellHeightIndexPathCache *qmui_indexPathHeightCache; - -@end - -@interface UITableView (QMUIIndexPathHeightCacheInvalidation) - -/// 当需要reloadData的时候,又不想使布局失效,可以调用下面这个方法。例如在底部加载更多。 -- (void)qmui_reloadDataWithoutInvalidateIndexPathHeightCache; - -@end +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.m index e0de7bd7..7850986a 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UITableView+QMUI.m @@ -1,68 +1,375 @@ +/** + * 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. + */ + // // UITableView+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// 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)qmui_styledAsQMUITableView { - UIColor *backgroundColor = nil; - if (self.style == UITableViewStylePlain) { - backgroundColor = TableViewBackgroundColor; ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + 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 result; + }; + }); + + OverrideImplementation([UITableView class], @selector(sizeThatFits:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^CGSize(UITableView *selfObject, CGSize size) { + [selfObject alertEstimatedHeightUsageIfDetected]; + + // call super + CGSize (*originSelectorIMP)(id, SEL, CGSize); + originSelectorIMP = (CGSize (*)(id, SEL, CGSize))originalIMPProvider(); + CGSize result = originSelectorIMP(selfObject, originCMD, size); + + return result; + }; + }); + + 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) { + + 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) { + + // 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 + } + }; + }); + } + + 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 + } + } + } + + // call super + 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]; + } + }; + }); + }); +} + +// 防止 release 版本滚动到不合法的 indexPath 会 crash +- (void)qmui_scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated { + if (!indexPath) { + return; + } + + BOOL isIndexPathLegal = YES; + NSInteger numberOfSections = [self numberOfSections]; + if (indexPath.section >= numberOfSections) { + isIndexPathLegal = NO; + } else if (indexPath.row != NSNotFound) { + NSInteger rows = [self numberOfRowsInSection:indexPath.section]; + isIndexPathLegal = indexPath.row < rows; + } + if (!isIndexPathLegal) { + QMUIAssert(NO, @"UITableView (QMUI)", @"%@ - target indexPath : %@ ,不合法的indexPath。\n%@", self, indexPath, [NSThread callStackSymbols]); } else { - backgroundColor = TableViewGroupedBackgroundColor; + [self qmui_scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; } - if (backgroundColor) { - self.backgroundColor = backgroundColor; +} + +- (void)qmui_styledAsQMUITableView { + + if (!QMUICMIActivated) return; + + [self _qmui_configEstimatedRowHeight]; + + self.backgroundColor = PreferredValueForTableViewStyle(self.style, TableViewBackgroundColor, TableViewGroupedBackgroundColor, TableViewInsetGroupedBackgroundColor); + self.separatorColor = PreferredValueForTableViewStyle(self.style, TableViewSeparatorColor, TableViewGroupedSeparatorColor, TableViewInsetGroupedSeparatorColor); + + + // 去掉空白的cell + if (self.style == UITableViewStylePlain) { + self.tableFooterView = [[UIView alloc] init]; } - self.separatorColor = TableViewSeparatorColor; - self.tableFooterView = [[UIView alloc] init];// 去掉尾部空cell - 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; +} + +- (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 { - if (view && [view isKindOfClass:[UIView class]]) { - CGPoint origin = [self convertPoint:view.frame.origin fromView:view.superview]; - return [self indexPathForRowAtPoint:origin]; + if (!view || !view.superview) { + return nil; } - return nil; + + if ([view isKindOfClass:[UITableViewCell class]] && ([NSStringFromClass(view.superview.class) isEqualToString:@"UITableViewWrapperView"] ? view.superview.superview : view.superview) == self) { + // iOS 11 下,cell.superview 是 UITableView,iOS 11 以前,cell.superview 是 UITableViewWrapperView + return [self indexPathForCell:(UITableViewCell *)view]; + } + + return [self qmui_indexPathForRowAtView:view.superview]; } - (NSInteger)qmui_indexForSectionHeaderAtView:(UIView *)view { + [self alertEstimatedHeightUsageIfDetected]; + if (!view || ![view isKindOfClass:[UIView class]]) { return -1; } CGPoint origin = [self convertPoint:view.frame.origin fromView:view.superview]; - origin = CGPointToFixed(origin, 4); + origin = CGPointToFixed(origin, kFloatValuePrecision);// 避免一些浮点数精度问题导致的计算错误 - NSUInteger numberOfSection = [self numberOfSections]; - for (NSInteger i = numberOfSection - 1; i >= 0; i--) { - CGRect rectForHeader = [self rectForHeaderInSection:i];// 这个接口获取到的 rect 是在 contentSize 里的 rect,而不是实际看到的 rect,所以要自行区分 headerView 是否被停靠在顶部 - BOOL isHeaderViewPinToTop = self.style == UITableViewStylePlain && (CGRectGetMinY(rectForHeader) - self.contentOffset.y < self.contentInset.top); - if (isHeaderViewPinToTop) { - rectForHeader = CGRectSetY(rectForHeader, CGRectGetMinY(rectForHeader) + (self.contentInset.top - CGRectGetMinY(rectForHeader) + self.contentOffset.y)); + 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)) { + 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; } - - rectForHeader = CGRectToFixed(rectForHeader, 4); - if (CGRectContainsPoint(rectForHeader, origin)) { - return i; + } + return -1; +} + +- (NSArray *)qmui_indexForVisibleSectionHeaders { + // 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:@(section)]; + } + } + if (result.count == 0) { + result = nil; + } + return result; +} + +- (NSInteger)qmui_indexOfPinnedSectionHeader { + NSArray *visibleSectionIndex = [self qmui_indexForVisibleSectionHeaders]; + for (NSInteger i = 0; i < visibleSectionIndex.count; i++) { + NSInteger section = visibleSectionIndex[i].integerValue; + if ([self qmui_isHeaderPinnedForSection:section]) { + return section; + } else { + continue; } } return -1; } +- (BOOL)qmui_isHeaderPinnedForSection:(NSInteger)section { + if (self.style != UITableViewStylePlain) return NO; + if (section >= [self numberOfSections]) return NO; + + // 系统这两个接口获取到的 rect 是在 contentSize 里的 rect,而不是实际看到的 rect + CGRect rectForSection = [self rectForSection:section]; + CGRect rectForHeader = [self rectForHeaderInSection: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 (section >= [self numberOfSections]) return NO; + + // 不存在 header 就不用判断 + CGRect rectForSectionHeader = [self rectForHeaderInSection:section]; + if (CGRectGetHeight(rectForSectionHeader) <= 0) return NO; + + // 系统这个接口获取到的 rect 是在 contentSize 里的 rect,而不是实际看到的 rect + 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 { NSInteger numberOfRowsInSection = [self.dataSource tableView:self numberOfRowsInSection:indexPath.section]; if (numberOfRowsInSection == 1) { @@ -78,7 +385,7 @@ - (QMUITableViewCellPosition)qmui_positionForRowAtIndexPath:(NSIndexPath *)index } - (BOOL)qmui_cellVisibleAtIndexPath:(NSIndexPath *)indexPath { - NSArray *visibleCellIndexPaths = self.indexPathsForVisibleRows; + NSArray *visibleCellIndexPaths = self.indexPathsForVisibleRows; for (NSIndexPath *visibleIndexPath in visibleCellIndexPaths) { if ([indexPath isEqual:visibleIndexPath]) { return YES; @@ -88,18 +395,23 @@ - (BOOL)qmui_cellVisibleAtIndexPath:(NSIndexPath *)indexPath { } - (void)qmui_clearsSelection { - NSArray *selectedIndexPaths = [self indexPathsForSelectedRows]; + NSArray *selectedIndexPaths = [self indexPathsForSelectedRows]; for (NSIndexPath *indexPath in selectedIndexPaths) { [self deselectRowAtIndexPath:indexPath animated:YES]; } } - (void)qmui_scrollToRowFittingOffsetY:(CGFloat)offsetY atIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated { + [self alertEstimatedHeightUsageIfDetected]; + if (![self qmui_canScroll]) { return; } CGRect rectForRow = [self rectForRowAtIndexPath:indexPath]; + if (CGRectEqualToRect(rectForRow, CGRectZero)) { + return; + } // 如果要滚到的row在列表尾部,则这个row是不可能滚到顶部的(因为列表尾部已经不够空间了),所以要判断一下 BOOL canScrollRowToTop = CGRectGetMaxY(rectForRow) + CGRectGetHeight(self.frame) - (offsetY + CGRectGetHeight(rectForRow)) <= self.contentSize.height; @@ -110,7 +422,17 @@ - (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]; + if (!self.dataSource || !self.delegate) { return CGSizeZero; } @@ -126,7 +448,7 @@ - (CGSize)qmui_realContentSize { } CGRect lastSectionRect = [self rectForSection:lastSection]; - realContentSize.height = fmaxf(realContentSize.height, CGRectGetMaxY(lastSectionRect)); + realContentSize.height = fmax(realContentSize.height, CGRectGetMaxY(lastSectionRect)); return realContentSize; } @@ -137,268 +459,153 @@ - (BOOL)qmui_canScroll { } if ([self.tableHeaderView isKindOfClass:[UISearchBar class]]) { - BOOL canScroll = self.qmui_realContentSize.height + UIEdgeInsetsGetVerticalValue(self.contentInset) > CGRectGetHeight(self.bounds); + BOOL canScroll = self.qmui_realContentSize.height + UIEdgeInsetsGetVerticalValue(self.adjustedContentInset) > CGRectGetHeight(self.bounds); return canScroll; } else { return [super qmui_canScroll]; } } -@end - - -/// ====================== 计算动态cell高度相关 ======================= - -@implementation UITableView (QMUIKeyedHeightCache) - -- (QMUICellHeightKeyCache *)qmui_keyedHeightCache { - QMUICellHeightKeyCache *cache = objc_getAssociatedObject(self, _cmd); - if (!cache) { - cache = [[QMUICellHeightKeyCache alloc] init]; - objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - return cache; -} - -@end - -@implementation UITableView (QMUICellHeightIndexPathCache) - -- (QMUICellHeightIndexPathCache *)qmui_indexPathHeightCache { - QMUICellHeightIndexPathCache *cache = objc_getAssociatedObject(self, _cmd); - if (!cache) { - cache = [[QMUICellHeightIndexPathCache alloc] init]; - objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - return cache; -} - -@end - -@implementation UITableView (QMUIIndexPathHeightCacheInvalidation) - -- (void)qmui_reloadDataWithoutInvalidateIndexPathHeightCache { - [self qmui_reloadData]; -} - -+ (void)load { - SEL selectors[] = { - @selector(reloadData), - @selector(insertSections:withRowAnimation:), - @selector(deleteSections:withRowAnimation:), - @selector(reloadSections:withRowAnimation:), - @selector(moveSection:toSection:), - @selector(insertRowsAtIndexPaths:withRowAnimation:), - @selector(deleteRowsAtIndexPaths:withRowAnimation:), - @selector(reloadRowsAtIndexPaths:withRowAnimation:), - @selector(moveRowAtIndexPath:toIndexPath:) - }; - for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) { - SEL originalSelector = selectors[index]; - SEL swizzledSelector = NSSelectorFromString([@"qmui_" stringByAppendingString:NSStringFromSelector(originalSelector)]); - Method originalMethod = class_getInstanceMethod(self, originalSelector); - Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector); - method_exchangeImplementations(originalMethod, swizzledMethod); - } -} - -- (void)qmui_reloadData { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - [heightsBySection removeAllObjects]; - }]; +- (void)alertEstimatedHeightUsageIfDetected { + 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]; } - [self qmui_reloadData]; } -- (void)qmui_insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) { - [self.qmui_indexPathHeightCache buildSectionsIfNeeded:section]; - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - [heightsBySection insertObject:[NSMutableArray array] atIndex:section]; - }]; - }]; - } - [self qmui_insertSections:sections withRowAnimation:animation]; +- (void)QMUISymbolicUsingTableViewEstimatedHeightMakeWarning { + QMUILog(@"UITableView (QMUI)", @"当开启了 UITableView 的 estimatedRow(SectionHeader / SectionFooter)Height 功能后,不应该手动修改 contentOffset 和 contentSize,也会影响 contentSize、sizeThatFits:、rectForXxx 等方法的计算,请注意确认当前是否存在不合理的业务代码。可添加 'QMUISymbolicUsingTableViewEstimatedHeightMakeWarning' 的 Symbolic Breakpoint 以捕捉此类信息。"); } -- (void)qmui_deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [sections enumerateIndexesUsingBlock:^(NSUInteger section, BOOL *stop) { - [self.qmui_indexPathHeightCache buildSectionsIfNeeded:section]; - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - [heightsBySection removeObjectAtIndex:section]; - }]; - }]; - } - [self qmui_deleteSections:sections withRowAnimation:animation]; +- (void)qmui_performBatchUpdates:(void (NS_NOESCAPE ^ _Nullable)(void))updates completion:(void (^ _Nullable)(BOOL finished))completion { + [self performBatchUpdates:updates completion:completion]; } -- (void)qmui_reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [sections enumerateIndexesUsingBlock: ^(NSUInteger section, BOOL *stop) { - [self.qmui_indexPathHeightCache buildSectionsIfNeeded:section]; - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - [heightsBySection[section] removeAllObjects]; - }]; - }]; - } - [self qmui_reloadSections:sections withRowAnimation:animation]; +- (CGRect)qmui_indexFrame { + CGRect indexFrame = CGRectZero; + [self qmui_performSelector:NSSelectorFromString(@"indexFrame") withPrimitiveReturnValue:&indexFrame]; + return indexFrame; } -- (void)qmui_moveSection:(NSInteger)section toSection:(NSInteger)newSection { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache buildSectionsIfNeeded:section]; - [self.qmui_indexPathHeightCache buildSectionsIfNeeded:newSection]; - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - [heightsBySection exchangeObjectAtIndex:section withObjectAtIndex:newSection]; - }]; - } - [self qmui_moveSection:section toSection:newSection]; -} +@end -- (void)qmui_insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:indexPaths]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - NSMutableArray *rows = heightsBySection[indexPath.section]; - [rows insertObject:@-1 atIndex:indexPath.row]; - }]; - }]; - } - [self qmui_insertRowsAtIndexPaths:indexPaths withRowAnimation:animation]; -} +@interface UITableViewCell (QMUI_Private) -- (void)qmui_deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:indexPaths]; - NSMutableDictionary *mutableIndexSetsToRemove = [NSMutableDictionary dictionary]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - NSMutableIndexSet *mutableIndexSet = mutableIndexSetsToRemove[@(indexPath.section)]; - if (!mutableIndexSet) { - mutableIndexSet = [NSMutableIndexSet indexSet]; - mutableIndexSetsToRemove[@(indexPath.section)] = mutableIndexSet; - } - [mutableIndexSet addIndex:indexPath.row]; - }]; - [mutableIndexSetsToRemove enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, NSIndexSet *indexSet, BOOL *stop) { - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - NSMutableArray *rows = heightsBySection[key.integerValue]; - [rows removeObjectsAtIndexes:indexSet]; - }]; - }]; - } - [self qmui_deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation]; -} +@property(nonatomic, assign, readwrite) QMUITableViewCellPosition qmui_cellPosition; +@end -- (void)qmui_reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:indexPaths]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - NSMutableArray *rows = heightsBySection[indexPath.section]; - rows[indexPath.row] = @-1; - }]; - }]; - } - [self qmui_reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation]; -} +@implementation UITableView (QMUI_InsetGrouped) -- (void)qmui_moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { - if (self.qmui_indexPathHeightCache.automaticallyInvalidateEnabled) { - [self.qmui_indexPathHeightCache buildCachesAtIndexPathsIfNeeded:@[sourceIndexPath, destinationIndexPath]]; - [self.qmui_indexPathHeightCache enumerateAllOrientationsUsingBlock:^(NSMutableArray *heightsBySection) { - if (heightsBySection.count > 0 && heightsBySection.count > sourceIndexPath.section && heightsBySection.count > destinationIndexPath.section) { - NSMutableArray *sourceRows = heightsBySection[sourceIndexPath.section]; - NSMutableArray *destinationRows = heightsBySection[destinationIndexPath.section]; - NSNumber *sourceValue = sourceRows[sourceIndexPath.row]; - NSNumber *destinationValue = destinationRows[destinationIndexPath.row]; - sourceRows[sourceIndexPath.row] = destinationValue; - destinationRows[destinationIndexPath.row] = sourceValue; - } - }]; - } - [self qmui_moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; ++ (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; + }; + }); + }); } -@end - -@implementation UITableView (QMUILayoutCell) - -- (UITableViewCell *)templateCellForReuseIdentifier:(NSString *)identifier { - NSAssert(identifier.length > 0, @"Expect a valid identifier - %@", identifier); - NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd); - if (!templateCellsByIdentifiers) { - templateCellsByIdentifiers = @{}.mutableCopy; - objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - UITableViewCell *templateCell = templateCellsByIdentifiers[identifier]; - if (!templateCell) { - // 是否有通过dataSource返回的cell - if ([self.dataSource respondsToSelector:@selector(qmui_tableView:cellWithIdentifier:)] ) { - id dataSource = (id)self.dataSource; - templateCell = [dataSource qmui_tableView:self cellWithIdentifier:identifier]; - } - // 没有的话,则需要通过register来注册一个cell,否则会crash - if (!templateCell) { - templateCell = [self dequeueReusableCellWithIdentifier:identifier]; - NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier); - } - templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO; - templateCellsByIdentifiers[identifier] = templateCell; - NSLog(@"layout cell created - %@", identifier); +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]; } - return templateCell; } -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(__kindof UITableViewCell *))configuration { - if (!identifier || CGRectIsEmpty(self.bounds)) { - return 0; - } - UITableViewCell *cell = [self templateCellForReuseIdentifier:identifier]; - [cell prepareForReuse]; - if (configuration) { configuration(cell); } - CGFloat contentWidth = CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.contentInset); - CGSize fitSize = CGSizeZero; - if (cell && contentWidth > 0) { - SEL selector = @selector(sizeThatFits:); - BOOL inherited = ![cell isMemberOfClass:[UITableViewCell class]]; // 是否UITableViewCell - BOOL overrided = [cell.class instanceMethodForSelector:selector] != [UITableViewCell instanceMethodForSelector:selector]; // 是否重写了sizeThatFit: - if (inherited && !overrided) { - NSAssert(NO, @"Customized cell must override '-sizeThatFits:' method if not using auto layout."); - } - fitSize = [cell sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)]; +- (CGFloat)qmui_insetGroupedCornerRadius { + NSNumber *associatedValue = (NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_insetGroupedCornerRadius); + if (!associatedValue) { + // 从来没设置过(包括业务主动设置或者通过 UIAppearance 方式设置),则用 iOS 13 系统默认值 + // 不在 UITableView init 时设置是因为那样会使 UIAppearance 失效 + return 10; } - return ceil(fitSize.height); + return associatedValue.qmui_CGFloatValue; } -// 通过indexPath缓存高度 -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UITableViewCell *))configuration { - if (!identifier || !indexPath || CGRectIsEmpty(self.bounds)) { - return 0; +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]; } - if ([self.qmui_indexPathHeightCache existsHeightAtIndexPath:indexPath]) { - return [self.qmui_indexPathHeightCache heightForIndexPath:indexPath]; - } - CGFloat height = [self qmui_heightForCellWithIdentifier:identifier configuration:configuration]; - [self.qmui_indexPathHeightCache cacheHeight:height byIndexPath:indexPath]; - return height; } -// 通过key缓存高度 -- (CGFloat)qmui_heightForCellWithIdentifier:(NSString *)identifier cacheByKey:(id)key configuration:(void (^)(__kindof UITableViewCell *))configuration { - if (!identifier || !key || CGRectIsEmpty(self.bounds)) { - return 0; - } - if ([self.qmui_keyedHeightCache existsHeightForKey:key]) { - return [self.qmui_keyedHeightCache heightForKey:key]; +- (CGFloat)qmui_insetGroupedHorizontalInset { + NSNumber *associatedValue = (NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_insetGroupedHorizontalInset); + if (!associatedValue) { + // 从来没设置过(包括业务主动设置或者通过 UIAppearance 方式设置),则用 iOS 13 系统默认值 + // 不在 UITableView init 时设置是因为那样会使 UIAppearance 失效 + return PreferredValueForVisualDevice(20, 15); } - CGFloat height = [self qmui_heightForCellWithIdentifier:identifier configuration:configuration]; - [self.qmui_keyedHeightCache cacheHeight:height byKey:key]; - return height; + return associatedValue.qmui_CGFloatValue; } @end - diff --git a/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.h new file mode 100644 index 00000000..be562302 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.h @@ -0,0 +1,100 @@ +/** + * 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. + */ + +// +// UITableViewCell+QMUI.h +// QMUIKit +// +// Created by QMUI Team on 2018/7/5. +// + +#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, nullable) void (^qmui_setHighlightedBlock)(BOOL highlighted, BOOL animated); + +/// setSelected:animated: 方法的回调 block +@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; + +@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 new file mode 100644 index 00000000..e482082b --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UITableViewCell+QMUI.m @@ -0,0 +1,507 @@ +/** + * 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. + */ + +// +// UITableViewCell+QMUI.m +// QMUIKit +// +// Created by QMUI Team on 2018/7/5. +// + +#import "UITableViewCell+QMUI.h" +#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)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); + } + }); + + ExtendImplementationOfVoidMethodWithTwoArguments([UITableViewCell class], @selector(setSelected:animated:), BOOL, BOOL, ^(UITableViewCell *selfObject, BOOL selected, BOOL animated) { + if (selfObject.qmui_setSelectedBlock) { + 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 qmui_valueForKey:@"_editingAccessoryView"]; + } + if (self.accessoryView) { + return self.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 a63e0eab..5d4ced27 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.h @@ -1,31 +1,49 @@ +/** + * 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. + */ + // // UITextField+QMUI.h // qmui // -// Created by zhoonchen on 2017/3/29. -// Copyright © 2017年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2017/3/29. // #import #import -@class QMUIKeyboardManager; -@class QMUIKeyboardUserInfo; +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; -/// 键盘相关block,搭配QMUIKeyboardManager一起使用 +/// 输入框右边的 clearButton,在 UITextField 初始化后就存在 +@property(nullable, nonatomic, weak, readonly) UIButton *qmui_clearButton; -@property(nonatomic, copy) void (^qmui_keyboardWillShowNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); -@property(nonatomic, copy) void (^qmui_keyboardWillHideNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); -@property(nonatomic, copy) void (^qmui_keyboardWillChangeFrameNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); -@property(nonatomic, copy) void (^qmui_keyboardDidShowNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); -@property(nonatomic, copy) void (^qmui_keyboardDidHideNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); -@property(nonatomic, copy) void (^qmui_keyboardDidChangeFrameNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); +/// 自定义 clearButton 的图片,设置成nil,恢复到系统默认的图片 +@property(nullable, nonatomic, strong) UIImage *qmui_clearButtonImage UI_APPEARANCE_SELECTOR; -@property(nonatomic, strong, readonly) QMUIKeyboardManager *qmui_keyboardManager; +/** + * 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 f00a1015..3ac2ea59 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UITextField+QMUI.m @@ -1,144 +1,122 @@ +/** + * 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. + */ + // // UITextField+QMUI.m // qmui // -// Created by zhoonchen on 2017/3/29. -// Copyright © 2017年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2017/3/29. // #import "UITextField+QMUI.h" +#import "NSObject+QMUI.h" #import "QMUICore.h" -#import "QMUIKeyboardManager.h" - -@interface UITextField () - -@end +#import "UIImage+QMUI.h" @implementation UITextField (QMUI) -- (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); -} - -- (void)setQmui_keyboardWillShowNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardWillShowNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardWillShowNotificationBlock), keyboardWillShowNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardWillShowNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillShowNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardDidShowNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardDidShowNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardDidShowNotificationBlock), keyboardDidShowNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardDidShowNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidShowNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardWillHideNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardWillHideNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardWillHideNotificationBlock), keyboardWillHideNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardWillHideNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillHideNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardDidHideNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardDidHideNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardDidHideNotificationBlock), keyboardDidHideNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardDidHideNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidHideNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardWillChangeFrameNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardWillChangeFrameNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardWillChangeFrameNotificationBlock), keyboardWillChangeFrameNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardWillChangeFrameNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillChangeFrameNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardDidChangeFrameNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardDidChangeFrameNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardDidChangeFrameNotificationBlock), keyboardDidChangeFrameNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardDidChangeFrameNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidChangeFrameNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardManager:(QMUIKeyboardManager *)keyboardManager { - objc_setAssociatedObject(self, @selector(qmui_keyboardManager), keyboardManager, OBJC_ASSOCIATION_RETAIN_NONATOMIC); ++ (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]; } -- (QMUIKeyboardManager *)qmui_keyboardManager { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)initKeyboardManagerIfNeeded { - if (!self.qmui_keyboardManager) { - self.qmui_keyboardManager = [[QMUIKeyboardManager alloc] initWithDelegate:self]; - [self.qmui_keyboardManager addTargetResponder:self]; - } -} - -#pragma mark - - -- (void)keyboardWillShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardWillShowNotificationBlock) { - self.qmui_keyboardWillShowNotificationBlock(keyboardUserInfo); - } +- (NSRange)qmui_selectedRange { + return [self qmui_convertNSRangeFromUITextRange:self.selectedTextRange]; } -- (void)keyboardWillHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardWillHideNotificationBlock) { - self.qmui_keyboardWillHideNotificationBlock(keyboardUserInfo); - } +- (UIButton *)qmui_clearButton { + return [self qmui_valueForKey:@"clearButton"]; } -- (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardWillChangeFrameNotificationBlock) { - self.qmui_keyboardWillChangeFrameNotificationBlock(keyboardUserInfo); +static char kAssociatedObjectKey_clearButtonImage; +- (void)setQmui_clearButtonImage:(UIImage *)qmui_clearButtonImage { + 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]; + } } } -- (void)keyboardDidShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardDidShowNotificationBlock) { - self.qmui_keyboardDidShowNotificationBlock(keyboardUserInfo); - } +- (UIImage *)qmui_clearButtonImage { + return (UIImage *)objc_getAssociatedObject(self, &kAssociatedObjectKey_clearButtonImage); } -- (void)keyboardDidHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardDidHideNotificationBlock) { - self.qmui_keyboardDidHideNotificationBlock(keyboardUserInfo); - } +- (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); } -- (void)keyboardDidChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardDidChangeFrameNotificationBlock) { - self.qmui_keyboardDidChangeFrameNotificationBlock(keyboardUserInfo); +- (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 7b168b61..d28423e1 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.h @@ -1,24 +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. + */ + // // UITextView+QMUI.h // qmui // -// Created by zhoonchen on 2017/3/29. -// Copyright © 2017年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2017/3/29. // #import #import -@class QMUIKeyboardManager; -@class QMUIKeyboardUserInfo; +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] */ - (NSRange)qmui_convertNSRangeFromUITextRange:(UITextRange *)textRange; +/** + * convert NSRange to UITextRange + * @return return nil if range is invalidate. + */ +- (nullable UITextRange *)qmui_convertUITextRangeFromNSRange:(NSRange)range; + /** * 设置 text 会让 selectedTextRange 跳到最后一个字符,导致在中间修改文字后光标会跳到末尾,所以设置前要保存一下,设置后恢复过来 */ @@ -30,19 +52,17 @@ - (void)qmui_setAttributedTextKeepingSelectedRange:(NSAttributedString *)attributedText; /** - * [UITextView scrollRangeToVisible:] 并不会考虑 textContainerInset.bottom,所以使用这个方法来代替 - */ -- (void)qmui_scrollCaretVisibleAnimated:(BOOL)animated; - -/// 键盘相关block,搭配QMUIKeyboardManager一起使用 + [UITextView scrollRangeToVisible:] 并不会考虑 textContainerInset.bottom,所以使用这个方法来代替 -@property(nonatomic, copy) void (^qmui_keyboardWillShowNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); -@property(nonatomic, copy) void (^qmui_keyboardWillHideNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); -@property(nonatomic, copy) void (^qmui_keyboardWillChangeFrameNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); -@property(nonatomic, copy) void (^qmui_keyboardDidShowNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); -@property(nonatomic, copy) void (^qmui_keyboardDidHideNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); -@property(nonatomic, copy) void (^qmui_keyboardDidChangeFrameNotificationBlock)(QMUIKeyboardUserInfo *keyboardUserInfo); + @param range 要滚动到的文字区域,如果 range 非法则什么都不做 + */ +- (void)qmui_scrollRangeToVisible:(NSRange)range; -@property(nonatomic, strong, readonly) QMUIKeyboardManager *qmui_keyboardManager; +/** + * 将光标滚到可视区域 + */ +- (void)qmui_scrollCaretVisibleAnimated:(BOOL)animated; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.m index 1d3a21d4..63b4ac13 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UITextView+QMUI.m @@ -1,20 +1,60 @@ +/** + * 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. + */ + // // UITextView+QMUI.m // qmui // -// Created by zhoonchen on 2017/3/29. -// Copyright © 2017年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2017/3/29. // #import "UITextView+QMUI.h" #import "QMUICore.h" -#import "QMUIKeyboardManager.h" +#import "UIScrollView+QMUI.h" -@interface UITextView () +@implementation UITextView (QMUI) -@end +#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 + } +} -@implementation UITextView (QMUI) +- (NSRange)qmui_selectedRange { + return [self qmui_convertNSRangeFromUITextRange:self.selectedTextRange]; +} - (NSRange)qmui_convertNSRangeFromUITextRange:(UITextRange *)textRange { NSInteger location = [self offsetFromPosition:self.beginningOfDocument toPosition:textRange.start]; @@ -22,6 +62,16 @@ - (NSRange)qmui_convertNSRangeFromUITextRange:(UITextRange *)textRange { 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]; +} + - (void)qmui_setTextKeepingSelectedRange:(NSString *)text { UITextRange *selectedTextRange = self.selectedTextRange; self.text = text; @@ -34,143 +84,62 @@ - (void)qmui_setAttributedTextKeepingSelectedRange:(NSAttributedString *)attribu self.selectedTextRange = selectedTextRange; } -- (void)qmui_scrollCaretVisibleAnimated:(BOOL)animated { - if (CGRectIsEmpty(self.bounds)) { - return; - } +- (void)qmui_scrollRangeToVisible:(NSRange)range { + if (CGRectIsEmpty(self.bounds)) return; - CGRect caretRect = [self caretRectForPosition:self.selectedTextRange.end]; - CGFloat contentOffsetY = self.contentOffset.y; - if (CGRectGetMinY(caretRect) < self.contentOffset.y + self.textContainerInset.top) { - // 光标在可视区域上方,往下滚动 - contentOffsetY = CGRectGetMinY(caretRect) - self.textContainerInset.top - self.contentInset.top; - } else if (CGRectGetMaxY(caretRect) > self.contentOffset.y + CGRectGetHeight(self.bounds) - self.textContainerInset.bottom - self.contentInset.bottom) { - // 光标在可视区域下方,往上滚动 - contentOffsetY = CGRectGetMaxY(caretRect) - CGRectGetHeight(self.bounds) + self.textContainerInset.bottom + self.contentInset.bottom; - } else { - // 光标在可视区域内,不用调整 - return; - } - [self setContentOffset:CGPointMake(self.contentOffset.x, contentOffsetY) animated:animated]; -} - -- (void)setQmui_keyboardWillShowNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardWillShowNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardWillShowNotificationBlock), keyboardWillShowNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardWillShowNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillShowNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardDidShowNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardDidShowNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardDidShowNotificationBlock), keyboardDidShowNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardDidShowNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidShowNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardWillHideNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardWillHideNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardWillHideNotificationBlock), keyboardWillHideNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardWillHideNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillHideNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardDidHideNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardDidHideNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardDidHideNotificationBlock), keyboardDidHideNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardDidHideNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidHideNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardWillChangeFrameNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardWillChangeFrameNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardWillChangeFrameNotificationBlock), keyboardWillChangeFrameNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardWillChangeFrameNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardWillChangeFrameNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardDidChangeFrameNotificationBlock:(void (^)(QMUIKeyboardUserInfo *))keyboardDidChangeFrameNotificationBlock { - objc_setAssociatedObject(self, @selector(qmui_keyboardDidChangeFrameNotificationBlock), keyboardDidChangeFrameNotificationBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); - if (keyboardDidChangeFrameNotificationBlock) { - [self initKeyboardManagerIfNeeded]; - } -} - -- (void (^)(QMUIKeyboardUserInfo *))qmui_keyboardDidChangeFrameNotificationBlock { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)setQmui_keyboardManager:(QMUIKeyboardManager *)keyboardManager { - objc_setAssociatedObject(self, @selector(qmui_keyboardManager), keyboardManager, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (QMUIKeyboardManager *)qmui_keyboardManager { - return objc_getAssociatedObject(self, _cmd); -} - -- (void)initKeyboardManagerIfNeeded { - if (!self.qmui_keyboardManager) { - self.qmui_keyboardManager = [[QMUIKeyboardManager alloc] initWithDelegate:self]; - [self.qmui_keyboardManager addTargetResponder:self]; - } -} - -#pragma mark - - -- (void)keyboardWillShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardWillShowNotificationBlock) { - self.qmui_keyboardWillShowNotificationBlock(keyboardUserInfo); - } -} - -- (void)keyboardWillHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardWillHideNotificationBlock) { - self.qmui_keyboardWillHideNotificationBlock(keyboardUserInfo); + UITextRange *textRange = [self qmui_convertUITextRangeFromNSRange:range]; + if (!textRange) return; + + NSArray *selectionRects = [self selectionRectsForRange:textRange]; + CGRect rect = CGRectZero; + for (UITextSelectionRect *selectionRect in selectionRects) { + if (!CGRectIsEmpty(selectionRect.rect)) { + if (CGRectIsEmpty(rect)) { + rect = selectionRect.rect; + } else { + rect = CGRectUnion(rect, selectionRect.rect); + } + } } -} - -- (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardWillChangeFrameNotificationBlock) { - self.qmui_keyboardWillChangeFrameNotificationBlock(keyboardUserInfo); + if (!CGRectIsEmpty(rect)) { + rect = [self convertRect:rect fromView:self.textInputView]; + [self _scrollRectToVisible:rect animated:YES]; } } -- (void)keyboardDidShowWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardDidShowNotificationBlock) { - self.qmui_keyboardDidShowNotificationBlock(keyboardUserInfo); - } +- (void)qmui_scrollCaretVisibleAnimated:(BOOL)animated { + if (CGRectIsEmpty(self.bounds)) return; + + CGRect caretRect = [self caretRectForPosition:self.selectedTextRange.end]; + [self _scrollRectToVisible:caretRect animated:animated]; } -- (void)keyboardDidHideWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardDidHideNotificationBlock) { - self.qmui_keyboardDidHideNotificationBlock(keyboardUserInfo); +- (void)_scrollRectToVisible:(CGRect)rect animated:(BOOL)animated { + // scrollEnabled 为 NO 时可能产生不合法的 rect 值 https://github.com/Tencent/QMUI_iOS/issues/205 + if (!CGRectIsValidated(rect)) { + return; } -} - -- (void)keyboardDidChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { - if (self.qmui_keyboardDidChangeFrameNotificationBlock) { - self.qmui_keyboardDidChangeFrameNotificationBlock(keyboardUserInfo); + + CGFloat contentOffsetY = self.contentOffset.y; + + 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 { + contentOffsetY = -self.adjustedContentInset.top; } + [self setContentOffset:CGPointMake(self.contentOffset.x, contentOffsetY) animated:animated]; } @end diff --git a/QMUI/QMUIKit/UIKitExtensions/UIToolbar+QMUI.h b/QMUI/QMUIKit/UIKitExtensions/UIToolbar+QMUI.h new file mode 100644 index 00000000..5659f140 --- /dev/null +++ b/QMUI/QMUIKit/UIKitExtensions/UIToolbar+QMUI.h @@ -0,0 +1,23 @@ +/** + * 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.h +// QMUIKit +// +// Created by MoLice on 2021/N/24. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@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 b195cba0..e72e3f7d 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.h @@ -1,67 +1,146 @@ +/** + * 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+QMUI.h // qmui // -// Created by QQMail on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import +#import +#import "UIView+QMUIBorder.h" + +NS_ASSUME_NONNULL_BEGIN @interface UIView (QMUI) /** - * 相当于 initWithFrame:CGRectMake(0, 0, size.width, size.height) + 相当于 initWithFrame:CGRectMake(0, 0, size.width, size.height) + + @param size 初始化时的 size + @return 初始化得到的实例 */ - (instancetype)qmui_initWithSize:(CGSize)size; /** - * 设置view的width和height + 将要设置的 frame 用 CGRectApplyAffineTransformWithAnchorPoint 处理后再设置 + 注意这个方式会导致 self.bounds 也受 transform 的影响(系统默认行为是 frame 受 transform 影响,center 和 bounds 不会),如果有需要访问 self.bounds 的情况,请避免使用这个方式。 */ -- (void)qmui_setWidth:(CGFloat)width height:(CGFloat)height; +@property(nonatomic, assign) CGRect qmui_frameApplyTransform; /** - * 设置view的width + 在 iOS 11 及之后的版本,此属性将返回系统已有的 self.safeAreaInsets。在之前的版本此属性返回 UIEdgeInsetsZero */ -- (void)qmui_setWidth:(CGFloat)width; +@property(nonatomic, assign, readonly) UIEdgeInsets qmui_safeAreaInsets DEPRECATED_MSG_ATTRIBUTE("请使用系统的 UIView.safeAreaInsets,QMUI 4.4.0 已不再支持 iOS 10,没必要提供该兼容性之的接口了,后续会删除。"); /** - * 设置view的height + 有修改过 tintColor,则不会再受 superview.tintColor 的影响 */ -- (void)qmui_setHeight:(CGFloat)height; +@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; /** - * 设置view的x和y + 移除当前所有 subviews */ -- (void)qmui_setOriginX:(CGFloat)x y:(CGFloat)y; +- (void)qmui_removeAllSubviews; + +/// 同 [UIView convertPoint:toView:],但支持在分属两个不同 window 的 view 之间进行坐标转换,也支持参数 view 直接传一个 window。 +- (CGPoint)qmui_convertPoint:(CGPoint)point toView:(nullable UIView *)view; + +/// 同 [UIView convertPoint:fromView:],但支持在分属两个不同 window 的 view 之间进行坐标转换,也支持参数 view 直接传一个 window。 +- (CGPoint)qmui_convertPoint:(CGPoint)point fromView:(nullable UIView *)view; + +/// 同 [UIView convertRect:toView:],但支持在分属两个不同 window 的 view 之间进行坐标转换,也支持参数 view 直接传一个 window。 +- (CGRect)qmui_convertRect:(CGRect)rect toView:(nullable UIView *)view; + +/// 同 [UIView convertRect:fromView:],但支持在分属两个不同 window 的 view 之间进行坐标转换,也支持参数 view 直接传一个 window。 +- (CGRect)qmui_convertRect:(CGRect)rect fromView:(nullable UIView *)view; + ++ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion; ++ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion; ++ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration animations:(void (^ __nullable)(void))animations; ++ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion; +@end + + +@interface UIView (QMUI_Block) /** - * 设置view的x + 在 UIView 的 frame 变化前会调用这个 block,变化途径包括 setFrame:、setBounds:、setCenter:、setTransform:,你可以通过返回一个 rect 来达到修改 frame 的目的,最终执行 [super setFrame:] 时会使用这个 block 的返回值(除了 setTransform: 导致的 frame 变化)。 + @param view 当前的 view 本身,方便使用,省去 weak 操作 + @param followingFrame setFrame: 的参数 frame,也即即将被修改为的 rect 值 + @return 将会真正被使用的 frame 值 + @note 仅当 followingFrame 和 self.frame 值不相等时才会被调用 */ -- (void)qmui_setOriginX:(CGFloat)x; +@property(nullable, nonatomic, copy) CGRect (^qmui_frameWillChangeBlock)(__kindof UIView *view, CGRect followingFrame); /** - * 设置view的y + 在 UIView 的 frame 变化后会调用这个 block,变化途径包括 setFrame:、setBounds:、setCenter:、setTransform:,可用于监听布局的变化,或者在不方便重写 layoutSubviews 时使用这个 block 代替。 + @param view 当前的 view 本身,方便使用,省去 weak 操作 + @param precedingFrame 修改前的 frame 值 */ -- (void)qmui_setOriginY:(CGFloat)y; +@property(nullable, nonatomic, copy) void (^qmui_frameDidChangeBlock)(__kindof UIView *view, CGRect precedingFrame); /** - * 获取当前view在superview内的水平居中时的minX + 在 - [UIView layoutSubviews] 调用后就调用的 block + @param view 当前的 view 本身,方便使用,省去 weak 操作 */ -- (CGFloat)qmui_minXWhenCenterInSuperview; +@property(nullable, nonatomic, copy) void (^qmui_layoutSubviewsBlock)(__kindof UIView *view); /** - * 获取当前view在superview内的垂直居中时的minX + 在 UIView 的 sizeThatFits: 调用后就调用的 block,可返回一个修改后的值来作为原方法的返回值 + @param view 当前的 view 本身,方便使用,省去 weak 操作 + @param size sizeThatFits: 方法被调用时传进来的参数 size + @param superResult 原本的 sizeThatFits: 方法的返回值 */ -- (CGFloat)qmui_minYWhenCenterInSuperview; +@property(nullable, nonatomic, copy) CGSize (^qmui_sizeThatFitsBlock)(__kindof UIView *view, CGSize size, CGSize superResult); -- (void)qmui_removeAllSubviews; +/** + 当 tintColorDidChange 被调用的时候会调用这个 block,就不用重写方法了 + @param view 当前的 view 本身,方便使用,省去 weak 操作 + */ +@property(nullable, nonatomic, copy) void (^qmui_tintColorDidChangeBlock)(__kindof UIView *view); + +/** + 当 hitTest:withEvent: 被调用时会调用这个 block,就不用重写方法了 + @param point 事件产生的 point + @param event 事件 + @param originalView super 的返回结果 + */ +@property(nullable, nonatomic, copy) __kindof UIView * _Nullable (^qmui_hitTestBlock)(CGPoint point, UIEvent * _Nullable event, __kindof UIView * _Nullable originalView); -+ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion; -+ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion; -+ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration animations:(void (^)(void))animations; @end +@interface UIView (QMUI_ViewController) + +/** + 判断当前的 view 是否属于可视(可视的定义为已存在于 view 层级树里,或者在所处的 UIViewController 的 [viewWillAppear, viewWillDisappear) 生命周期之间) + */ +@property(nonatomic, assign, readonly) BOOL qmui_visible; + +/** + 当前的 view 是否是某个 UIViewController.view + */ +@property(nonatomic, assign) BOOL qmui_isControllerRootView; + +/** + 获取当前 view 所在的 UIViewController,会递归查找 superview,因此注意使用场景不要有过于频繁的调用 + */ +@property(nullable, nonatomic, weak, readonly) __kindof UIViewController *qmui_viewController; +@end + + @interface UIView (QMUI_Runtime) /** @@ -70,63 +149,118 @@ * @return YES 表示当前类重写了指定的方法,NO 表示没有重写,使用的是 UIView 默认的实现 */ - (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector; + @end /** - * Debug UIView 的时候用,对某个 view 的 subviews 都添加一个半透明的背景色,方面查看 view 的布局情况 + * 方便地将某个 UIView 截图并转成一个 UIImage,注意如果这个 UIView 本身做了 transform,也不会在截图上反映出来,截图始终都是原始 UIView 的截图。 */ -@interface UIView (QMUI_Debug) +@interface UIView (QMUI_Snapshotting) -/// 是否需要添加debug背景色,默认NO -@property(nonatomic, assign) BOOL qmui_shouldShowDebugColor; -/// 是否每个view的背景色随机,如果不随机则统一使用半透明红色,默认NO -@property(nonatomic, assign) BOOL qmui_needsDifferentDebugColor; -/// 标记一个view是否已经被添加了debug背景色,外部一般不使用 -@property(nonatomic, assign, readonly) BOOL qmui_hasDebugColor; +- (UIImage *)qmui_snapshotLayerImage; +- (UIImage *)qmui_snapshotImageAfterScreenUpdates:(BOOL)afterScreenUpdates; @end -typedef NS_OPTIONS(NSUInteger, QMUIBorderViewPosition) { - QMUIBorderViewPositionNone = 0, - QMUIBorderViewPositionTop = 1 << 0, - QMUIBorderViewPositionLeft = 1 << 1, - QMUIBorderViewPositionBottom = 1 << 2, - QMUIBorderViewPositionRight = 1 << 3 -}; +/** + 当某个 UIView 在 setFrame: 时高度传这个值,则会自动将 sizeThatFits 算出的高度设置为当前 view 的高度,相当于以下这段代码的简化: + @code + // 以前这么写 + CGSize size = [view sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)]; + view.frame = CGRectMake(x, y, width, size.height); + + // 现在可以这么写: + view.frame = CGRectMake(x, y, width, QMUIViewSelfSizingHeight); + @endcode + */ +extern const CGFloat QMUIViewSelfSizingHeight; /** - * UIView (QMUI_Border) 为 UIView 方便地显示某几个方向上的边框。 - * - * 系统的默认实现里,要为 UIView 加边框一般是通过 view.layer 来实现,view.layer 会给四条边都加上边框,如果你只想为其中某几条加上边框就很麻烦,于是 UIView (QMUI_Border) 提供了 qmui_borderPosition 来解决这个问题。 - * @warning 注意如果你需要为 UIView 四条边都加上边框,请使用系统默认的 view.layer 来实现,而不要用 UIView (QMUI_Border),会浪费资源,这也是为什么 QMUIBorderViewPosition 不提供一个 QMUIBorderViewPositionAll 枚举值的原因。 + * 对 view.frame 操作的简便封装,注意 view 与 view 之间互相计算时,需要保证处于同一个坐标系内。 */ -@interface UIView (QMUI_Border) +@interface UIView (QMUI_Layout) + +/// 等价于 CGRectGetMinY(frame) +@property(nonatomic, assign) CGFloat qmui_top; + +/// 等价于 CGRectGetMinX(frame) +@property(nonatomic, assign) CGFloat qmui_left; + +/// 等价于 CGRectGetMaxY(frame) +@property(nonatomic, assign) CGFloat qmui_bottom; + +/// 等价于 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; + +/// 保持其他三个边缘的位置不变的情况下,将左边缘拓展到某个指定的位置,注意宽度会跟随变化。 +@property(nonatomic, assign) CGFloat qmui_extendToLeft; -/// 设置边框类型,支持组合,例如:`borderType = QMUIBorderViewTypeTop|QMUIBorderViewTypeBottom` -@property(nonatomic, assign) QMUIBorderViewPosition qmui_borderPosition; +/// 保持其他三个边缘的位置不变的情况下,将底边缘拓展到某个指定的位置,注意高度会跟随变化。 +@property(nonatomic, assign) CGFloat qmui_extendToBottom; -/// 边框的大小,默认为PixelOne -@property(nonatomic, assign) IBInspectable CGFloat qmui_borderWidth; +/// 保持其他三个边缘的位置不变的情况下,将右边缘拓展到某个指定的位置,注意宽度会跟随变化。 +@property(nonatomic, assign) CGFloat qmui_extendToRight; -/// 边框的颜色,默认为UIColorSeparator -@property(nonatomic, strong) IBInspectable UIColor *qmui_borderColor; +/// 获取当前 view 在 superview 内水平居中时的 left +@property(nonatomic, assign, readonly) CGFloat qmui_leftWhenCenterInSuperview; -/// 虚线 : dashPhase默认是0,且当dashPattern设置了才有效 -@property(nonatomic, assign) CGFloat qmui_dashPhase; -@property(nonatomic, copy) NSArray *qmui_dashPattern; +/// 获取当前 view 在 superview 内垂直居中时的 top +@property(nonatomic, assign, readonly) CGFloat qmui_topWhenCenterInSuperview; -/// border的layer -@property(nonatomic, strong, readonly) CAShapeLayer *qmui_borderLayer; +@end + + +@interface UIView (CGAffineTransform) + +/// 获取当前 view 的 transform scale x +@property(nonatomic, assign, readonly) CGFloat qmui_scaleX; + +/// 获取当前 view 的 transform scale y +@property(nonatomic, assign, readonly) CGFloat qmui_scaleY; + +/// 获取当前 view 的 transform translation x +@property(nonatomic, assign, readonly) CGFloat qmui_translationX; + +/// 获取当前 view 的 transform translation y +@property(nonatomic, assign, readonly) CGFloat qmui_translationY; @end /** - * 方便地将某个 UIView 截图并转成一个 UIImage,注意如果这个 UIView 本身做了 transform,也不会在截图上反映出来,截图始终都是原始 UIView 的截图。 + * Debug UIView 的时候用,对某个 view 的 subviews 都添加一个半透明的背景色,方面查看 view 的布局情况 */ -@interface UIView (QMUI_Snapshotting) +@interface UIView (QMUI_Debug) + +/// 是否需要添加debug背景色,默认NO +@property(nonatomic, assign) BOOL qmui_shouldShowDebugColor; +/// 是否每个view的背景色随机,如果不随机则统一使用半透明红色,默认NO +@property(nonatomic, assign) BOOL qmui_needsDifferentDebugColor; -- (UIImage *)qmui_snapshotLayerImage; -- (UIImage *)qmui_snapshotImageAfterScreenUpdates:(BOOL)afterScreenUpdates; @end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.m index 1fff0287..faf17139 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIView+QMUI.m @@ -1,85 +1,229 @@ +/** + * 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+QMUI.m // qmui // -// Created by ZhoonChen on 15/7/20. -// Copyright (c) 2015年 QMUI Team. All rights reserved. +// Created by QMUI Team on 15/7/20. // #import "UIView+QMUI.h" #import "QMUICore.h" -#import "CALayer+QMUI.h" #import "UIColor+QMUI.h" #import "NSObject+QMUI.h" #import "UIImage+QMUI.h" +#import "NSNumber+QMUI.h" +#import "UIViewController+QMUI.h" +#import "QMUILog.h" +#import "QMUIWeakObjectContainer.h" -@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) -@implementation UIView (QMUI) ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + 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, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, tintColor); + + selfObject.qmui_tintColorCustomized = !!tintColor; + }; + }); + + // 这个私有方法在 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]; + } + }); + } + }); + }); +} - (instancetype)qmui_initWithSize:(CGSize)size { return [self initWithFrame:CGRectMakeWithSize(size)]; } -- (void)qmui_setWidth:(CGFloat)width height:(CGFloat)height { - CGRect frame = self.frame; - frame.size.height = height; - frame.size.width = width; - self.frame = frame; +- (void)setQmui_frameApplyTransform:(CGRect)qmui_frameApplyTransform { + self.frame = CGRectApplyAffineTransformWithAnchorPoint(qmui_frameApplyTransform, self.transform, self.layer.anchorPoint); +} + +- (CGRect)qmui_frameApplyTransform { + return self.frame; } -- (void)qmui_setWidth:(CGFloat)width { - CGRect frame = self.frame; - frame.size.width = width; - self.frame = frame; +- (UIEdgeInsets)qmui_safeAreaInsets { + return self.safeAreaInsets; } -- (void)qmui_setHeight:(CGFloat)height { - CGRect frame = self.frame; - frame.size.height = height; - self.frame = frame; +- (void)qmui_removeAllSubviews { + [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } -- (void)qmui_setOriginX:(CGFloat)x y:(CGFloat)y { - CGRect frame = self.frame; - frame.origin.x = x; - frame.origin.y = y; - self.frame = frame; +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"]; + } + } } -- (void)qmui_setOriginX:(CGFloat)x { - CGRect frame = self.frame; - frame.origin.x = x; - self.frame = frame; +- (UIEdgeInsets)qmui_outsideEdge { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_outsideEdge)) UIEdgeInsetsValue]; } -- (void)qmui_setOriginY:(CGFloat)y { - CGRect frame = self.frame; - frame.origin.y = y; - self.frame = frame; +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"]; + } } -- (CGFloat)qmui_minXWhenCenterInSuperview { - return CGFloatGetCenter(CGRectGetWidth(self.superview.bounds), CGRectGetWidth(self.frame)); +- (void (^)(__kindof UIView * _Nonnull))qmui_tintColorDidChangeBlock { + return (void (^)(__kindof UIView * _Nonnull))objc_getAssociatedObject(self, &kAssociatedObjectKey_tintColorDidChangeBlock); } -- (CGFloat)qmui_minYWhenCenterInSuperview { - return CGFloatGetCenter(CGRectGetHeight(self.superview.bounds), CGRectGetHeight(self.frame)); +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"]; } -- (void)qmui_removeAllSubviews { - [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; +- (__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]; + } + return [self convertPoint:point toView:view]; } -+ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { +- (CGPoint)qmui_convertPoint:(CGPoint)point fromView:(nullable UIView *)view { + UIWindow *selfWindow = [self isKindOfClass:[UIWindow class]] ? (UIWindow *)self : self.window; + UIWindow *fromWindow = [view isKindOfClass:[UIWindow class]] ? (UIWindow *)view : view.window; + if (selfWindow && fromWindow && selfWindow != fromWindow) { + CGPoint pointInFromWindow = fromWindow == view ? point : [view convertPoint:point toView:nil]; + CGPoint pointInSelfWindow = [selfWindow convertPoint:pointInFromWindow fromWindow:fromWindow]; + CGPoint pointInSelf = selfWindow == self ? pointInSelfWindow : [self convertPoint:pointInSelfWindow fromView:nil]; + return pointInSelf; + } + return [self convertPoint:point fromView:view]; +} + +- (CGRect)qmui_convertRect:(CGRect)rect toView:(nullable UIView *)view { + if (view) { + return [view qmui_convertRect:rect fromView:self]; + } + return [self convertRect:rect toView:view]; +} + +- (CGRect)qmui_convertRect:(CGRect)rect fromView:(nullable UIView *)view { + UIWindow *selfWindow = [self isKindOfClass:[UIWindow class]] ? (UIWindow *)self : self.window; + UIWindow *fromWindow = [view isKindOfClass:[UIWindow class]] ? (UIWindow *)view : view.window; + if (selfWindow && fromWindow && selfWindow != fromWindow) { + CGRect rectInFromWindow = fromWindow == view ? rect : [view convertRect:rect toView:nil]; + CGRect rectInSelfWindow = [selfWindow convertRect:rectInFromWindow fromWindow:fromWindow]; + CGRect rectInSelf = selfWindow == self ? rectInSelfWindow : [self convertRect:rectInSelfWindow fromView:nil]; + return rectInSelf; + } + return [self convertRect:rect fromView:view]; +} + ++ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion { if (animated) { [UIView animateWithDuration:duration delay:delay options:options animations:animations completion:completion]; } else { @@ -92,7 +236,7 @@ + (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duratio } } -+ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { ++ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration animations:(void (^ __nullable)(void))animations completion:(void (^)(BOOL finished))completion { if (animated) { [UIView animateWithDuration:duration animations:animations completion:completion]; } else { @@ -105,7 +249,7 @@ + (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duratio } } -+ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration animations:(void (^)(void))animations { ++ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration animations:(void (^ __nullable)(void))animations { if (animated) { [UIView animateWithDuration:duration animations:animations]; } else { @@ -115,6 +259,79 @@ + (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duratio } } ++ (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { + if (animated) { + [UIView animateWithDuration:duration delay:delay usingSpringWithDamping:dampingRatio initialSpringVelocity:velocity options:options animations:animations completion:completion]; + } else { + if (animations) { + animations(); + } + if (completion) { + completion(YES); + } + } +} + +- (void)QMUISymbolicUIViewBecomeFirstResponderWithoutKeyWindow { + QMUILogWarn(@"UIView (QMUI)", @"尝试让一个处于非 keyWindow 上的 %@ becomeFirstResponder,可能导致界面显示异常,请添加 '%@' 的 Symbolic Breakpoint 以捕捉此类信息\n%@", NSStringFromClass(self.class), NSStringFromSelector(_cmd), [NSThread callStackSymbols]); +} + +@end + +@implementation UIView (QMUI_ViewController) + +QMUISynthesizeBOOLProperty(qmui_isControllerRootView, setQmui_isControllerRootView) + +- (BOOL)qmui_visible { + if (self.hidden || self.alpha <= 0.01) { + return NO; + } + 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; +} + +static char kAssociatedObjectKey_viewController; +- (void)setQmui_viewController:(__kindof UIViewController * _Nullable)qmui_viewController { + QMUIWeakObjectContainer *weakContainer = objc_getAssociatedObject(self, &kAssociatedObjectKey_viewController); + if (!weakContainer) { + weakContainer = [[QMUIWeakObjectContainer alloc] init]; + } + weakContainer.object = qmui_viewController; + objc_setAssociatedObject(self, &kAssociatedObjectKey_viewController, weakContainer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + self.qmui_isControllerRootView = !!qmui_viewController; +} + +- (__kindof UIViewController *)qmui_viewController { + if (self.qmui_isControllerRootView) { + return (__kindof UIViewController *)((QMUIWeakObjectContainer *)objc_getAssociatedObject(self, &kAssociatedObjectKey_viewController)).object; + } + return self.superview.qmui_viewController; +} + +@end + +@interface UIViewController (QMUI_View) + +@end + +@implementation UIViewController (QMUI_View) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ExtendImplementationOfVoidMethodWithoutArguments([UIViewController class], @selector(viewDidLoad), ^(UIViewController *selfObject) { + selfObject.view.qmui_viewController = selfObject; + }); + }); +} + @end @@ -123,6 +340,7 @@ @implementation UIView (QMUI_Runtime) - (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector { // 排序依照 Xcode Interface Builder 里的控件排序,但保证子类在父类前面 NSMutableArray *viewSuperclasses = [[NSMutableArray alloc] initWithObjects: + [UIStackView class], [UILabel class], [UIButton class], [UISegmentedControl class], @@ -143,7 +361,10 @@ - (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector { [UIScrollView class], [UIDatePicker class], [UIPickerView class], - [UIWebView class], + [UIVisualEffectView class], + // Apple 不再接受使用了 UIWebView 的 App 提交,所以这里去掉 UIWebView + // https://github.com/Tencent/QMUI_iOS/issues/741 +// [UIWebView class], [UIWindow class], [UINavigationBar class], [UIToolbar class], @@ -153,13 +374,6 @@ - (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector { [UIView class], nil]; - if (NSClassFromString(@"UIStackView")) { - [viewSuperclasses addObject:[UIStackView class]]; - } - if (NSClassFromString(@"UIVisualEffectView")) { - [viewSuperclasses addObject:[UIVisualEffectView class]]; - } - for (NSInteger i = 0, l = viewSuperclasses.count; i < l; i++) { Class superclass = viewSuperclasses[i]; if ([self qmui_hasOverrideMethod:selector ofSuperclass:superclass]) { @@ -172,244 +386,279 @@ - (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector { @end -@implementation UIView (QMUI_Debug) +const CGFloat QMUIViewSelfSizingHeight = INFINITY; +const CGSize QMUIViewFixedSizeNone = {-1, -1}; + +@implementation UIView (QMUI_Layout) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ReplaceMethod([self class], @selector(layoutSubviews), @selector(qmui_layoutSubviews)); - ReplaceMethod([self class], @selector(addSubview:), @selector(qmui_addSubview:)); - ReplaceMethod([self class], @selector(becomeFirstResponder), @selector(qmui_becomeFirstResponder)); + + OverrideImplementation([UIView class], @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIView *selfObject, CGRect frame) { + + if (!CGSizeEqualToSize(selfObject.qmui_fixedSize, QMUIViewFixedSizeNone)) { + frame.size = selfObject.qmui_fixedSize; + } + + // 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); + } + + // 对非法的 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); + } + } + + CGRect precedingFrame = selfObject.frame; + BOOL valueChange = !CGRectEqualToRect(frame, precedingFrame); + if (selfObject.qmui_frameWillChangeBlock && valueChange) { + frame = selfObject.qmui_frameWillChangeBlock(selfObject, frame); + } + + // call super + void (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, frame); + + if (selfObject.qmui_frameDidChangeBlock && valueChange) { + selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); + } + }; + }); + + 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 (*originSelectorIMP)(id, SEL, CGRect); + originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, bounds); + + if (selfObject.qmui_frameDidChangeBlock && valueChange) { + selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); + } + }; + }); + + 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 (*originSelectorIMP)(id, SEL, CGPoint); + originSelectorIMP = (void (*)(id, SEL, CGPoint))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, center); + + if (selfObject.qmui_frameDidChangeBlock && valueChange) { + selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); + } + }; + }); + + 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 (*originSelectorIMP)(id, SEL, CGAffineTransform); + originSelectorIMP = (void (*)(id, SEL, CGAffineTransform))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, transform); + + if (selfObject.qmui_frameDidChangeBlock && valueChange) { + selfObject.qmui_frameDidChangeBlock(selfObject, precedingFrame); + } + }; + }); }); } -static char kAssociatedObjectKey_needsDifferentDebugColor; -- (void)setQmui_needsDifferentDebugColor:(BOOL)qmui_needsDifferentDebugColor { - objc_setAssociatedObject(self, &kAssociatedObjectKey_needsDifferentDebugColor, @(qmui_needsDifferentDebugColor), OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} -- (BOOL)qmui_needsDifferentDebugColor { - BOOL flag = [objc_getAssociatedObject(self, &kAssociatedObjectKey_needsDifferentDebugColor) boolValue]; - return flag; +- (CGFloat)qmui_top { + return CGRectGetMinY(self.frame); } -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]; - } -} -- (BOOL)qmui_shouldShowDebugColor { - BOOL flag = [objc_getAssociatedObject(self, &kAssociatedObjectKey_shouldShowDebugColor) boolValue]; - return flag; +- (void)setQmui_top:(CGFloat)top { + self.frame = CGRectSetY(self.frame, top); } -static char kAssociatedObjectKey_hasDebugColor; -- (void)setQmui_hasDebugColor:(BOOL)qmui_hasDebugColor { - objc_setAssociatedObject(self, &kAssociatedObjectKey_hasDebugColor, @(qmui_hasDebugColor), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +- (CGFloat)qmui_left { + return CGRectGetMinX(self.frame); } -- (BOOL)qmui_hasDebugColor { - BOOL flag = [objc_getAssociatedObject(self, &kAssociatedObjectKey_hasDebugColor) boolValue]; - return flag; + +- (void)setQmui_left:(CGFloat)left { + self.frame = CGRectSetX(self.frame, left); } -- (void)qmui_layoutSubviews { - [self qmui_layoutSubviews]; - if (self.qmui_shouldShowDebugColor) { - self.qmui_hasDebugColor = YES; - self.backgroundColor = [self debugColor]; - [self renderColorWithSubviews:self.subviews]; - } +- (CGFloat)qmui_bottom { + return CGRectGetMaxY(self.frame); } -- (void)renderColorWithSubviews:(NSArray *)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]; - } +- (void)setQmui_bottom:(CGFloat)bottom { + self.frame = CGRectSetY(self.frame, bottom - CGRectGetHeight(self.frame)); } -- (UIColor *)debugColor { - if (!self.qmui_needsDifferentDebugColor) { - return UIColorTestRed; - } else { - return [[UIColor qmui_randomColor] colorWithAlphaComponent:.8]; - } +- (CGFloat)qmui_right { + return CGRectGetMaxX(self.frame); } -- (void)qmui_addSubview:(UIView *)view { - if (view == self) { - NSAssert(NO, @"把自己作为 subview 添加到自己身上!\n%@", [NSThread callStackSymbols]); - } - [self qmui_addSubview:view]; +- (void)setQmui_right:(CGFloat)right { + self.frame = CGRectSetX(self.frame, right - CGRectGetWidth(self.frame)); } -- (BOOL)qmui_becomeFirstResponder { - if (IS_SIMULATOR && ![self isKindOfClass:[UIWindow class]] && self.window && !self.window.keyWindow) { - [self QMUISymbolicUIViewBecomeFirstResponderWithoutKeyWindow]; - } - return [self qmui_becomeFirstResponder]; +- (CGPoint)qmui_origin { + return self.frame.origin; } -- (void)QMUISymbolicUIViewBecomeFirstResponderWithoutKeyWindow { - NSLog(@"尝试让一个处于非 keyWindow 上的 %@ becomeFirstResponder,请添加 '%@' 的 Symbolic Breakpoint 以捕捉此类错误", NSStringFromClass(self.class), NSStringFromSelector(_cmd)); - NSLog(@"%@", [NSThread callStackSymbols]); +- (void)setQmui_origin:(CGPoint)qmui_origin { + self.center = CGPointMake(qmui_origin.x + CGRectGetWidth(self.frame) / 2, qmui_origin.y + CGRectGetHeight(self.frame) / 2); } -@end +- (CGFloat)qmui_width { + return CGRectGetWidth(self.frame); +} +- (void)setQmui_width:(CGFloat)width { + self.frame = CGRectSetWidth(self.frame, width); +} -@implementation UIView (QMUI_Border) +- (CGFloat)qmui_height { + return CGRectGetHeight(self.frame); +} -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - ReplaceMethod([self class], @selector(initWithFrame:), @selector(qmui_initWithFrame:)); - ReplaceMethod([self class], @selector(initWithCoder:), @selector(qmui_initWithCoder:)); - ReplaceMethod([self class], @selector(layoutSublayersOfLayer:), @selector(qmui_layoutSublayersOfLayer:)); - }); +- (void)setQmui_height:(CGFloat)height { + self.frame = CGRectSetHeight(self.frame, height); } -- (instancetype)qmui_initWithFrame:(CGRect)frame { - [self qmui_initWithFrame:frame]; - [self setDefaultStyle]; - return self; +- (CGSize)qmui_size { + return self.frame.size; } -- (instancetype)qmui_initWithCoder:(NSCoder *)aDecoder { - [self qmui_initWithCoder:aDecoder]; - [self setDefaultStyle]; - return self; +- (void)setQmui_size:(CGSize)qmui_size { + self.frame = CGRectSetSize(self.frame, qmui_size); } -- (void)qmui_layoutSublayersOfLayer:(CALayer *)layer { - - [self qmui_layoutSublayersOfLayer:layer]; - - if ((!self.qmui_borderLayer && self.qmui_borderPosition == QMUIBorderViewPositionNone) || (!self.qmui_borderLayer && self.qmui_borderWidth == 0)) { - return; - } - - if (self.qmui_borderLayer && self.qmui_borderPosition == QMUIBorderViewPositionNone && !self.qmui_borderLayer.path) { - return; - } - - if (self.qmui_borderLayer && self.qmui_borderWidth == 0 && self.qmui_borderLayer.lineWidth == 0) { - return; - } +static char kAssociatedObjectKey_fixedSize; +- (void)setQmui_fixedSize:(CGSize)qmui_fixedSize { + objc_setAssociatedObject(self, &kAssociatedObjectKey_fixedSize, @(qmui_fixedSize), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - if (!self.qmui_borderLayer) { - self.qmui_borderLayer = [CAShapeLayer layer]; - [self.qmui_borderLayer qmui_removeDefaultAnimations]; - [self.layer addSublayer:self.qmui_borderLayer]; - } - self.qmui_borderLayer.frame = self.bounds; - - CGFloat borderWidth = self.qmui_borderWidth; - self.qmui_borderLayer.lineWidth = borderWidth; - self.qmui_borderLayer.strokeColor = self.qmui_borderColor.CGColor; - self.qmui_borderLayer.lineDashPhase = self.qmui_dashPhase; - if (self.qmui_dashPattern) { - self.qmui_borderLayer.lineDashPattern = self.qmui_dashPattern; - } - - UIBezierPath *path = nil; - - if (self.qmui_borderPosition != QMUIBorderViewPositionNone) { - path = [UIBezierPath bezierPath]; - } - - if (self.qmui_borderPosition & QMUIBorderViewPositionTop) { - [path moveToPoint:CGPointMake(0, borderWidth / 2)]; - [path addLineToPoint:CGPointMake(CGRectGetWidth(self.bounds), borderWidth / 2)]; - } - - if (self.qmui_borderPosition & QMUIBorderViewPositionLeft) { - [path moveToPoint:CGPointMake(borderWidth / 2, 0)]; - [path addLineToPoint:CGPointMake(borderWidth / 2, CGRectGetHeight(self.bounds) - 0)]; - } - - if (self.qmui_borderPosition & QMUIBorderViewPositionBottom) { - [path moveToPoint:CGPointMake(0, CGRectGetHeight(self.bounds) - borderWidth / 2)]; - [path addLineToPoint:CGPointMake(CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds) - borderWidth / 2)]; + 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; } - - if (self.qmui_borderPosition & QMUIBorderViewPositionRight) { - [path moveToPoint:CGPointMake(CGRectGetWidth(self.bounds) - borderWidth / 2, 0)]; - [path addLineToPoint:CGPointMake(CGRectGetWidth(self.bounds) - borderWidth / 2, CGRectGetHeight(self.bounds))]; +} + +- (CGSize)qmui_fixedSize { + NSNumber *result = objc_getAssociatedObject(self, &kAssociatedObjectKey_fixedSize); + if (!result) { + return QMUIViewFixedSizeNone; } - - self.qmui_borderLayer.path = path.CGPath; + return result.CGSizeValue; } -- (void)setDefaultStyle { - self.qmui_borderWidth = PixelOne; - self.qmui_borderColor = UIColorSeparator; +- (CGFloat)qmui_extendToTop { + return self.qmui_top; } -static char kAssociatedObjectKey_borderPosition; -- (void)setQmui_borderPosition:(QMUIBorderViewPosition)qmui_borderPosition { - objc_setAssociatedObject(self, &kAssociatedObjectKey_borderPosition, @(qmui_borderPosition), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self setNeedsLayout]; +- (void)setQmui_extendToTop:(CGFloat)qmui_extendToTop { + self.qmui_height = self.qmui_bottom - qmui_extendToTop; + self.qmui_top = qmui_extendToTop; } -- (QMUIBorderViewPosition)qmui_borderPosition { - return (QMUIBorderViewPosition)[objc_getAssociatedObject(self, &kAssociatedObjectKey_borderPosition) unsignedIntegerValue]; +- (CGFloat)qmui_extendToLeft { + return self.qmui_left; } -static char kAssociatedObjectKey_borderWidth; -- (void)setQmui_borderWidth:(CGFloat)qmui_borderWidth { - objc_setAssociatedObject(self, &kAssociatedObjectKey_borderWidth, @(qmui_borderWidth), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self setNeedsLayout]; +- (void)setQmui_extendToLeft:(CGFloat)qmui_extendToLeft { + self.qmui_width = self.qmui_right - qmui_extendToLeft; + self.qmui_left = qmui_extendToLeft; } -- (CGFloat)qmui_borderWidth { - return (CGFloat)[objc_getAssociatedObject(self, &kAssociatedObjectKey_borderWidth) floatValue]; +- (CGFloat)qmui_extendToBottom { + return self.qmui_bottom; } -static char kAssociatedObjectKey_borderColor; -- (void)setQmui_borderColor:(UIColor *)qmui_borderColor { - objc_setAssociatedObject(self, &kAssociatedObjectKey_borderColor, qmui_borderColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self setNeedsLayout]; +- (void)setQmui_extendToBottom:(CGFloat)qmui_extendToBottom { + self.qmui_height = qmui_extendToBottom - self.qmui_top; + self.qmui_bottom = qmui_extendToBottom; } -- (UIColor *)qmui_borderColor { - return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_borderColor); +- (CGFloat)qmui_extendToRight { + return self.qmui_right; } -static char kAssociatedObjectKey_dashPhase; -- (void)setQmui_dashPhase:(CGFloat)qmui_dashPhase { - objc_setAssociatedObject(self, &kAssociatedObjectKey_dashPhase, @(qmui_dashPhase), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self setNeedsLayout]; +- (void)setQmui_extendToRight:(CGFloat)qmui_extendToRight { + self.qmui_width = qmui_extendToRight - self.qmui_left; + self.qmui_right = qmui_extendToRight; } -- (CGFloat)qmui_dashPhase { - return (CGFloat)[objc_getAssociatedObject(self, &kAssociatedObjectKey_dashPhase) floatValue]; +- (CGFloat)qmui_leftWhenCenterInSuperview { + return CGFloatGetCenter(CGRectGetWidth(self.superview.bounds), CGRectGetWidth(self.frame)); } -static char kAssociatedObjectKey_dashPattern; -- (void)setQmui_dashPattern:(NSArray *)qmui_dashPattern { - objc_setAssociatedObject(self, &kAssociatedObjectKey_dashPattern, qmui_dashPattern, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self setNeedsLayout]; +- (CGFloat)qmui_topWhenCenterInSuperview { + return CGFloatGetCenter(CGRectGetHeight(self.superview.bounds), CGRectGetHeight(self.frame)); } -- (NSArray *)qmui_dashPattern { - return (NSArray *)objc_getAssociatedObject(self, &kAssociatedObjectKey_dashPattern); +@end + + +@implementation UIView (CGAffineTransform) + +- (CGFloat)qmui_scaleX { + return self.transform.a; +} + +- (CGFloat)qmui_scaleY { + return self.transform.d; } -static char kAssociatedObjectKey_borderLayer; -- (void)setQmui_borderLayer:(CAShapeLayer *)qmui_borderLayer { - objc_setAssociatedObject(self, &kAssociatedObjectKey_borderLayer, qmui_borderLayer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +- (CGFloat)qmui_translationX { + return self.transform.tx; } -- (CAShapeLayer *)qmui_borderLayer { - return (CAShapeLayer *)objc_getAssociatedObject(self, &kAssociatedObjectKey_borderLayer); +- (CGFloat)qmui_translationY { + return self.transform.ty; } @end @@ -426,3 +675,116 @@ - (UIImage *)qmui_snapshotImageAfterScreenUpdates:(BOOL)afterScreenUpdates { } @end + + +@implementation UIView (QMUI_Debug) + +QMUISynthesizeBOOLProperty(qmui_needsDifferentDebugColor, setQmui_needsDifferentDebugColor) + +static char kAssociatedObjectKey_shouldShowDebugColor; +- (void)setQmui_shouldShowDebugColor:(BOOL)qmui_shouldShowDebugColor { + objc_setAssociatedObject(self, &kAssociatedObjectKey_shouldShowDebugColor, @(qmui_shouldShowDebugColor), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (qmui_shouldShowDebugColor) { + [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_shouldShowDebugColor = self.qmui_shouldShowDebugColor; + view.qmui_needsDifferentDebugColor = self.qmui_needsDifferentDebugColor; + if (view.qmui_shouldShowDebugColor) { + view.backgroundColor = [view debugColor]; + } else { + view.backgroundColor = UIColor.clearColor; + } + } +} + +- (UIColor *)debugColor { + if (!self.qmui_needsDifferentDebugColor) { + return UIColorTestRed; + } else { + return [[UIColor qmui_randomColor] colorWithAlphaComponent:.3]; + } +} + +@end 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 6289808f..51848248 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.h @@ -1,23 +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. + */ + // // UIViewController+QMUI.h // qmui // -// Created by QQMail on 16/1/12. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/1/12. // #import +#import "QMUICore.h" + +NS_ASSUME_NONNULL_BEGIN + +/// 在 App 的 rootViewController.view.frame.size 发生变化(例如横竖屏旋转,或者 iPad Split View 模式下调整大小)前发出通知,你可以通过 QMUIPrecedingAppSizeUserInfoKey 获取变化前的值(也即当前值),用 QMUIFollowingAppSizeUserInfoKey 获取变化后的值。 +extern NSNotificationName const QMUIAppSizeWillChangeNotification; + +/// 对应一个 NSValue 包裹的 CGSize 对象 +extern NSString *const QMUIPrecedingAppSizeUserInfoKey; + +/// 对应一个 NSValue 包裹的 CGSize 对象 +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 + + QMUIViewControllerVisible = QMUIViewControllerWillAppear | QMUIViewControllerDidAppear,// 表示是否处于可视范围,判断时请用 & 运算,例如 qmui_visibleState & QMUIViewControllerVisible +}; -/** - * @warning 在这里兼容了 iOS 9.0 以下的版本对 loadViewIfNeeded 方法的调用 - */ @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(nonatomic, weak, readonly) UIViewController *qmui_previousViewController; +@property(nullable, nonatomic, weak, readonly) UIViewController *qmui_previousViewController; /** 获取上一个UIViewController的title,可用于设置自定义返回按钮的文字 */ -@property(nonatomic, copy, readonly) NSString *qmui_previousViewControllerTitle; +@property(nullable, nonatomic, copy, readonly) NSString *qmui_previousViewControllerTitle; /** * 获取当前controller里的最高层可见viewController(可见的意思是还会判断self.view.window是否存在) @@ -26,7 +59,7 @@ * * @return 当前controller里的最高层可见viewController */ -- (UIViewController *)qmui_visibleViewControllerIfExist; +- (nullable UIViewController *)qmui_visibleViewControllerIfExist; /** * 当前 viewController 是否是被以 present 的方式显示的,是则返回 YES,否则返回 NO @@ -34,14 +67,92 @@ */ - (BOOL)qmui_isPresented; -/** 是否响应 QMUINavigationControllerDelegate */ -- (BOOL)qmui_respondQMUINavigationControllerDelegate; - /** * 是否应该响应一些UI相关的通知,例如 UIKeyboardNotification、UIMenuControllerNotification等,因为有可能当前界面已经被切走了(push到其他界面),但仍可能收到通知,所以在响应通知之前都应该做一下这个判断 */ - (BOOL)qmui_isViewLoadedAndVisible; +/** + 判断当前 viewController 是否为传入的 viewController 本身,或是其“子控制器” (childViewController)、孙子控制器(即 childViewController 的 childViewController ...) + */ +- (BOOL)qmui_isDescendantOfViewController:(UIViewController *)viewController; + +/** + 获取当前 viewController 所处的的生命周期阶段(也即 viewDidLoad/viewWillApear/viewDidAppear/viewWillDisappear/viewDidDisappear) + */ +@property(nonatomic, assign, readonly) QMUIViewControllerVisibleState qmui_visibleState; + +/** + 在当前 viewController 生命周期发生变化的时候调用 + */ +@property(nullable, nonatomic, copy) void (^qmui_visibleStateDidChangeBlock)(__kindof UIViewController *viewController, QMUIViewControllerVisibleState visibleState); + +/** + * UINavigationBar 在 self.view 坐标系里的 maxY,一般用于 self.view.subviews 布局时参考用 + * @warning 注意由于使用了坐标系转换的计算,所以要求在 self.view.window 存在的情况下使用才可以,因此请勿在 viewDidLoad 内使用,建议在 viewDidLayoutSubviews、viewWillAppear: 里使用。 + * @warning 如果不存在 UINavigationBar,则返回 0 + */ +@property(nonatomic, assign, readonly) CGFloat qmui_navigationBarMaxYInViewCoordinator; + +/** + * 底部 UIToolbar 在 self.view 坐标系里的占位高度,一般用于 self.view.subviews 布局时参考用 + * @warning 注意由于使用了坐标系转换的计算,所以要求在 self.view.window 存在的情况下使用才可以,因此请勿在 viewDidLoad 内使用,建议在 viewDidLayoutSubviews、viewWillAppear: 里使用。 + * @warning 如果不存在 UIToolbar,则返回 0 + */ +@property(nonatomic, assign, readonly) CGFloat qmui_toolbarSpacingInViewCoordinator; + +/** + * 底部 UITabBar 在 self.view 坐标系里的占位高度,一般用于 self.view.subviews 布局时参考用 + * @warning 注意由于使用了坐标系转换的计算,所以要求在 self.view.window 存在的情况下使用才可以,因此请勿在 viewDidLoad 内使用,建议在 viewDidLayoutSubviews、viewWillAppear: 里使用。 + * @warning 如果不存在 UITabBar,则返回 0 + */ +@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 才能拿到真正的值。 + */ +@property(nonatomic, assign, readonly) BOOL qmui_prefersStatusBarHidden; + +/** + 获取当前 viewController 的 statusBar style,与系统 preferredStatusBarStyle 的区别在于,系统的方法在对 containerViewController(例如 UITabBarController、UINavigationController 等)调用时,返回的是 containerViewController 自身的 preferredStatusBarStyle 的值,但真正决定 statusBar style 的是该 containerViewController 的 childViewControllerForStatusBarHidden 的 preferredStatusBarStyle 的值,所以只有用 qmui_preferredStatusBarStyle 才能拿到真正的值。 + */ +@property(nonatomic, assign, readonly) UIStatusBarStyle qmui_preferredStatusBarStyle; + +/** + 判断当前 viewController 是否具备显示 LargeTitle 的条件 + @warning 需要 viewController 在 navigationController 栈内才能正确判断 + */ +@property(nonatomic, assign, readonly) BOOL qmui_prefersLargeTitleDisplayed; + +@end + +/** + * 日常业务中经常碰到这样的场景:进入界面后会异步加载数据,当数据加载完并且 viewDidAppear: 后要执行一些操作(例如滚动列表到某一行并高亮它),若数据在 viewDidAppear: 前就已经加载完,也需要等到 viewDidAppear: 时才做那些操作。 + * 当你需要实现这种场景的效果时,可以用以下两个属性,具体请查看属性注释。 + */ +@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 就会被自动执行。 +@property(nonatomic, assign, getter = isQmui_dataLoaded) BOOL qmui_dataLoaded; + @end @interface UIViewController (Runtime) @@ -51,5 +162,42 @@ * @param selector 要判断的方法 * @return YES 表示当前类重写了指定的方法,NO 表示没有重写,使用的是 UIViewController 默认的实现 */ -- (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector; +- (BOOL)qmui_hasOverrideUIKitMethod:(_Nonnull SEL)selector; @end + +@interface UIViewController (QMUINavigationController) + +/// 判断当前 viewController 是否处于手势返回中,仅对当前手势返回涉及到的前后两个 viewController 有效 +@property(nonatomic, assign, readonly) BOOL qmui_navigationControllerPoppingInteracted; + +/// 基本与上一个属性 qmui_navigationControllerPoppingInteracted 相同,只不过 qmui_navigationControllerPoppingInteracted 是在 began 时就为 YES,而这个属性仅在 changed 时才为 YES。 +/// @note viewController 会在走完 viewWillAppear: 之后才将这个值置为 YES。 +@property(nonatomic, assign) BOOL qmui_navigationControllerPopGestureRecognizerChanging; + +/// 当前 viewController 是否正在被手势返回 pop +@property(nonatomic, assign) BOOL qmui_poppingByInteractivePopGestureRecognizer; + +/// 当前 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) + +/** + * 获取当前应用里最顶层的可见viewController + * @warning 注意返回值可能为nil,要做好保护 + */ ++ (nullable UIViewController *)visibleViewController; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.m b/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.m index 6eb363f7..de9fbbcf 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIViewController+QMUI.m @@ -1,57 +1,207 @@ +/** + * 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+QMUI.m // qmui // -// Created by QQMail on 16/1/12. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/1/12. // #import "UIViewController+QMUI.h" -#import "QMUINavigationController.h" #import "UINavigationController+QMUI.h" -#import #import "QMUICore.h" +#import "UIInterface+QMUI.h" #import "NSObject+QMUI.h" +#import "QMUILog.h" +#import "UIView+QMUI.h" + +NSNotificationName const QMUIAppSizeWillChangeNotification = @"QMUIAppSizeWillChangeNotification"; +NSString *const QMUIPrecedingAppSizeUserInfoKey = @"QMUIPrecedingAppSizeUserInfoKey"; +NSString *const QMUIFollowingAppSizeUserInfoKey = @"QMUIFollowingAppSizeUserInfoKey"; @implementation UIViewController (QMUI) -void qmui_loadViewIfNeeded (id current_self, SEL current_cmd) { - // 主动调用 self.view,从而触发 loadView,以模拟 iOS 9.0 以下的系统 loadViewIfNeeded 行为 - QMUILog(@"%@", ((UIViewController *)current_self).view); -} +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; dispatch_once(&onceToken, ^{ - // 为 description 增加更丰富的信息 - ReplaceMethod([UIViewController class], @selector(description), @selector(qmui_description)); + ExchangeImplementations([UIViewController class], @selector(description), @selector(qmuivc_description)); + + ExtendImplementationOfVoidMethodWithoutArguments([UIViewController class], @selector(viewDidLoad), ^(UIViewController *selfObject) { + selfObject.qmui_visibleState = QMUIViewControllerViewDidLoad; + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(viewWillAppear:), BOOL, ^(UIViewController *selfObject, BOOL animated) { + selfObject.qmui_visibleState = QMUIViewControllerWillAppear; + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(viewDidAppear:), BOOL, ^(UIViewController *selfObject, BOOL animated) { + selfObject.qmui_visibleState = QMUIViewControllerDidAppear; + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(viewWillDisappear:), BOOL, ^(UIViewController *selfObject, BOOL animated) { + selfObject.qmui_visibleState = QMUIViewControllerWillDisappear; + }); + + ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(viewDidDisappear:), BOOL, ^(UIViewController *selfObject, BOOL animated) { + selfObject.qmui_visibleState = QMUIViewControllerDidDisappear; + }); + + OverrideImplementation([UIViewController class], @selector(viewWillTransitionToSize:withTransitionCoordinator:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject, CGSize size, id coordinator) { + + 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)}]; + } + } + + // call super + void (*originSelectorIMP)(id, SEL, CGSize, id); + originSelectorIMP = (void (*)(id, SEL, CGSize, id))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, size, coordinator); + }; + }); - // 兼容 iOS 9.0 以下的版本对 loadViewIfNeeded 方法的调用 - if (![[UIViewController class] instancesRespondToSelector:@selector(loadViewIfNeeded)]) { - Class metaclass = [self class]; - BOOL success = class_addMethod(metaclass, @selector(loadViewIfNeeded), (IMP)qmui_loadViewIfNeeded, "v@:"); - QMUILog(@"%@ %s, success = %@", NSStringFromClass([self class]), __func__, StringFromBOOL(success)); + // 修复 iOS 11 及以后,UIScrollView 无法自动适配不透明的 tabBar,导致底部 inset 错误的问题 + // https://github.com/Tencent/QMUI_iOS/issues/218 + 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); + }; + }); } - // 实现 AutomaticallyRotateDeviceOrientation 开关的功能 - ReplaceMethod([UIViewController class], @selector(viewWillAppear:), @selector(qmui_viewWillAppear:)); + // 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 *)qmui_description { - NSString *result = [NSString stringWithFormat:@"%@\nsuperclass:\t\t\t\t%@\ntitle:\t\t\t\t\t%@\nview:\t\t\t\t\t%@", [self qmui_description], NSStringFromClass(self.superclass), self.title, [self isViewLoaded] ? self.view : nil]; +- (NSString *)qmuivc_description { + 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 qmui_description], [navController.visibleViewController qmui_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 qmui_description]]; + NSString *tabBarDescription = [NSString stringWithFormat:@"; viewControllers(%@): %@; selectedViewController(%@): %@", @(tabBarController.viewControllers.count), [self descriptionWithViewControllers:tabBarController.viewControllers], @(tabBarController.selectedIndex), [tabBarController.selectedViewController qmuivc_description]]; result = [result stringByAppendingString:tabBarDescription]; } @@ -60,113 +210,65 @@ - (NSString *)qmui_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] qmui_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]; } -- (void)qmui_viewWillAppear:(BOOL)animated { - [self qmui_viewWillAppear:animated]; - if (!AutomaticallyRotateDeviceOrientation) { - 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 = [self interfaceOrientationMask:self.supportedInterfaceOrientations containsDeviceOrientation:deviceOrientation] ? deviceOrientation : [self deviceOrientationWithInterfaceOrientationMask:self.supportedInterfaceOrientations]; - - // 之前没用私有接口修改过,拿就按最标准的方式去旋转 - if (!shouldConsiderBeforeChanging) { - if ([QMUIHelper rotateToDeviceOrientation:deviceOrientationToRotate]) { - [QMUIHelper sharedInstance].orientationBeforeChangingByHelper = deviceOrientation; - } else { - [QMUIHelper sharedInstance].orientationBeforeChangingByHelper = UIDeviceOrientationUnknown; ++ (BOOL)qmui_isSystemContainerViewController { + for (Class clz in @[UINavigationController.class, UITabBarController.class, UISplitViewController.class]) { + if ([self isSubclassOfClass:clz]) { + return YES; } - return; } - - // 用私有接口修改过方向,但下一个界面和当前界面方向不相同,则要把修改前记录下来的那个设备方向考虑进来 - deviceOrientationToRotate = [self interfaceOrientationMask:self.supportedInterfaceOrientations containsDeviceOrientation:deviceOrientationBeforeChangingByHelper] ? deviceOrientationBeforeChangingByHelper : [self deviceOrientationWithInterfaceOrientationMask:self.supportedInterfaceOrientations]; - [QMUIHelper rotateToDeviceOrientation:deviceOrientationToRotate]; + return NO; } -- (UIDeviceOrientation)deviceOrientationWithInterfaceOrientationMask:(UIInterfaceOrientationMask)mask { - if ((mask & UIInterfaceOrientationMaskAll) == UIInterfaceOrientationMaskAll) { - return [UIDevice currentDevice].orientation; - } - if ((mask & UIInterfaceOrientationMaskAllButUpsideDown) == UIInterfaceOrientationMaskAllButUpsideDown) { - return [UIDevice currentDevice].orientation; - } - if ((mask & UIInterfaceOrientationMaskPortrait) == UIInterfaceOrientationMaskPortrait) { - return UIDeviceOrientationPortrait; - } - if ((mask & UIInterfaceOrientationMaskLandscape) == UIInterfaceOrientationMaskLandscape) { - return [UIDevice currentDevice].orientation == UIDeviceOrientationLandscapeLeft ? UIDeviceOrientationLandscapeLeft : UIDeviceOrientationLandscapeRight; - } - if ((mask & UIInterfaceOrientationMaskLandscapeLeft) == UIInterfaceOrientationMaskLandscapeLeft) { - return UIDeviceOrientationLandscapeRight; - } - if ((mask & UIInterfaceOrientationMaskLandscapeRight) == UIInterfaceOrientationMaskLandscapeRight) { - return UIDeviceOrientationLandscapeLeft; - } - if ((mask & UIInterfaceOrientationMaskPortraitUpsideDown) == UIInterfaceOrientationMaskPortraitUpsideDown) { - return UIDeviceOrientationPortraitUpsideDown; - } - return [UIDevice currentDevice].orientation; +- (BOOL)qmui_isSystemContainerViewController { + return self.class.qmui_isSystemContainerViewController; } -- (BOOL)interfaceOrientationMask:(UIInterfaceOrientationMask)mask containsDeviceOrientation:(UIDeviceOrientation)deviceOrientation { - if (deviceOrientation == UIDeviceOrientationUnknown) { - return YES;// YES 表示不用额外处理 +static char kAssociatedObjectKey_visibleState; +- (void)setQmui_visibleState:(QMUIViewControllerVisibleState)qmui_visibleState { + BOOL valueChanged = self.qmui_visibleState != qmui_visibleState; + objc_setAssociatedObject(self, &kAssociatedObjectKey_visibleState, @(qmui_visibleState), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (valueChanged && self.qmui_visibleStateDidChangeBlock) { + self.qmui_visibleStateDidChangeBlock(self, qmui_visibleState); } - - if ((mask & UIInterfaceOrientationMaskAll) == UIInterfaceOrientationMaskAll) { - return YES; - } - if ((mask & UIInterfaceOrientationMaskAllButUpsideDown) == UIInterfaceOrientationMaskAllButUpsideDown) { - return UIInterfaceOrientationPortraitUpsideDown != deviceOrientation; - } - if ((mask & UIInterfaceOrientationMaskPortrait) == UIInterfaceOrientationMaskPortrait) { - return UIInterfaceOrientationPortrait == deviceOrientation; - } - if ((mask & UIInterfaceOrientationMaskLandscape) == UIInterfaceOrientationMaskLandscape) { - return UIInterfaceOrientationLandscapeLeft == deviceOrientation || UIInterfaceOrientationLandscapeRight == deviceOrientation; - } - if ((mask & UIInterfaceOrientationMaskLandscapeLeft) == UIInterfaceOrientationMaskLandscapeLeft) { - return UIInterfaceOrientationLandscapeLeft == deviceOrientation; - } - if ((mask & UIInterfaceOrientationMaskLandscapeRight) == UIInterfaceOrientationMaskLandscapeRight) { - return UIInterfaceOrientationLandscapeRight == deviceOrientation; - } - if ((mask & UIInterfaceOrientationMaskPortraitUpsideDown) == UIInterfaceOrientationMaskPortraitUpsideDown) { - return UIInterfaceOrientationPortraitUpsideDown == deviceOrientation; - } - - return YES; +} + +- (QMUIViewControllerVisibleState)qmui_visibleState { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_visibleState)) unsignedIntegerValue]; } - (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]; + UIViewController *previousViewController = nil; + NSArray *viewControllers = self.navigationController.viewControllers; + NSUInteger index = [viewControllers indexOfObject:self]; + if (index != NSNotFound && index > 0) { + previousViewController = viewControllers[index - 1]; + + // 系统 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; + } + } } - 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; } @@ -197,20 +299,226 @@ - (UIViewController *)qmui_visibleViewControllerIfExist { return [((UITabBarController *)self).selectedViewController qmui_visibleViewControllerIfExist]; } - if ([self isViewLoaded] && self.view.window) { + if ([self qmui_isViewLoadedAndVisible]) { return self; } else { - NSLog(@"qmui_visibleViewControllerIfExist:,找不到可见的viewController。self = %@, self.view.window = %@", self, self.view.window); + QMUILog(@"UIViewController (QMUI)", @"qmui_visibleViewControllerIfExist:,找不到可见的viewController。self = %@, self.view.window = %@", self, [self isViewLoaded] ? self.view.window : nil); return nil; } } -- (BOOL)qmui_respondQMUINavigationControllerDelegate { - return [[self class] conformsToProtocol:@protocol(QMUINavigationControllerDelegate)]; +- (BOOL)qmui_isViewLoadedAndVisible { + return self.isViewLoaded && self.view.qmui_visible; } -- (BOOL)qmui_isViewLoadedAndVisible { - return self.isViewLoaded && self.view.window; +- (CGFloat)qmui_navigationBarMaxYInViewCoordinator { + if (!self.isViewLoaded) { + return 0; + } + + // 手势返回过程中 self.navigationController 已经不存在了,所以暂时通过遍历 view 层级的方式去获取到 navigationController 的引用 + UINavigationController *navigationController = self.navigationController; + if (!navigationController) { + navigationController = self.view.superview.superview.qmui_viewController; + if (![navigationController isKindOfClass:[UINavigationController class]]) { + navigationController = nil; + } + } + + if (!navigationController) { + return 0; + } + + UINavigationBar *navigationBar = navigationController.navigationBar; + CGFloat barMinX = CGRectGetMinX(navigationBar.frame); + CGFloat barPresentationMinX = CGRectGetMinX(navigationBar.layer.presentationLayer.frame); + CGFloat superviewX = CGRectGetMinX(self.view.superview.frame); + CGFloat superviewX2 = CGRectGetMinX(self.view.superview.superview.frame); + + if (self.qmui_navigationControllerPoppingInteracted) { + if (barMinX != 0 && barMinX == barPresentationMinX) { + // 返回到无 bar 的界面 + return 0; + } else if (barMinX > 0) { + if (self.qmui_willAppearByInteractivePopGestureRecognizer) { + // 要手势返回去的那个界面隐藏了 bar + return 0; + } + } else if (barMinX < 0) { + // 正在手势返回的这个界面隐藏了 bar + if (!self.qmui_willAppearByInteractivePopGestureRecognizer) { + return 0; + } + } else { + // 正在手势返回的这个界面隐藏了 bar + if (barPresentationMinX != 0 && !self.qmui_willAppearByInteractivePopGestureRecognizer) { + return 0; + } + } + } else { + if (barMinX > 0) { + // 正在 pop 回无 bar 的界面 + if (superviewX2 <= 0) { + // 即将回到的那个无 bar 的界面 + return 0; + } + } else if (barMinX < 0) { + if (barPresentationMinX < 0) { + // 从无 bar push 进无 bar 的界面 + return 0; + } + // 正在从有 bar 的界面 push 到无 bar 的界面(bar 被推到左边屏幕外,所以是负数) + if (superviewX >= 0) { + // 即将进入的那个无 bar 的界面 + return 0; + } + } else { + if (superviewX < 0 && barPresentationMinX != 0) { + // 无 bar push 进有 bar 的界面时,背后的那个无 bar 的界面 + return 0; + } + if (superviewX2 > 0 && barPresentationMinX < 0) { + // 无 bar pop 回有 bar 的界面时,被 pop 掉的那个无 bar 的界面 + return 0; + } + } + } + + CGRect navigationBarFrameInView = [self.view convertRect:navigationBar.frame fromView:navigationBar.superview]; + CGRect navigationBarFrame = CGRectIntersection(self.view.bounds, navigationBarFrameInView); + + // 两个 rect 如果不存在交集,CGRectIntersection 计算结果可能为非法的 rect,所以这里做个保护 + if (!CGRectIsValidated(navigationBarFrame)) { + return 0; + } + + CGFloat result = CGRectGetMaxY(navigationBarFrame); + return result; +} + +- (CGFloat)qmui_toolbarSpacingInViewCoordinator { + if (!self.isViewLoaded) { + return 0; + } + if (!self.navigationController.toolbar || self.navigationController.toolbarHidden) { + return 0; + } + CGRect toolbarFrame = CGRectIntersection(self.view.bounds, [self.view convertRect:self.navigationController.toolbar.frame fromView:self.navigationController.toolbar.superview]); + + // 两个 rect 如果不存在交集,CGRectIntersection 计算结果可能为非法的 rect,所以这里做个保护 + if (!CGRectIsValidated(toolbarFrame)) { + return 0; + } + + CGFloat result = CGRectGetHeight(self.view.bounds) - CGRectGetMinY(toolbarFrame); + return result; +} + +- (CGFloat)qmui_tabBarSpacingInViewCoordinator { + if (!self.isViewLoaded) { + return 0; + } + 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,所以这里做个保护 + if (!CGRectIsValidated(tabBarFrame)) { + return 0; + } + + CGFloat result = CGRectGetHeight(self.view.bounds) - CGRectGetMinY(tabBarFrame); + return result; +} + +- (BOOL)qmui_prefersStatusBarHidden { + if (self.childViewControllerForStatusBarHidden) { + return self.childViewControllerForStatusBarHidden.qmui_prefersStatusBarHidden; + } + return self.prefersStatusBarHidden; +} + +- (UIStatusBarStyle)qmui_preferredStatusBarStyle { + if (self.childViewControllerForStatusBarStyle) { + return self.childViewControllerForStatusBarStyle.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) + +QMUISynthesizeIdCopyProperty(qmui_didAppearAndLoadDataBlock, setQmui_didAppearAndLoadDataBlock) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + 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; + } + }; + }); + }); +} + +static char kAssociatedObjectKey_dataLoaded; +- (void)setQmui_dataLoaded:(BOOL)qmui_dataLoaded { + objc_setAssociatedObject(self, &kAssociatedObjectKey_dataLoaded, @(qmui_dataLoaded), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + if (self.qmui_didAppearAndLoadDataBlock && qmui_dataLoaded && self.qmui_visibleState >= QMUIViewControllerDidAppear) { + self.qmui_didAppearAndLoadDataBlock(); + self.qmui_didAppearAndLoadDataBlock = nil; + } +} + +- (BOOL)isQmui_dataLoaded { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_dataLoaded)) boolValue]; } @end @@ -246,3 +554,42 @@ - (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector { } @end + +@implementation UIViewController (QMUINavigationController) + +QMUISynthesizeBOOLProperty(qmui_navigationControllerPopGestureRecognizerChanging, setQmui_navigationControllerPopGestureRecognizerChanging) +QMUISynthesizeBOOLProperty(qmui_poppingByInteractivePopGestureRecognizer, setQmui_poppingByInteractivePopGestureRecognizer) +QMUISynthesizeBOOLProperty(qmui_willAppearByInteractivePopGestureRecognizer, setQmui_willAppearByInteractivePopGestureRecognizer) + +- (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 *visibleViewController = [rootViewController qmui_visibleViewControllerIfExist]; + return visibleViewController; +} + +@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 276bbccb..41745ca0 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.h +++ b/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.h @@ -1,13 +1,41 @@ +/** + * 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. + */ + // // UIWindow+QMUI.h // qmui // -// Created by MoLice on 16/7/21. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/7/21. // #import @interface UIWindow (QMUI) +/** + 允许当前 window 接管 statusBar 的样式设置,默认为 YES。 + + @note 经测试,- [UIViewController prefersStatusBarHidden]、- [UIViewController preferredStatusBarStyle]、- [UIViewController preferredStatusBarUpdateAnimation] 系列方法仅当该 viewController 所在的 UIWindow 符合以下条件时才能生效: + 1. window 处于最顶层,没有其他 window 遮挡 + 2. iOS 10 及以后,window.frame 与 mainScreen.bounds 相等(origin、size 都应一模一样) + 因此当我们在某些情况下利用 UIWindow 去实现遮罩、浮层等效果时,会错误地导致原来的 window 内的 viewController 丢失了对 statusBar 的控制权(因为你新加的 window 满足了上文所有条件),为了避免这种情况,可以将你自己的 window.qmui_capturesStatusBarAppearance = NO,这样你的 window 就不会影响原 window 对 statusBar 的控制权。同理,如果你的 window 本身就不需要盖住整个屏幕,那就算你不设置 qmui_capturesStatusBarAppearance 也不会影响原 window 的表现。 + + @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 ac2d7b6f..57e03b66 100644 --- a/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.m +++ b/QMUI/QMUIKit/UIKitExtensions/UIWindow+QMUI.m @@ -1,9 +1,16 @@ +/** + * 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. + */ + // // UIWindow+QMUI.m // qmui // -// Created by MoLice on 16/7/21. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 16/7/21. // #import "UIWindow+QMUI.h" @@ -11,22 +18,114 @@ @implementation UIWindow (QMUI) +QMUISynthesizeBOOLProperty(qmui_capturesStatusBarAppearance, setQmui_capturesStatusBarAppearance) + + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ReplaceMethod([self class], @selector(init), @selector(qmui_init)); + + // -[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) { + + if (selfObject.qmui_capturesStatusBarAppearance) { + // call super + BOOL (*originSelectorIMP)(id, SEL); + originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); + BOOL result = originSelectorIMP(selfObject, originCMD); + return result; + } + + return NO; + }; + }); }); } -- (instancetype)qmui_init { - if (IOS_VERSION < 9.0) { - // iOS 9 以前的版本,UIWindow init时如果不给一个frame,默认是CGRectZero,而iOS 9以后的版本,由于增加了分屏(Split View)功能,你的App可能运行在一个非全屏大小的区域内,所以UIWindow如果调用init方法(而不是initWithFrame:)来初始化,系统会自动为你的window设置一个合适的大小。所以这里对iOS 9以前的版本做适配,默认给一个全屏的frame - UIWindow *window = [self qmui_init]; - window.frame = [[UIScreen mainScreen] bounds]; - return window; +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 [self qmui_init]; + 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; + } + + 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; + } + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + }; + }); + } oncePerIdentifier:@"UIWindow (QMUI) keyWindow"]; } @end diff --git a/QMUI/QMUIKit/UIMainFrame/QMUICommonTableViewController.h b/QMUI/QMUIKit/UIMainFrame/QMUICommonTableViewController.h deleted file mode 100644 index 9a0b9184..00000000 --- a/QMUI/QMUIKit/UIMainFrame/QMUICommonTableViewController.h +++ /dev/null @@ -1,130 +0,0 @@ -// -// QMUICommonTableViewController.h -// qmui -// -// Created by QQMail on 14-6-24. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUICommonViewController.h" -#import "QMUISearchController.h" -#import "QMUITableView.h" - -/** - * 配合属性 `tableViewInitialContentInset` 使用,标志 `tableViewInitialContentInset` 是否有被修改过 - * @see tableViewInitialContentInset - */ -extern const UIEdgeInsets QMUICommonTableViewControllerInitialContentInsetNotSet; - -/** - * 可作为项目内所有 `UITableViewController` 的基类,注意是继承自 `QMUICommonViewController` 而不是 `UITableViewController`。 - * - * 一般通过 `initWithStyle:` 方法初始化,对于要生成 `UITableViewStylePlain` 类型的列表,推荐使用 `init:` 方法。 - * - * 提供的功能包括: - * - * 1. 集成 `QMUISearchController`,可通过属性 `shouldShowSearchBar` 来快速为列表生成一个 searchBar 及 searchController,具体请查看 QMUICommonTableViewController (Search)。 - * - * 2. 通过属性 `tableViewInitialContentInset` 和 `tableViewInitialScrollIndicatorInsets` 来提供对界面初始状态下的列表 `contentInset`、`contentOffset` 的调整能力,一般在系统的 `automaticallyAdjustsScrollViewInsets` 属性无法满足需求时使用。 - * - * @note emptyView 会从 tableHeaderView 的下方开始布局到 tableView 最底部,因此它会遮挡 tableHeaderView 之外的部分(比如 tableFooterView 和 cells ),你可以重写 layoutEmptyView 来改变这个布局方式 - * - * @see QMUISearchController - */ -@interface QMUICommonTableViewController : QMUICommonViewController - -- (instancetype)initWithStyle:(UITableViewStyle)style NS_DESIGNATED_INITIALIZER; -- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER; - -/** - * 初始化时调用的方法,会在两个 NS_DESIGNATED_INITIALIZER 方法中被调用,所以子类如果需要同时支持两个 NS_DESIGNATED_INITIALIZER 方法,则建议把初始化时要做的事情放到这个方法里。否则仅需重写要支持的那个 NS_DESIGNATED_INITIALIZER 方法即可。 - */ -- (void)didInitializedWithStyle:(UITableViewStyle)style NS_REQUIRES_SUPER; - -/// 获取当前的 `UITableViewStyle` -@property(nonatomic, assign, readonly) UITableViewStyle style; - -/// 获取当前的 tableView -@property(nonatomic, strong, readonly) IBOutlet QMUITableView *tableView; - -/** - * 列表使用自定义的contentInset,不使用系统默认计算的,默认为QMUICommonTableViewControllerInitialContentInsetNotSet。
- * 当更改了这个值后,会把self.automaticallyAdjustsScrollViewInsets = NO - */ -@property(nonatomic, assign) UIEdgeInsets tableViewInitialContentInset; - -/** - * 是否需要让scrollIndicatorInsets与tableView.contentInsets区分开来,如果不设置,则与tableView.contentInset保持一致。 - * - * 只有当更改了tableViewInitialContentInset后,这个属性才会生效。 - */ -@property(nonatomic, assign) UIEdgeInsets tableViewInitialScrollIndicatorInsets; - -@end - - -@interface QMUICommonTableViewController (QMUISubclassingHooks) - -/** - * 初始化tableView,在initSubViews的时候被自动调用。 - * - * 一般情况下,有关tableView的设置属性的代码都应该写在这里。 - */ -- (void)initTableView; - -/** - * 是否需要在第一次进入界面时将tableHeaderView隐藏(通过调整self.tableView.contentOffset实现) - * - * 默认为NO - * - * @see QMUITableViewDelegate - */ -- (BOOL)shouldHideTableHeaderViewInitial; - -@end - - -@interface QMUICommonTableViewController (Search) - -/** - * 控制列表里是否需要搜索框,如果为 YES,则会在 viewDidLoad 之后创建一个 searchBar 作为 tableHeaderView;如果为 NO,则会移除已有的 searchBar 和 searchController。 - * 默认为 NO。 - * @note 若在 viewDidLoad 之前设置为 YES,也会等到 viewDidLoad 时才去创建搜索框。 - */ -@property(nonatomic, assign) BOOL shouldShowSearchBar; - -/** - * 获取当前的 searchController,注意只有当 `shouldShowSearchBar` 为 `YES` 时才有用 - * - * 默认为 `nil` - * - * @see QMUITableViewDelegate - */ -@property(nonatomic, strong, readonly) QMUISearchController *searchController; - -/** - * 获取当前的 searchBar,注意只有当 `shouldShowSearchBar` 为 `YES` 时才有用 - * - * 默认为 `nil` - * - * @see QMUITableViewDelegate - */ -@property(nonatomic, strong, readonly) UISearchBar *searchBar; - -/** - * 是否应该在显示空界面时自动隐藏搜索框 - * - * 默认为 `NO` - */ -- (BOOL)shouldHideSearchBarWhenEmptyViewShowing; - -/** - * 初始化searchController和searchBar,在initSubViews的时候被自动调用。 - * - * 会询问 `self.shouldShowSearchBar`,若返回 `YES`,则创建 searchBar 并将其以 `tableHeaderView` 的形式呈现在界面里;若返回 `NO`,则将 `tableHeaderView` 置为nil。 - * - * @warning `self.shouldShowSearchBar` 默认为 NO,需要 searchBar 的界面必须手动将其置为 `YES`。 - */ -- (void)initSearchController; - -@end diff --git a/QMUI/QMUIKit/UIMainFrame/QMUICommonTableViewController.m b/QMUI/QMUIKit/UIMainFrame/QMUICommonTableViewController.m deleted file mode 100644 index b24dfaa9..00000000 --- a/QMUI/QMUIKit/UIMainFrame/QMUICommonTableViewController.m +++ /dev/null @@ -1,424 +0,0 @@ -// -// QMUICommonTableViewController.m -// qmui -// -// Created by QQMail on 14-6-24. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUICommonTableViewController.h" -#import "QMUICore.h" -#import "QMUITableView.h" -#import "QMUIEmptyView.h" -#import "QMUILabel.h" -#import "UIScrollView+QMUI.h" -#import "UITableView+QMUI.h" -#import "UICollectionView+QMUI.h" - -const UIEdgeInsets QMUICommonTableViewControllerInitialContentInsetNotSet = {-1, -1, -1, -1}; -const NSInteger kSectionHeaderFooterLabelTag = 1024; - -@interface QMUICommonTableViewController () { - BOOL _shouldShowSearchBar; - QMUISearchController *_searchController; - UISearchBar *_searchBar; -} - -@property(nonatomic,strong,readwrite) QMUITableView *tableView; -@property(nonatomic,assign) BOOL hasSetInitialContentInset; -@property(nonatomic,assign) BOOL hasHideTableHeaderViewInitial; - -@end - - -@implementation QMUICommonTableViewController - -- (instancetype)initWithStyle:(UITableViewStyle)style { - if (self = [super initWithNibName:nil bundle:nil]) { - [self didInitializedWithStyle:style]; - } - return self; -} - -- (instancetype)init { - return [self initWithStyle:UITableViewStylePlain]; -} - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - return [self init]; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitializedWithStyle:UITableViewStylePlain]; - } - return self; -} - -- (void)didInitializedWithStyle:(UITableViewStyle)style { - _style = style; - self.hasHideTableHeaderViewInitial = NO; - self.tableViewInitialContentInset = QMUICommonTableViewControllerInitialContentInsetNotSet; - self.tableViewInitialScrollIndicatorInsets = QMUICommonTableViewControllerInitialContentInsetNotSet; -} - -- (void)dealloc { - // 用下划线而不是self.xxx来访问tableView,避免dealloc时self.view尚未被加载,此时调用self.tableView反而会触发loadView - _tableView.delegate = nil; - _tableView.dataSource = nil; -} - -- (NSString *)description { - if (![self isViewLoaded]) { - return [super description]; - } - - NSString *result = [NSString stringWithFormat:@"%@\ntableView:\t\t\t\t%@", [super description], self.tableView]; - NSInteger sections = [self.tableView.dataSource numberOfSectionsInTableView:self.tableView]; - 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]; - 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 ? @"," : @""]; - } - [sectionCountString appendString:@"\t\t\t\t\t\t)"]; - result = [result stringByAppendingString:sectionCountString]; - } - return result; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - UIColor *backgroundColor = nil; - if (self.style == UITableViewStylePlain) { - backgroundColor = TableViewBackgroundColor; - } else { - backgroundColor = TableViewGroupedBackgroundColor; - } - if (backgroundColor) { - self.view.backgroundColor = backgroundColor; - } -} - -- (void)initSubviews { - [super initSubviews]; - [self initTableView]; - [self initSearchController]; -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - [self.tableView qmui_clearsSelection]; - [self.searchController.tableView qmui_clearsSelection]; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - BOOL shouldChangeTableViewFrame = !CGRectEqualToRect(self.view.bounds, self.tableView.frame); - if (shouldChangeTableViewFrame) { - self.tableView.frame = self.view.bounds; - } - - if ([self shouldAdjustTableViewContentInsetsInitially] && !self.hasSetInitialContentInset) { - self.tableView.contentInset = self.tableViewInitialContentInset; - if ([self shouldAdjustTableViewScrollIndicatorInsetsInitially]) { - self.tableView.scrollIndicatorInsets = self.tableViewInitialScrollIndicatorInsets; - } else { - // 默认和tableView.contentInset一致 - self.tableView.scrollIndicatorInsets = self.tableView.contentInset; - } - [self.tableView qmui_scrollToTop]; - self.hasSetInitialContentInset = YES; - } - - [self hideTableHeaderViewInitialIfCanWithAnimated:NO force:NO]; - - [self layoutEmptyView]; -} - - -#pragma mark - 工具方法 - -- (QMUITableView *)tableView { - if (!_tableView) { - [self loadViewIfNeeded]; - } - return _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.contentOffset.y + CGRectGetHeight(self.tableView.tableHeaderView.frame)); - [self.tableView setContentOffset:contentOffset animated:animated]; - self.hasHideTableHeaderViewInitial = YES; - } -} - -- (void)contentSizeCategoryDidChanged:(NSNotification *)notification { - [super contentSizeCategoryDidChanged:notification]; - [self.tableView reloadData]; -} - -- (void)setTableViewInitialContentInset:(UIEdgeInsets)tableViewInitialContentInset { - _tableViewInitialContentInset = tableViewInitialContentInset; - if (UIEdgeInsetsEqualToEdgeInsets(tableViewInitialContentInset, QMUICommonTableViewControllerInitialContentInsetNotSet)) { - self.automaticallyAdjustsScrollViewInsets = YES; - } else { - self.automaticallyAdjustsScrollViewInsets = NO; - } -} - -- (BOOL)shouldAdjustTableViewContentInsetsInitially { - BOOL shouldAdjust = !UIEdgeInsetsEqualToEdgeInsets(self.tableViewInitialContentInset, QMUICommonTableViewControllerInitialContentInsetNotSet); - return shouldAdjust; -} - -- (BOOL)shouldAdjustTableViewScrollIndicatorInsetsInitially { - BOOL shouldAdjust = !UIEdgeInsetsEqualToEdgeInsets(self.tableViewInitialScrollIndicatorInsets, QMUICommonTableViewControllerInitialContentInsetNotSet); - return shouldAdjust; -} - -#pragma mark - 空列表视图 QMUIEmptyView - -- (void)showEmptyView { - if (!self.emptyView) { - self.emptyView = [[QMUIEmptyView alloc] init]; - } - [self.tableView addSubview:self.emptyView]; - [self layoutEmptyView]; - if ([self shouldHideSearchBarWhenEmptyViewShowing] && self.tableView.tableHeaderView == self.searchBar) { - self.tableView.tableHeaderView = nil; - } -} - -- (void)hideEmptyView { - [self.emptyView removeFromSuperview]; -BeginIgnoreDeprecatedWarning - if (self.shouldShowSearchBar && [self shouldHideSearchBarWhenEmptyViewShowing] && self.tableView.tableHeaderView == nil) { -EndIgnoreDeprecatedWarning - [self initSearchController]; - // 隐藏 emptyView 后重新设置 tableHeaderView,会导致原先 shouldHideTableHeaderViewInitial 隐藏头部的操作被重置,所以下面的 force 参数要传 YES - // https://github.com/QMUI/QMUI_iOS/issues/128 - self.tableView.tableHeaderView = self.searchBar; - [self hideTableHeaderViewInitialIfCanWithAnimated:NO force:YES]; - } -} - -- (BOOL)layoutEmptyView { - if (!self.emptyView || !self.emptyView.superview) { - return NO; - } - // 当存在 tableHeaderView 时,emptyView 的高度为 tableView 的高度减去 headerView 的高度 - if (self.tableView.tableHeaderView) { - self.emptyView.frame = CGRectMake(0, CGRectGetMaxY(self.tableView.tableHeaderView.frame), CGRectGetWidth(self.tableView.bounds) - UIEdgeInsetsGetHorizontalValue(self.tableView.contentInset), CGRectGetHeight(self.tableView.bounds) - UIEdgeInsetsGetVerticalValue(self.tableView.contentInset) - CGRectGetMaxY(self.tableView.tableHeaderView.frame)); - } else { - self.emptyView.frame = CGRectMake(0, 0, CGRectGetWidth(self.tableView.bounds) - UIEdgeInsetsGetHorizontalValue(self.tableView.contentInset), CGRectGetHeight(self.tableView.bounds) - UIEdgeInsetsGetVerticalValue(self.tableView.contentInset)); - } - return YES; -} - -#pragma mark - - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - return 1; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return 0; -} - -// 默认拿title来构建一个view然后添加到viewForHeaderInSection里面,如果业务重写了viewForHeaderInSection,则titleForHeaderInSection被覆盖 -// viewForFooterInSection同上 -- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { - NSString *title = [self tableView:tableView realTitleForHeaderInSection:section]; - if (title) { - UITableViewHeaderFooterView *headerFooterView = [self tableHeaderFooterLabelInTableView:tableView identifier:@"headerTitle"]; - QMUILabel *label = (QMUILabel *)[headerFooterView.contentView viewWithTag:kSectionHeaderFooterLabelTag]; - label.text = title; - label.contentEdgeInsets = tableView.style == UITableViewStylePlain ? TableViewSectionHeaderContentInset : TableViewGroupedSectionHeaderContentInset; - label.font = tableView.style == UITableViewStylePlain ? TableViewSectionHeaderFont : TableViewGroupedSectionHeaderFont; - label.textColor = tableView.style == UITableViewStylePlain ? TableViewSectionHeaderTextColor : TableViewGroupedSectionHeaderTextColor; - label.backgroundColor = tableView.style == UITableViewStylePlain ? TableViewSectionHeaderBackgroundColor : UIColorClear; - CGFloat labelLimitWidth = CGRectGetWidth(tableView.bounds) - UIEdgeInsetsGetHorizontalValue(tableView.contentInset); - CGSize labelSize = [label sizeThatFits:CGSizeMake(labelLimitWidth, CGFLOAT_MAX)]; - label.frame = CGRectMake(0, 0, labelLimitWidth, labelSize.height); - return label; - } - return nil; -} - -// 同viewForHeaderInSection -- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section { - NSString *title = [self tableView:tableView realTitleForFooterInSection:section]; - if (title) { - UITableViewHeaderFooterView *headerFooterView = [self tableHeaderFooterLabelInTableView:tableView identifier:@"footerTitle"]; - QMUILabel *label = (QMUILabel *)[headerFooterView.contentView viewWithTag:kSectionHeaderFooterLabelTag]; - label.text = title; - label.contentEdgeInsets = tableView.style == UITableViewStylePlain ? TableViewSectionFooterContentInset : TableViewGroupedSectionFooterContentInset; - label.font = tableView.style == UITableViewStylePlain ? TableViewSectionFooterFont : TableViewGroupedSectionFooterFont; - label.textColor = tableView.style == UITableViewStylePlain ? TableViewSectionFooterTextColor : TableViewGroupedSectionFooterTextColor; - label.backgroundColor = tableView.style == UITableViewStylePlain ? TableViewSectionFooterBackgroundColor : UIColorClear; - CGFloat labelLimitWidth = CGRectGetWidth(tableView.bounds) - UIEdgeInsetsGetHorizontalValue(tableView.contentInset); - CGSize labelSize = [label sizeThatFits:CGSizeMake(labelLimitWidth, CGFLOAT_MAX)]; - label.frame = CGRectMake(0, 0, labelLimitWidth, labelSize.height); - return label; - } - return nil; -} - -- (UITableViewHeaderFooterView *)tableHeaderFooterLabelInTableView:(UITableView *)tableView identifier:(NSString *)identifier { - UITableViewHeaderFooterView *headerFooterView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:identifier]; - if (!headerFooterView) { - QMUILabel *label = [[QMUILabel alloc] init]; - label.tag = kSectionHeaderFooterLabelTag; - label.numberOfLines = 0; - headerFooterView = [[UITableViewHeaderFooterView alloc] initWithReuseIdentifier:identifier]; - [headerFooterView.contentView addSubview:label]; - } - return headerFooterView; -} - -/** - * iOS5之前的版本,如果viewForHeaderInSection返回的是nil,那么heightForHeaderInSection会自动计算数值为0,iOS5以及之后的版本,则不会自动计算,需要手动来计算heightForHeaderInSection。 - * - * Apple Document: Prior to iOS 5.0, table views would automatically resize the heights of headers to 0 for sections where tableView:viewForHeaderInSection: returned a nil view. In iOS 5.0 and later, you must return the actual height for each section header in this method. - * @see https://developer.apple.com/library/ios/DOCUMENTATION/UIKit/Reference/UITableViewDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/UITableViewDelegate/tableView%3aheightForHeaderInSection%3a - */ -- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { - if ([tableView.delegate respondsToSelector:@selector(tableView:viewForHeaderInSection:)]) { - UIView *view = [tableView.delegate tableView:tableView viewForHeaderInSection:section]; - if (view) { - CGFloat height = [view sizeThatFits:CGSizeMake(CGRectGetWidth(tableView.bounds), CGFLOAT_MAX)].height; - return fmax(height, tableView.style == UITableViewStylePlain ? TableViewSectionHeaderHeight : TableViewGroupedSectionHeaderHeight); - } - } - // 默认 plain 类型直接设置为 0,TableViewSectionHeaderHeight 是在需要重写 headerHeight 的时候才用的 - return tableView.style == UITableViewStylePlain ? 0 : TableViewGroupedSectionHeaderHeight; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { - if ([tableView.delegate respondsToSelector:@selector(tableView:viewForFooterInSection:)]) { - UIView *view = [tableView.delegate tableView:tableView viewForFooterInSection:section]; - if (view) { - return MAX(CGRectGetHeight(view.bounds), tableView.style == UITableViewStylePlain ? TableViewSectionFooterHeight : TableViewGroupedSectionFooterHeight); - } - } - // 默认 plain 类型直接设置为 0,TableViewSectionFooterHeight 是在需要重写 footerHeight 的时候才用的 - return tableView.style == UITableViewStylePlain ? 0 : TableViewGroupedSectionFooterHeight; -} - -// 是否有定义某个section的header title -- (NSString *)tableView:(UITableView *)tableView realTitleForHeaderInSection:(NSInteger)section { - if ([tableView.dataSource respondsToSelector:@selector(tableView:titleForHeaderInSection:)]) { - NSString *sectionTitle = [tableView.dataSource tableView:tableView titleForHeaderInSection:section]; - if (sectionTitle && sectionTitle.length > 0) { - return sectionTitle; - } - } - return nil; -} - -// 是否有定义某个section的footer title -- (NSString *)tableView:(UITableView *)tableView realTitleForFooterInSection:(NSInteger)section { - if ([tableView.dataSource respondsToSelector:@selector(tableView:titleForFooterInSection:)]) { - NSString *sectionFooter = [tableView.dataSource tableView:tableView titleForFooterInSection:section]; - if (sectionFooter && sectionFooter.length > 0) { - return sectionFooter; - } - } - return nil; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - return nil; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - return TableViewCellNormalHeight; -} - -@end - - -@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.view addSubview:self.tableView]; - } -} - -- (BOOL)shouldHideTableHeaderViewInitial { - return NO; -} - -@end - - -@implementation QMUICommonTableViewController (Search) - -- (BOOL)shouldShowSearchBar { - return _shouldShowSearchBar; -} - -- (void)setShouldShowSearchBar:(BOOL)shouldShowSearchBar { - BOOL isValueChanged = _shouldShowSearchBar != shouldShowSearchBar; - if (!isValueChanged) { - return; - } - - _shouldShowSearchBar = shouldShowSearchBar; - - if (shouldShowSearchBar) { - [self initSearchController]; - } else { - if (self.searchBar) { - if (self.tableView.tableHeaderView == self.searchBar) { - self.tableView.tableHeaderView = nil; - } - [self.searchBar removeFromSuperview]; - _searchBar = nil; - } - if (self.searchController) { - self.searchController.searchResultsDelegate = nil; - _searchController = nil; - } - } -} - -- (QMUISearchController *)searchController { - return _searchController; -} - -- (UISearchBar *)searchBar { - return _searchBar; -} - -- (void)initSearchController { -BeginIgnoreDeprecatedWarning - if ([self isViewLoaded] && self.shouldShowSearchBar && !self.searchController) { -EndIgnoreDeprecatedWarning - _searchController = [[QMUISearchController alloc] initWithContentsViewController:self]; - self.searchController.searchResultsDelegate = self; - self.searchController.searchBar.placeholder = @"搜索"; - self.tableView.tableHeaderView = self.searchController.searchBar; - _searchBar = self.searchController.searchBar; - } -} - -- (BOOL)shouldHideSearchBarWhenEmptyViewShowing { - return NO; -} - -#pragma mark - - -- (void)searchController:(QMUISearchController *)searchController updateResultsForSearchString:(NSString *)searchString { - -} - -@end diff --git a/QMUI/QMUIKit/UIMainFrame/QMUICommonViewController.h b/QMUI/QMUIKit/UIMainFrame/QMUICommonViewController.h deleted file mode 100644 index 17d12384..00000000 --- a/QMUI/QMUIKit/UIMainFrame/QMUICommonViewController.h +++ /dev/null @@ -1,154 +0,0 @@ -// -// QMUICommonViewController.h -// qmui -// -// Created by QQMail on 14-6-22. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import -#import "QMUINavigationController.h" - -@class QMUINavigationTitleView; -@class QMUIEmptyView; - - -/** - * 可作为项目内所有 `UIViewController` 的基类,提供的功能包括: - * - * 1. 自带顶部标题控件 `QMUINavigationTitleView`,支持loading、副标题、下拉菜单,设置标题依然使用系统的 `setTitle:` 方法 - * - * 2. 自带空界面控件 `QMUIEmptyView`,支持显示loading、空文案、操作按钮 - * - * 3. 自动在 `dealloc` 时移除所有注册到 `NSNotificationCenter` 里的监听,避免野指针 crash - * - * 4. 统一约定的常用接口,例如初始化 subview、设置顶部 `navigationItem`、底部 `toolbarItem`、响应系统的动态字体大小变化、...,从而保证相同类型的代码集中到同一个方法内,避免多人交叉维护时代码分散难以查找 - * - * 5. 配合 `QMUINavigationController` 使用时,可以得到 `willPopInNavigationControllerWithAnimated:`、`didPopInNavigationControllerWithAnimated:` 这两个时机 - * - * @see QMUINavigationTitleView - * @see QMUIEmptyView - */ -@interface QMUICommonViewController : UIViewController - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER; -- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER; - -/** - * 初始化时调用的方法,会在两个 NS_DESIGNATED_INITIALIZER 方法中被调用,所以子类如果需要同时支持两个 NS_DESIGNATED_INITIALIZER 方法,则建议把初始化时要做的事情放到这个方法里。否则仅需重写要支持的那个 NS_DESIGNATED_INITIALIZER 方法即可。 - */ -- (void)didInitialized NS_REQUIRES_SUPER; - -/** - * QMUICommonViewController默认都会增加一个QMUINavigationTitleView的titleView,然后重写了setTitle来间接设置titleView的值。所以设置title的时候就跟系统的接口一样:self.title = xxx。 - * - * 同时,QMUINavigationTitleView提供了更多的功能,具体可以参考QMUINavigationTitleView的文档。
- * @see QMUINavigationTitleView - */ -@property(nonatomic, strong, readonly) QMUINavigationTitleView *titleView; - -/** - * 修改当前界面要支持的横竖屏方向,默认为 SupportedOrientationMask - */ -@property(nonatomic, assign) UIInterfaceOrientationMask supportedOrientationMask; - -/** - * 空列表控件,支持显示提示文字、loading、操作按钮 - */ -@property(nonatomic, strong) QMUIEmptyView *emptyView; - -/// 当前self.emptyView是否显示 -@property(nonatomic, assign, readonly, getter = isEmptyViewShowing) BOOL emptyViewShowing; - -/** - * 显示emptyView - * emptyView 的以下系列接口可以按需进行重写 - * - * @see QMUIEmptyView - */ -- (void)showEmptyView; - -/** - * 显示loading的emptyView - */ -- (void)showEmptyViewWithLoading; - -/** - * 显示带text、detailText、button的emptyView - */ -- (void)showEmptyViewWithText:(NSString *)text - detailText:(NSString *)detailText - buttonTitle:(NSString *)buttonTitle - buttonAction:(SEL)action; - -/** - * 显示带image、text、detailText、button的emptyView - */ -- (void)showEmptyViewWithImage:(UIImage *)image - text:(NSString *)text - detailText:(NSString *)detailText - buttonTitle:(NSString *)buttonTitle - buttonAction:(SEL)action; - -/** - * 显示带loading、image、text、detailText、button的emptyView - */ -- (void)showEmptyViewWithLoading:(BOOL)showLoading - image:(UIImage *)image - text:(NSString *)text - detailText:(NSString *)detailText - buttonTitle:(NSString *)buttonTitle - buttonAction:(SEL)action; - -/** - * 隐藏emptyView - */ -- (void)hideEmptyView; - -/** - * 布局emptyView,如果emptyView没有被初始化或者没被添加到界面上,则直接忽略掉。 - * - * 如果有特殊的情况,子类可以重写,实现自己的样式 - * - * @return YES表示成功进行一次布局,NO表示本次调用并没有进行布局操作(例如emptyView还没被初始化) - */ -- (BOOL)layoutEmptyView; - -@end - - -@interface QMUICommonViewController (QMUISubclassingHooks) - -/** - * 负责初始化和设置controller里面的view,也就是self.view的subView。目的在于分类代码,所以与view初始化的相关代码都写在这里。 - * - * @warning initSubviews只负责subviews的init,不负责布局。布局相关的代码应该写在 viewDidLayoutSubviews - */ -- (void)initSubviews NS_REQUIRES_SUPER; - -/** - * 负责设置和更新navigationItem,包括title、leftBarButtonItem、rightBarButtonItem。viewDidLoad里面会自动调用,允许手动调用更新。目的在于分类代码,所有与navigationItem相关的代码都写在这里。在需要修改navigationItem的时候都只调用这个接口。 - * - * @param isInEditMode 是否用于编辑模式下 - * @param animated 是否使用动画呈现 - */ -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated NS_REQUIRES_SUPER; - -/** - * 负责设置和更新toolbarItem。在viewWillAppear里面自动调用(因为toolbar是navigationController的,是每个界面公用的,所以必须在每个界面的viewWillAppear时更新,不能放在viewDidLoad里),允许手动调用。目的在于分类代码,所有与toolbarItem相关的代码都写在这里。在需要修改toolbarItem的时候都只调用这个接口。 - * - * @param isInEditMode 是否用于编辑模式下 - * @param animated 是否使用动画呈现 - */ -- (void)setToolbarItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated NS_REQUIRES_SUPER; - -/** - * 动态字体的回调函数。 - * - * 交给子类重写,当系统字体发生变化的时候,会调用这个方法,一些font的设置或者reloadData可以放在里面 - * - * @param notification test - */ -- (void)contentSizeCategoryDidChanged:(NSNotification *)notification; - -@end diff --git a/QMUI/QMUIKit/UIMainFrame/QMUICommonViewController.m b/QMUI/QMUIKit/UIMainFrame/QMUICommonViewController.m deleted file mode 100644 index 824c6b3d..00000000 --- a/QMUI/QMUIKit/UIMainFrame/QMUICommonViewController.m +++ /dev/null @@ -1,215 +0,0 @@ -// -// QMUICommonViewController.m -// qmui -// -// Created by QQMail on 14-6-22. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUICommonViewController.h" -#import "QMUICore.h" -#import "QMUINavigationTitleView.h" -#import "QMUIEmptyView.h" -#import "NSString+QMUI.h" -#import "UIViewController+QMUI.h" - -@interface QMUICommonViewController () - -@property(nonatomic,strong,readwrite) QMUINavigationTitleView *titleView; -@end - -@implementation QMUICommonViewController - -#pragma mark - 生命周期 - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - self.titleView = [[QMUINavigationTitleView alloc] init]; - self.titleView.title = self.title;// 从 storyboard 初始化的话,可能带有 self.title 的值 - - self.hidesBottomBarWhenPushed = HidesBottomBarWhenPushedInitially; - - // 不管navigationBar的backgroundImage如何设置,都让布局撑到屏幕顶部,方便布局的统一 - self.extendedLayoutIncludesOpaqueBars = YES; - - self.supportedOrientationMask = SupportedOrientationMask; - - // 动态字体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; - } - } - [self initSubviews]; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - [self layoutEmptyView]; -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - [self setNavigationItemsIsInEditMode:NO animated:NO]; - [self setToolbarItemsIsInEditMode:NO animated:NO]; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - - -#pragma mark - 空列表视图 QMUIEmptyView - -- (void)showEmptyView { - if (!self.emptyView) { - self.emptyView = [[QMUIEmptyView alloc] initWithFrame:self.view.bounds]; - } - [self.view addSubview:self.emptyView]; -} - -- (void)hideEmptyView { - [self.emptyView removeFromSuperview]; -} - -- (BOOL)isEmptyViewShowing { - return self.emptyView && self.emptyView.superview; -} - -- (void)showEmptyViewWithLoading { - [self showEmptyView]; - [self.emptyView setImage:nil]; - [self.emptyView setLoadingViewHidden:NO]; - [self.emptyView setTextLabelText:nil]; - [self.emptyView setDetailTextLabelText:nil]; - [self.emptyView setActionButtonTitle:nil]; -} - -- (void)showEmptyViewWithText:(NSString *)text - detailText:(NSString *)detailText - buttonTitle:(NSString *)buttonTitle - buttonAction:(SEL)action { - [self showEmptyViewWithLoading:NO image:nil text:text detailText:detailText buttonTitle:buttonTitle buttonAction:action]; -} - -- (void)showEmptyViewWithImage:(UIImage *)image - text:(NSString *)text - detailText:(NSString *)detailText - buttonTitle:(NSString *)buttonTitle - buttonAction:(SEL)action { - [self showEmptyViewWithLoading:NO image:image text:text detailText:detailText buttonTitle:buttonTitle buttonAction:action]; -} - -- (void)showEmptyViewWithLoading:(BOOL)showLoading - image:(UIImage *)image - text:(NSString *)text - detailText:(NSString *)detailText - buttonTitle:(NSString *)buttonTitle - buttonAction:(SEL)action { - [self showEmptyView]; - [self.emptyView setLoadingViewHidden:!showLoading]; - [self.emptyView setImage:image]; - [self.emptyView setTextLabelText:text]; - [self.emptyView setDetailTextLabelText:detailText]; - [self.emptyView setActionButtonTitle:buttonTitle]; - [self.emptyView.actionButton removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents]; - [self.emptyView.actionButton addTarget:self action:action forControlEvents:UIControlEventTouchUpInside]; -} - -- (BOOL)layoutEmptyView { - if (self.emptyView) { - // 由于为self.emptyView设置frame时会调用到self.view,为了避免导致viewDidLoad提前触发,这里需要判断一下self.view是否已经被初始化 - BOOL viewDidLoad = self.emptyView.superview || [self isViewLoaded]; - if (viewDidLoad) { - CGSize newEmptyViewSize = self.emptyView.superview.bounds.size; - CGSize oldEmptyViewSize = self.emptyView.frame.size; - if (!CGSizeEqualToSize(newEmptyViewSize, oldEmptyViewSize)) { - self.emptyView.frame = CGRectMake(CGRectGetMinX(self.emptyView.frame), CGRectGetMinY(self.emptyView.frame), newEmptyViewSize.width, newEmptyViewSize.height); - } - return YES; - } - } - - return NO; -} - -#pragma mark - 屏幕旋转 - -- (BOOL)shouldAutorotate { - return YES; -} - -- (UIInterfaceOrientationMask)supportedInterfaceOrientations { - return self.supportedOrientationMask; -} - -#pragma mark - - -- (BOOL)shouldSetStatusBarStyleLight { - return StatusbarStyleLightInitially; -} - -- (UIStatusBarStyle)preferredStatusBarStyle { - return StatusbarStyleLightInitially ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; -} - -- (BOOL)preferredNavigationBarHidden { - return NavigationBarHiddenInitially; -} - -- (void)viewControllerKeepingAppearWhenSetViewControllersWithAnimated:(BOOL)animated { - // 通常和 viewWillAppear: 里做的事情保持一致 - [self setNavigationItemsIsInEditMode:NO animated:NO]; - [self setToolbarItemsIsInEditMode:NO animated:NO]; -} - -@end - -@implementation QMUICommonViewController (QMUISubclassingHooks) - -- (void)initSubviews { - // 子类重写 -} - -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - // 子类重写 - if (IOS_VERSION < 8.0 && [NSStringFromClass(self.navigationItem.class) qmui_includesString:@"UISearchBarNavigationItem"]) { - // iOS 7 下,UISearchDisplayController.displaysSearchBarInNavigationBar 为 YES 时,不允许修改 self.navigationItem - } else { - self.navigationItem.titleView = self.titleView; - } -} - -- (void)setToolbarItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - // 子类重写 -} - -- (void)contentSizeCategoryDidChanged:(NSNotification *)notification { - // 子类重写 -} - -@end diff --git a/QMUI/QMUIKit/UIMainFrame/QMUINavigationController.h b/QMUI/QMUIKit/UIMainFrame/QMUINavigationController.h deleted file mode 100644 index 78a22857..00000000 --- a/QMUI/QMUIKit/UIMainFrame/QMUINavigationController.h +++ /dev/null @@ -1,135 +0,0 @@ -// -// QMUINavigationController.h -// qmui -// -// Created by QQMail on 14-6-24. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import - - -@interface QMUINavigationController : UINavigationController - -/** - * 初始化时调用的方法,会在 initWithNibName:bundle: 和 initWithCoder: 这两个指定的初始化方法中被调用,所以子类如果需要同时支持两个初始化方法,则建议把初始化时要做的事情放到这个方法里。否则仅需重写要支持的那个初始化方法即可。 - */ -- (void)didInitialized NS_REQUIRES_SUPER; - -@end - -@interface QMUINavigationController (UISubclassingHooks) - -/** - * 每个界面Controller在即将展示的时候被调用,在`UINavigationController`的方法`navigationController:willShowViewController:animated:`中会自动被调用,同时因为如果把一个界面dismiss后回来此时并不会调用`navigationController:willShowViewController`,所以需要在`viewWillAppear`里面也会调用一次。 - */ -- (void)willShowViewController:(nonnull UIViewController *)viewController animated:(BOOL)animated NS_REQUIRES_SUPER; - -/** - * 同上 - */ -- (void)didShowViewController:(nonnull UIViewController *)viewController animated:(BOOL)animated NS_REQUIRES_SUPER; - -@end - -/** - * 若某些 UIViewController 实现了 QMUINavigationControllerDelegate,则在 QMUINavigationController 里显示时,可以很方便地控制 viewController 之间的样式切换(例如状态栏、导航栏等),不用在每个 viewController 的 viewWillAppear: 或viewWillDisappear: 里面单独控制。 - * QMUICommonViewController、QMUICommonTableViewController 默认实现了这个协议。 - */ -@protocol QMUINavigationControllerDelegate - -@required - -/// 是否需要将状态栏改为浅色文字,默认为宏StatusbarStyleLightInitially的值 -- (BOOL)shouldSetStatusBarStyleLight; - -/// 设置每个界面导航栏的显示/隐藏,为了减少对项目的侵入性,默认不开启这个接口的功能,只有当 shouldCustomNavigationBarTransitionIfBarHiddenable 返回 YES 时才会开启此功能。如果需要全局开启,那么就在 Controller 基类里面返回 YES;如果是老项目并不想全局使用此功能,那么则可以在单独的界面里面开启。 -- (BOOL)preferredNavigationBarHidden; - -@optional - -/** - * 在 self.navigationController 进行以下 4 个操作前,相应的 viewController 的 willPopInNavigationControllerWithAnimated: 方法会被调用: - * 1. popViewControllerAnimated: - * 2. popToViewController:animated: - * 3. popToRootViewControllerAnimated: - * 4. setViewControllers:animated: - * - * 此时 self 仍存在于 self.navigationController.viewControllers 堆栈内。 - * - * 在 ARC 环境下,viewController 可能被放在 autorelease 池中,因此 viewController 被pop后不一定立即被销毁,所以一些对实时性要求很高的内存管理逻辑可以写在这里(而不是写在dealloc内) - * - * @warning 不要尝试将 willPopInNavigationControllerWithAnimated: 视为点击返回按钮的回调,因为导致 viewController 被 pop 的情况不止点击返回按钮这一途径。系统的返回按钮是无法添加回调的,只能使用自定义的返回按钮。 - */ -- (void)willPopInNavigationControllerWithAnimated:(BOOL)animated; - -/** - * 在 self.navigationController 进行以下 4 个操作后,相应的 viewController 的 didPopInNavigationControllerWithAnimated: 方法会被调用: - * 1. popViewControllerAnimated: - * 2. popToViewController:animated: - * 3. popToRootViewControllerAnimated: - * 4. setViewControllers:animated: - * - * @warning 此时 self 已经不在 viewControllers 数组内 - */ -- (void)didPopInNavigationControllerWithAnimated:(BOOL)animated; - -/** - * 当通过 setViewControllers:animated: 来修改 viewController 的堆栈时,如果参数 viewControllers.lastObject 与当前的 self.viewControllers.lastObject 不相同,则意味着会产生界面的切换,这种情况系统会自动调用两个切换的界面的生命周期方法,但如果两者相同,则意味着并不会产生界面切换,此时之前就已经在显示的那个 viewController 的 viewWillAppear:、viewDidAppear: 并不会被调用,那如果用户确实需要在这个时候修改一些界面元素,则找不到一个时机。所以这个方法就是提供这样一个时机给用户修改界面元素。 - */ -- (void)viewControllerKeepingAppearWhenSetViewControllersWithAnimated:(BOOL)animated; - -/// 设置titleView的tintColor -- (nullable UIColor *)titleViewTintColor; - -/// 设置导航栏的背景图,默认为NavBarBackgroundImage -- (nullable UIImage *)navigationBarBackgroundImage; - -/// 设置导航栏底部的分隔线图片,默认为NavBarShadowImage,必须在navigationBar设置了背景图后才有效 -- (nullable UIImage *)navigationBarShadowImage; - -/// 设置当前导航栏的UIBarButtonItem的tintColor,默认为NavBarTintColor -- (nullable UIColor *)navigationBarTintColor; - -/// 设置系统返回按钮title,如果返回nil则使用系统默认的返回按钮标题 -- (nullable NSString *)backBarButtonItemTitleWithPreviousViewController:(nullable UIViewController *)viewController; - -/** - * 设置当前导航栏是否需要使用自定义的 push/pop transition 效果,默认返回NO。
- * 因为系统的UINavigationController只有一个navBar,所以会导致在切换controller的时候,如果两个controller的navBar状态不一致(包括backgroundImage、shadowImage、barTintColor等等),就会导致在刚要切换的瞬间,navBar的状态都立马变成下一个controller所设置的样式了,为了解决这种情况,QMUI给出了一个方案,有四个方法可以决定你在转场的时候要不要使用自定义的navBar来模仿真实的navBar。具体方法如下: - * @see UINavigationController+NavigationBarTransition.h - */ -- (BOOL)shouldCustomNavigationBarTransitionWhenPushAppearing; - -/** - * 同上 - * @see UINavigationController+NavigationBarTransition.h - */ -- (BOOL)shouldCustomNavigationBarTransitionWhenPushDisappearing; - -/** - * 同上 - * @see UINavigationController+NavigationBarTransition.h - */ -- (BOOL)shouldCustomNavigationBarTransitionWhenPopAppearing; - -/** - * 同上 - * @see UINavigationController+NavigationBarTransition.h - */ -- (BOOL)shouldCustomNavigationBarTransitionWhenPopDisappearing; - -/** - * 自定义navBar效果过程中UINavigationController的containerView的背景色 - * @see UINavigationController+NavigationBarTransition.h - */ -- (nullable UIColor *)containerViewBackgroundColorWhenTransitioning; - -/** - * 当切换界面时,如果不同界面导航栏的显示状态不同,可以通过 shouldCustomNavigationBarTransitionIfBarHiddenable 设置是否需要接管导航栏的显示和隐藏。从而不需要在各自的界面的 viewWillappear 和 viewWillDisappear 里面去管理导航栏的状态。 - * @see UINavigationController+NavigationBarTransition.h - * @see preferredNavigationBarHidden - */ -- (BOOL)shouldCustomNavigationBarTransitionIfBarHiddenable; - -@end diff --git a/QMUI/QMUIKit/UIMainFrame/QMUINavigationController.m b/QMUI/QMUIKit/UIMainFrame/QMUINavigationController.m deleted file mode 100644 index c1b23157..00000000 --- a/QMUI/QMUIKit/UIMainFrame/QMUINavigationController.m +++ /dev/null @@ -1,392 +0,0 @@ -// -// QMUINavigationController.m -// qmui -// -// Created by QQMail on 14-6-24. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QMUINavigationController.h" -#import "QMUICore.h" -#import "QMUINavigationTitleView.h" -#import "QMUICommonViewController.h" -#import "QMUIButton.h" -#import "UIViewController+QMUI.h" -#import "UINavigationController+QMUI.h" - - -@interface UIViewController (QMUINavigationController) - -@property(nonatomic, assign) BOOL qmui_isViewWillAppeare; - -@end - -@implementation UIViewController (QMUINavigationController) - -+ (void)load { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - Class cls = [self class]; - ReplaceMethod(cls, @selector(viewWillAppear:), @selector(observe_viewWillAppear:)); - ReplaceMethod(cls, @selector(viewDidDisappear:), @selector(observe_viewDidDisappear:)); - }); -} - -- (void)observe_viewWillAppear:(BOOL)animated { - [self observe_viewWillAppear:animated]; - self.qmui_isViewWillAppeare = YES; -} - -- (void)observe_viewDidDisappear:(BOOL)animated { - [self observe_viewDidDisappear:animated]; - self.qmui_isViewWillAppeare = NO; -} - -- (BOOL)qmui_isViewWillAppeare { - return [objc_getAssociatedObject(self, _cmd) boolValue]; -} - -- (void)setQmui_isViewWillAppeare:(BOOL)qmui_isViewWillAppeare { - [self willChangeValueForKey:@"qmui_isViewWillAppeare"]; - objc_setAssociatedObject(self, @selector(qmui_isViewWillAppeare), [[NSNumber alloc] initWithBool:qmui_isViewWillAppeare], OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self didChangeValueForKey:@"qmui_isViewWillAppeare"]; -} - -@end - - -@interface QMUINavigationController () - -/// 记录当前是否正在 push/pop 界面的动画过程,如果动画尚未结束,不应该继续 push/pop 其他界面 -@property(nonatomic, assign) BOOL isViewControllerTransiting; - -/// 即将要被pop的controller -@property(nonatomic, weak) UIViewController *viewControllerPopping; - -/** - * 因为QMUINavigationController把delegate指向了自己来做一些基类要做的事情,所以如果当外面重新指定了delegate,那么就会覆盖原本的delegate。
- * 为了避免这个问题,并且外面也可以实现实现navigationController的delegate方法,这里使用delegateProxy来保存外面指定的delegate,然后在基类的delegate方法实现里面会去调用delegateProxy的方法实现。 - */ -@property(nonatomic, weak) id delegateProxy; - -@end - -@implementation QMUINavigationController - -#pragma mark - 生命周期函数 && 基类方法重写 - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - // UIView.tintColor 并不支持 UIAppearance 协议,所以不能通过 appearance 来设置,只能在实例里设置 - self.navigationBar.tintColor = NavBarTintColor; - self.toolbar.tintColor = ToolBarTintColor; -} - -- (void)dealloc { - self.delegate = nil; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - if (!self.delegate) { - self.delegate = self; - } - // 手势允许多次addTarget - [self.interactivePopGestureRecognizer addTarget:self action:@selector(handleInteractivePopGestureRecognizer:)]; -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - [self willShowViewController:self.topViewController animated:animated]; -} - -- (void)viewDidAppear:(BOOL)animated { - [super viewDidAppear:animated]; - [self didShowViewController:self.topViewController animated:animated]; -} - -- (UIViewController *)popViewControllerAnimated:(BOOL)animated { - // 从横屏界面pop 到竖屏界面,系统会调用两次 popViewController,如果这里加这个 if 判断,会误拦第二次 pop,导致错误 -// if (self.isViewControllerTransiting) { -// NSAssert(NO, @"isViewControllerTransiting = YES, %s, self.viewControllers = %@", __func__, self.viewControllers); -// return nil; -// } - - if (self.viewControllers.count < 2) { - // 只剩 1 个 viewController 或者不存在 viewController 时,调用 popViewControllerAnimated: 后不会有任何变化,所以不需要触发 willPop / didPop - return [super popViewControllerAnimated:animated]; - } - - if (animated) { - self.isViewControllerTransiting = YES; - } - - UIViewController *viewController = [self topViewController]; - self.viewControllerPopping = viewController; - if ([viewController respondsToSelector:@selector(willPopInNavigationControllerWithAnimated:)]) { - [((UIViewController *)viewController) willPopInNavigationControllerWithAnimated:animated]; - } - viewController = [super popViewControllerAnimated:animated]; - if ([viewController respondsToSelector:@selector(didPopInNavigationControllerWithAnimated:)]) { - [((UIViewController *)viewController) didPopInNavigationControllerWithAnimated:animated]; - } - return viewController; -} - -- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated { - // 从横屏界面pop 到竖屏界面,系统会调用两次 popViewController,如果这里加这个 if 判断,会误拦第二次 pop,导致错误 -// if (self.isViewControllerTransiting) { -// NSAssert(NO, @"isViewControllerTransiting = YES, %s, self.viewControllers = %@", __func__, self.viewControllers); -// return nil; -// } - - if (!viewController || self.topViewController == viewController) { - // 当要被 pop 到的 viewController 已经处于最顶层时,调用 super 默认也是什么都不做,所以直接 return 掉 - return [super popToViewController:viewController animated:animated]; - } - - if (animated) { - self.isViewControllerTransiting = YES; - } - - self.viewControllerPopping = self.topViewController; - - // will pop - for (NSInteger i = self.viewControllers.count - 1; i > 0; i--) { - UIViewController *viewControllerPopping = self.viewControllers[i]; - if (viewControllerPopping == viewController) { - break; - } - - if ([viewControllerPopping respondsToSelector:@selector(willPopInNavigationControllerWithAnimated:)]) { - BOOL animatedArgument = i == self.viewControllers.count - 1 ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop - [((UIViewController *)viewControllerPopping) willPopInNavigationControllerWithAnimated:animatedArgument]; - } - } - - NSArray *poppedViewControllers = [super popToViewController:viewController animated:animated]; - - // did pop - for (NSInteger i = poppedViewControllers.count - 1; i >= 0; i--) { - UIViewController *viewControllerPopped = poppedViewControllers[i]; - if ([viewControllerPopped respondsToSelector:@selector(didPopInNavigationControllerWithAnimated:)]) { - BOOL animatedArgument = i == poppedViewControllers.count - 1 ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop - [((UIViewController *)viewControllerPopped) didPopInNavigationControllerWithAnimated:animatedArgument]; - } - } - - return poppedViewControllers; -} - -- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated { - // 从横屏界面pop 到竖屏界面,系统会调用两次 popViewController,如果这里加这个 if 判断,会误拦第二次 pop,导致错误 -// if (self.isViewControllerTransiting) { -// NSAssert(NO, @"isViewControllerTransiting = YES, %s, self.viewControllers = %@", __func__, self.viewControllers); -// return nil; -// } - - // 在配合 tabBarItem 使用的情况下,快速重复点击相同 item 可能会重复调用 popToRootViewControllerAnimated:,而此时其实已经处于 rootViewController 了,就没必要继续走后续的流程,否则一些变量会得不到重置。 - if (self.topViewController == self.qmui_rootViewController) { - return nil; - } - - if (animated) { - self.isViewControllerTransiting = YES; - } - - self.viewControllerPopping = self.topViewController; - - // will pop - for (NSInteger i = self.viewControllers.count - 1; i > 0; i--) { - UIViewController *viewControllerPopping = self.viewControllers[i]; - if ([viewControllerPopping respondsToSelector:@selector(willPopInNavigationControllerWithAnimated:)]) { - BOOL animatedArgument = i == self.viewControllers.count - 1 ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop - [((UIViewController *)viewControllerPopping) willPopInNavigationControllerWithAnimated:animatedArgument]; - } - } - - NSArray * poppedViewControllers = [super popToRootViewControllerAnimated:animated]; - - // did pop - for (NSInteger i = poppedViewControllers.count - 1; i >= 0; i--) { - UIViewController *viewControllerPopped = poppedViewControllers[i]; - if ([viewControllerPopped respondsToSelector:@selector(didPopInNavigationControllerWithAnimated:)]) { - BOOL animatedArgument = i == poppedViewControllers.count - 1 ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop - [((UIViewController *)viewControllerPopped) didPopInNavigationControllerWithAnimated:animatedArgument]; - } - } - return poppedViewControllers; -} - -- (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated { - UIViewController *topViewController = self.topViewController; - - // will pop - NSMutableArray *viewControllersPopping = self.viewControllers.mutableCopy; - [viewControllersPopping removeObjectsInArray:viewControllers]; - [viewControllersPopping enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(UIViewController * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - if ([obj respondsToSelector:@selector(willPopInNavigationControllerWithAnimated:)]) { - BOOL animatedArgument = obj == topViewController ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop - [((UIViewController *)obj) willPopInNavigationControllerWithAnimated:animatedArgument]; - } - }]; - - [super setViewControllers:viewControllers animated:animated]; - - // did pop - [viewControllersPopping enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(UIViewController * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - if ([obj respondsToSelector:@selector(didPopInNavigationControllerWithAnimated:)]) { - BOOL animatedArgument = obj == topViewController ? animated : NO;// 只有当前可视的那个 viewController 的 animated 是跟随参数走的,其他 viewController 由于不可视,不管参数的值为多少,都认为是无动画地 pop - [((UIViewController *)obj) didPopInNavigationControllerWithAnimated:animatedArgument]; - } - }]; - - // 操作前后如果 topViewController 没发生变化,则为它调用一个特殊的时机 - if (topViewController == viewControllers.lastObject) { - if ([topViewController respondsToSelector:@selector(viewControllerKeepingAppearWhenSetViewControllersWithAnimated:)]) { - [((UIViewController *)topViewController) viewControllerKeepingAppearWhenSetViewControllersWithAnimated:animated]; - } - } -} - -- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { - if (self.isViewControllerTransiting || !viewController) { - NSAssert(NO, @"%s, isViewControllerTransiting = %@, viewController = %@, self.viewControllers = %@", __func__, StringFromBOOL(self.isViewControllerTransiting), viewController, self.viewControllers); - return; - } - - if (animated) { - self.isViewControllerTransiting = YES; - } - - UIViewController *currentViewController = self.topViewController; - if (currentViewController) { - if (!NeedsBackBarButtonItemTitle) { - currentViewController.navigationItem.backBarButtonItem = [QMUINavigationButton barButtonItemWithType:QMUINavigationButtonTypeNormal title:@"" position:QMUINavigationButtonPositionLeft target:nil action:NULL]; - } else { - UIViewController *vc = (UIViewController *)viewController; - if ([vc respondsToSelector:@selector(backBarButtonItemTitleWithPreviousViewController:)]) { - NSString *title = [vc backBarButtonItemTitleWithPreviousViewController:currentViewController]; - currentViewController.navigationItem.backBarButtonItem = [QMUINavigationButton barButtonItemWithType:QMUINavigationButtonTypeNormal title:title position:QMUINavigationButtonPositionLeft target:nil action:NULL]; - } - } - } - [super pushViewController:viewController animated:animated]; -} - -- (void)setDelegate:(id)delegate { - self.delegateProxy = delegate != self ? delegate : nil; - [super setDelegate:delegate ? self : nil]; -} - -// 重写这个方法才能让 viewControllers 对 statusBar 的控制生效 -- (UIViewController *)childViewControllerForStatusBarStyle { - return self.topViewController; -} - -#pragma mark - 自定义方法 - -// 接管系统手势返回的回调 -- (void)handleInteractivePopGestureRecognizer:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer { - UIGestureRecognizerState state = gestureRecognizer.state; - if (state == UIGestureRecognizerStateBegan) { - [self.viewControllerPopping addObserver:self forKeyPath:@"qmui_isViewWillAppeare" options:NSKeyValueObservingOptionNew context:nil]; - } else if (state == UIGestureRecognizerStateEnded) { - if (CGRectGetMinX(self.topViewController.view.superview.frame) < 0) { - // by molice:只是碰巧发现如果是手势返回取消时,不管在哪个位置取消,self.topViewController.view.superview.frame.orgin.x必定是-124,所以用这个<0的条件来判断 - QMUILog(@"手势返回放弃了"); - } else { - QMUILog(@"执行手势返回"); - } - } -} - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if ([keyPath isEqualToString:@"qmui_isViewWillAppeare"]) { - [self.viewControllerPopping removeObserver:self forKeyPath:@"qmui_isViewWillAppeare"]; - NSNumber *newValue = change[NSKeyValueChangeNewKey]; - if (newValue.boolValue) { - [self navigationController:self willShowViewController:self.viewControllerPopping animated:YES]; - self.viewControllerPopping = nil; - self.isViewControllerTransiting = NO; - } - } -} - -#pragma mark - - -// 注意如果实现了某一个navigationController的delegate方法,必须同时检查并且调用delegateProxy相对应的方法 - -- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated { - [self willShowViewController:viewController animated:animated]; - if ([self.delegateProxy respondsToSelector:_cmd]) { - [self.delegateProxy navigationController:navigationController willShowViewController:viewController animated:animated]; - } -} - -- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { - self.viewControllerPopping = nil; - self.isViewControllerTransiting = NO; - [self didShowViewController:viewController animated:animated]; - if ([self.delegateProxy respondsToSelector:_cmd]) { - [self.delegateProxy navigationController:navigationController didShowViewController:viewController animated:animated]; - } -} - -- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { - return [super methodSignatureForSelector:aSelector] ?: [(id)self.delegateProxy methodSignatureForSelector:aSelector]; -} - -- (void)forwardInvocation:(NSInvocation *)anInvocation { - if ([(id)self.delegateProxy respondsToSelector:anInvocation.selector]) { - [anInvocation invokeWithTarget:(id)self.delegateProxy]; - } -} - -- (BOOL)respondsToSelector:(SEL)aSelector { - return [super respondsToSelector:aSelector] || ([self shouldRespondDelegeateProxyWithSelector:aSelector] && [self.delegateProxy respondsToSelector:aSelector]); -} - -- (BOOL)shouldRespondDelegeateProxyWithSelector:(SEL)aSelctor { - // 目前仅支持下面两个delegate方法,如果需要增加全局的自定义转场动画,可以额外增加多上面注释的两个方法。 - return [NSStringFromSelector(aSelctor) isEqualToString:@"navigationController:willShowViewController:animated:"] || - [NSStringFromSelector(aSelctor) isEqualToString:@"navigationController:didShowViewController:animated:"]; -} - -#pragma mark - 屏幕旋转 - -- (BOOL)shouldAutorotate { - return [self.topViewController qmui_hasOverrideUIKitMethod:_cmd] ? [self.topViewController shouldAutorotate] : YES; -} - -- (UIInterfaceOrientationMask)supportedInterfaceOrientations { - return [self.topViewController qmui_hasOverrideUIKitMethod:_cmd] ? [self.topViewController supportedInterfaceOrientations] : SupportedOrientationMask; -} - -@end - - -@implementation QMUINavigationController (UISubclassingHooks) - -- (void)willShowViewController:(UIViewController *)viewController animated:(BOOL)animated { - // 子类可以重写 -} - -- (void)didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { - // 子类可以重写 -} - -@end diff --git a/QMUI/QMUIKit/UIMainFrame/QMUITabBarViewController.h b/QMUI/QMUIKit/UIMainFrame/QMUITabBarViewController.h deleted file mode 100644 index 23debcdf..00000000 --- a/QMUI/QMUIKit/UIMainFrame/QMUITabBarViewController.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// QMUITabBarViewController.h -// qmui -// -// Created by QQMail on 15/3/29. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import - -/** - * 建议作为项目里 tabBarController 的基类,内部处理了几件事情: - * 1. 配合配置表修改 tabBar 的样式。 - * 2. 管理界面支持显示的方向。 - * - * @warning 当你需要实现“tabBarController 首页那几个界面显示 tabBar,而 push 进去的所有子界面都隐藏 tabBar”的效果时,可将配置表里的 HidesBottomBarWhenPushedInitially 改为 YES,然后手动将 tabBarController 首页的那几个界面的 hidesBottomBarWhenPushed 属性改为 NO,即可实现。 - */ -@interface QMUITabBarViewController : UITabBarController - -/** - * 初始化时调用的方法,会在 initWithNibName:bundle: 和 initWithCoder: 这两个指定的初始化方法中被调用,所以子类如果需要同时支持两个初始化方法,则建议把初始化时要做的事情放到这个方法里。否则仅需重写要支持的那个初始化方法即可。 - */ -- (void)didInitialized NS_REQUIRES_SUPER; -@end diff --git a/QMUI/QMUIKit/UIMainFrame/QMUITabBarViewController.m b/QMUI/QMUIKit/UIMainFrame/QMUITabBarViewController.m deleted file mode 100644 index a484605c..00000000 --- a/QMUI/QMUIKit/UIMainFrame/QMUITabBarViewController.m +++ /dev/null @@ -1,44 +0,0 @@ -// -// QMUITabBarViewController.m -// qmui -// -// Created by QQMail on 15/3/29. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUITabBarViewController.h" -#import "QMUICore.h" -#import "UIViewController+QMUI.h" - -@implementation QMUITabBarViewController - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self didInitialized]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder { - if (self = [super initWithCoder:aDecoder]) { - [self didInitialized]; - } - return self; -} - -- (void)didInitialized { - // UIView.tintColor 并不支持 UIAppearance 协议,所以不能通过 appearance 来设置,只能在实例里设置 - self.tabBar.tintColor = TabBarTintColor; -} - -#pragma mark - 屏幕旋转 - -- (BOOL)shouldAutorotate { - return [self.selectedViewController qmui_hasOverrideUIKitMethod:_cmd] ? [self.selectedViewController shouldAutorotate] : YES; -} - -- (UIInterfaceOrientationMask)supportedInterfaceOrientations { - return [self.selectedViewController qmui_hasOverrideUIKitMethod:_cmd] ? [self.selectedViewController supportedInterfaceOrientations] : SupportedOrientationMask; -} - -@end diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Info.plist b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Info.plist deleted file mode 100644 index 682aeacb..00000000 --- a/QMUI/QMUIKit/UIResources/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/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@2x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@2x.png deleted file mode 100644 index 5542e83d..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@3x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@3x.png deleted file mode 100644 index 42879851..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_emotion_delete@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@2x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@2x.png deleted file mode 100644 index 91afabd5..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@3x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@3x.png deleted file mode 100644 index f26dcc97..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault_small@2x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault_small@2x.png deleted file mode 100644 index 83fb994c..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault_small@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault_small@3x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault_small@3x.png deleted file mode 100644 index 035404a5..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_icloud_download_fault_small@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@2x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@2x.png deleted file mode 100644 index 3df2019b..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@3x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@3x.png deleted file mode 100644 index 41e8cf0d..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@2x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@2x.png deleted file mode 100644 index b10aa3e3..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@3x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@3x.png deleted file mode 100644 index ca0885f5..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_checkbox_checked@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@2x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@2x.png deleted file mode 100644 index f3cfb98d..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@3x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@3x.png deleted file mode 100644 index f3b9dd68..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_pickerImage_video_mark@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@2x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@2x.png deleted file mode 100644 index 6188d1dc..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@3x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@3x.png deleted file mode 100644 index 44aaa1a5..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@2x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@2x.png deleted file mode 100644 index 2e79b6d9..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@3x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@3x.png deleted file mode 100644 index a06a77d5..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_previewImage_checkbox_checked@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@2x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@2x.png deleted file mode 100644 index 23cfcb47..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@3x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@3x.png deleted file mode 100644 index c76ffdb1..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_done@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@2x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@2x.png deleted file mode 100644 index 65f5493e..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@3x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@3x.png deleted file mode 100644 index 85bd47ae..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_error@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@2x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@2x.png deleted file mode 100644 index 2c9171ba..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@3x.png b/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@3x.png deleted file mode 100644 index 14942bd3..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUIResources.bundle/Contents/Resources/QMUI_tips_info@3x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_0@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_0@2x.png deleted file mode 100755 index 9ca00213..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_0@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_100@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_100@2x.png deleted file mode 100755 index 5b38f93d..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_100@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_101@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_101@2x.png deleted file mode 100755 index 6060a6bf..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_101@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_102@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_102@2x.png deleted file mode 100755 index d3dc98dc..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_102@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_103@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_103@2x.png deleted file mode 100755 index 5043d02b..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_103@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_104@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_104@2x.png deleted file mode 100755 index dd98f4ec..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_104@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_105@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_105@2x.png deleted file mode 100755 index 50367591..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_105@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_106@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_106@2x.png deleted file mode 100755 index 164783c8..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_106@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_107@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_107@2x.png deleted file mode 100755 index d057bc3c..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_107@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_108@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_108@2x.png deleted file mode 100755 index 74df4f41..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_108@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_109@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_109@2x.png deleted file mode 100755 index 1e88d876..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_109@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_10@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_10@2x.png deleted file mode 100755 index 41050d8c..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_10@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_110@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_110@2x.png deleted file mode 100755 index 131edf1b..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_110@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_111@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_111@2x.png deleted file mode 100755 index 4a08a216..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_111@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_112@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_112@2x.png deleted file mode 100755 index 64f345ad..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_112@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_11@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_11@2x.png deleted file mode 100755 index b74b571d..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_11@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_12@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_12@2x.png deleted file mode 100755 index adbe5296..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_12@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_13@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_13@2x.png deleted file mode 100755 index e1c5f553..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_13@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_14@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_14@2x.png deleted file mode 100755 index 31f7a8b9..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_14@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_15@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_15@2x.png deleted file mode 100755 index 48f5ba18..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_15@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_16@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_16@2x.png deleted file mode 100755 index 4e65f9a4..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_16@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_17@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_17@2x.png deleted file mode 100755 index c71d3baf..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_17@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_18@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_18@2x.png deleted file mode 100755 index b11b30a9..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_18@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_19@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_19@2x.png deleted file mode 100755 index 484f5fb9..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_19@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_1@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_1@2x.png deleted file mode 100755 index 61b5dffc..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_1@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_20@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_20@2x.png deleted file mode 100755 index 4582d88a..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_20@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_21@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_21@2x.png deleted file mode 100755 index 3e864d09..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_21@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_22@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_22@2x.png deleted file mode 100755 index 7bd704ea..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_22@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_23@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_23@2x.png deleted file mode 100755 index 32761113..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_23@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_24@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_24@2x.png deleted file mode 100755 index 2e967d59..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_24@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_25@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_25@2x.png deleted file mode 100755 index 27a30f4b..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_25@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_26@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_26@2x.png deleted file mode 100755 index 3b6c44aa..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_26@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_27@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_27@2x.png deleted file mode 100755 index dc9caf8d..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_27@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_28@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_28@2x.png deleted file mode 100755 index 1a911fdb..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_28@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_29@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_29@2x.png deleted file mode 100755 index fa8bafca..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_29@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_2@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_2@2x.png deleted file mode 100755 index 123d2d97..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_2@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_30@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_30@2x.png deleted file mode 100755 index 19df5e2d..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_30@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_31@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_31@2x.png deleted file mode 100755 index 6ee15e16..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_31@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_32@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_32@2x.png deleted file mode 100755 index 27bfb656..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_32@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_33@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_33@2x.png deleted file mode 100755 index 02c1a006..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_33@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_34@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_34@2x.png deleted file mode 100755 index 24e62c16..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_34@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_35@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_35@2x.png deleted file mode 100755 index 97bb3a24..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_35@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_36@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_36@2x.png deleted file mode 100755 index e91c9b30..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_36@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_37@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_37@2x.png deleted file mode 100755 index b7957714..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_37@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_38@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_38@2x.png deleted file mode 100755 index 69f14b1e..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_38@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_39@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_39@2x.png deleted file mode 100755 index 95454ef9..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_39@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_3@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_3@2x.png deleted file mode 100755 index 02de382e..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_3@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_40@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_40@2x.png deleted file mode 100755 index 9e45809a..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_40@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_41@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_41@2x.png deleted file mode 100755 index 58d2d45a..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_41@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_42@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_42@2x.png deleted file mode 100755 index 02a9c3de..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_42@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_43@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_43@2x.png deleted file mode 100755 index 9dce687e..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_43@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_44@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_44@2x.png deleted file mode 100755 index dcec1332..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_44@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_45@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_45@2x.png deleted file mode 100755 index 9aa41d60..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_45@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_46@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_46@2x.png deleted file mode 100755 index 90435b72..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_46@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_47@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_47@2x.png deleted file mode 100755 index d45d0531..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_47@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_48@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_48@2x.png deleted file mode 100755 index 9befadbf..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_48@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_49@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_49@2x.png deleted file mode 100755 index 42b98fdf..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_49@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_4@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_4@2x.png deleted file mode 100755 index 248f58c9..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_4@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_50@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_50@2x.png deleted file mode 100755 index 0d17001e..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_50@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_51@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_51@2x.png deleted file mode 100755 index aadd6b79..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_51@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_52@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_52@2x.png deleted file mode 100755 index 59b833a3..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_52@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_53@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_53@2x.png deleted file mode 100755 index 26fe1208..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_53@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_54@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_54@2x.png deleted file mode 100755 index 8806eb18..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_54@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_55@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_55@2x.png deleted file mode 100755 index 345205c0..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_55@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_56@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_56@2x.png deleted file mode 100755 index 89dea16a..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_56@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_57@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_57@2x.png deleted file mode 100755 index 03d5769c..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_57@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_58@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_58@2x.png deleted file mode 100755 index 5025f781..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_58@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_59@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_59@2x.png deleted file mode 100755 index f48364b1..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_59@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_5@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_5@2x.png deleted file mode 100755 index 86bd58f0..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_5@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_60@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_60@2x.png deleted file mode 100755 index f13533b0..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_60@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_61@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_61@2x.png deleted file mode 100755 index 01b8bd4e..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_61@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_62@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_62@2x.png deleted file mode 100755 index 7d845d02..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_62@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_63@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_63@2x.png deleted file mode 100755 index 0a451dff..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_63@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_64@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_64@2x.png deleted file mode 100755 index 23cb090c..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_64@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_65@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_65@2x.png deleted file mode 100755 index 7241cf33..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_65@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_66@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_66@2x.png deleted file mode 100755 index 72ba5daa..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_66@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_67@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_67@2x.png deleted file mode 100755 index 77decd68..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_67@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_68@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_68@2x.png deleted file mode 100755 index 249c8071..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_68@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_69@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_69@2x.png deleted file mode 100755 index 3329c4e6..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_69@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_6@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_6@2x.png deleted file mode 100755 index 27123024..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_6@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_70@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_70@2x.png deleted file mode 100755 index 91047e51..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_70@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_71@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_71@2x.png deleted file mode 100755 index bdd0aaf3..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_71@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_72@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_72@2x.png deleted file mode 100755 index 44dcabbc..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_72@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_73@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_73@2x.png deleted file mode 100755 index bd70cc5d..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_73@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_74@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_74@2x.png deleted file mode 100755 index 4cc4436f..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_74@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_75@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_75@2x.png deleted file mode 100755 index 5a32ffda..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_75@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_76@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_76@2x.png deleted file mode 100755 index 37f1a522..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_76@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_77@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_77@2x.png deleted file mode 100755 index 2e92d448..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_77@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_78@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_78@2x.png deleted file mode 100755 index 06d89e9d..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_78@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_79@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_79@2x.png deleted file mode 100755 index 83a623d6..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_79@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_7@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_7@2x.png deleted file mode 100755 index a271087f..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_7@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_80@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_80@2x.png deleted file mode 100755 index 309080fc..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_80@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_81@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_81@2x.png deleted file mode 100755 index 8efc1790..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_81@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_82@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_82@2x.png deleted file mode 100755 index eb4dd077..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_82@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_83@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_83@2x.png deleted file mode 100755 index ea063288..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_83@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_84@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_84@2x.png deleted file mode 100755 index 1c3feb69..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_84@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_85@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_85@2x.png deleted file mode 100755 index 60fc7925..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_85@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_86@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_86@2x.png deleted file mode 100755 index a394e624..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_86@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_87@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_87@2x.png deleted file mode 100755 index 4514672c..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_87@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_88@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_88@2x.png deleted file mode 100755 index 5f613479..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_88@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_89@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_89@2x.png deleted file mode 100755 index 08e36b85..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_89@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_8@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_8@2x.png deleted file mode 100755 index 378b4dd2..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_8@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_90@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_90@2x.png deleted file mode 100755 index 629e515c..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_90@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_91@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_91@2x.png deleted file mode 100755 index 6407e6fd..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_91@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_92@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_92@2x.png deleted file mode 100755 index 83c94987..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_92@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_93@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_93@2x.png deleted file mode 100755 index 1cf4760e..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_93@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_94@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_94@2x.png deleted file mode 100755 index adc05c16..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_94@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_95@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_95@2x.png deleted file mode 100755 index 28464117..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_95@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_96@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_96@2x.png deleted file mode 100755 index 954c4b72..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_96@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_97@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_97@2x.png deleted file mode 100755 index 2e9729c8..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_97@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_98@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_98@2x.png deleted file mode 100755 index 649f732a..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_98@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_99@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_99@2x.png deleted file mode 100755 index 3aa35cdd..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_99@2x.png and /dev/null differ diff --git a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_9@2x.png b/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_9@2x.png deleted file mode 100755 index d399252e..00000000 Binary files a/QMUI/QMUIKit/UIResources/QMUI_QQEmotion.bundle/Contents/Resources/smiley_9@2x.png and /dev/null differ 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 91d689c7..fb1a1ff5 100644 --- a/QMUI/README.md +++ b/QMUI/README.md @@ -1,16 +1,15 @@ - -

- Banner -

- # QMUI iOS -QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设计目的是用于辅助快速搭建一个具备基本设计还原效果的 iOS 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。 - -官网:[http://qmuiteam.com/ios](http://qmuiteam.com/ios) +

+ Banner +

+QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设计目的是用于辅助快速搭建一个具备基本设计还原效果的 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 配置 @@ -28,22 +27,48 @@ QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设 提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。 -## 支持 iOS 版本 -QMUI iOS 支持 iOS 7+。 +## 支持iOS版本 + +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) +![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) + ## 注意事项 -- 关于版本更新:QMUI iOS 更新较为频繁(通常半个月到一个月会发一次 Release),每次更新都会有一些新旧版本兼容的工作要做,请仔细阅读[版本更新说明](https://github.com/QMUI/QMUI_iOS/releases)。 -- 关于 AutoLayout:目前暂未支持,考虑到 AutoLayout 的普及性,我们将会尽快支持。 + +- 关于 AutoLayout:通常可以配合 Masonry 等常见的 AutoLayout 框架使用,若遇到不兼容的个案请提 issue。 - 关于 xib / storyboard:现已全面支持。 -- 关于 Swift:暂未检查过在 Swift 下使用 QMUI 的问题,如遇到问题可以反馈给我们,我们会尽快兼容。 +- 关于 Swift:可以正常使用,如遇到问题请提 issue。 +- 关于 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)。 ## 其他 + 建议搭配 QMUI 专用的 Code Snippets 及文件模板使用: 1. [QMUI_iOS_CodeSnippets](https://github.com/QMUI/QMUI_iOS_CodeSnippets) 2. [QMUI_iOS_Templates](https://github.com/QMUI/QMUI_iOS_Templates) diff --git a/QMUI/add_license.py b/QMUI/add_license.py new file mode 100644 index 00000000..793a3e99 --- /dev/null +++ b/QMUI/add_license.py @@ -0,0 +1,164 @@ +# -*- encoding:utf-8 -*- + +import os +import re + +# 配置参数 + +root_src_dir = '.' # 代码目录名 +ignore_dirs = ['test'] # 要忽略的目录(目录名完全匹配) +rules = [ + # Android + # { + # 'suffix': '.java', + # 'new_comment': 'comment_for_java.txt', + # 'old_comment': 'comment_for_java.txt', + # 'ignore_files': [] # 要忽略的文件名(文件名完整匹配) + # }, + # { + # 'suffix': '.xml', + # 'new_comment': 'comment_for_xml.txt', + # 'old_comment': 'comment_for_xml.txt', + # 'keep_on_top_lines': [re.compile(r'.*<\?xml version="1\.0".*')] # 要保证在文件前面的行(注释将加在这些行之后)(正则) + # }, + # iOS + { + 'suffix': '.h', + 'new_comment': 'new_license_content.txt', # 要更新的 license 文件 + 'old_comment': 'old_license_content.txt', # 老的的 license 文件,如果文件没有更新,那么内容要保持跟新文件一样 + 'delete_lines': [re.compile(r'.*//.*Copyright.*All rights reserved.*')] # 要从源文件中删除的行(正则) + # 'ignore_files': [] # 要忽略的文件名(文件名完整匹配) + }, + { + 'suffix': '.m', + 'new_comment': 'new_license_content.txt', + 'old_comment': 'old_license_content.txt', + 'delete_lines': [re.compile(r'.*//.*Copyright.*All rights reserved.*')] # 要从源文件中删除的行(正则) + # 'ignore_files': [] # 要忽略的文件名(文件名完整匹配) + }, +] + + +# 全局变量 + +delete_files = [] + + +def is_match_anyone_dir(path, dir_list): + for d in dir_list: + if "/{dir}/".format(dir=d) in path: + return True + return False + + +def is_match_anyone_str(filename, str_list): + for s in str_list: + if filename == s: + return True + return False + + +def is_match_anyone_regex(line, regex_list): + for regex in regex_list: + if regex.match(line): + return True + return False + + +def find_file(start, suffix, ignore_dirs, ignore_files): + list = [] + for relpath, dirs, files in os.walk(start): + for filename in files: + if filename.endswith(suffix): + full_file_name = os.path.join(relpath, filename) + if not is_match_anyone_dir(full_file_name, ignore_dirs) and not is_match_anyone_str(filename, ignore_files): + list.append(full_file_name) + return list + + +def add_comment(rule): + print('processing with {rule}'.format(rule=rule)) + + new_comment_filename = rule['new_comment'] + old_comment_filename = rule['old_comment'] + file_suffix = rule['suffix'] + ignore_files = rule.get('ignore_files', []) + keep_on_top_lines = rule.get('keep_on_top_lines', []) + delete_lines = rule.get('delete_lines', []) + + delete_count = 0 + + with open(new_comment_filename, 'r', encoding = "utf-8") as f: + new_comment_content = f.read() + + with open(old_comment_filename, 'r', encoding = "utf-8") as f: + old_comment_lines = f.readlines() + + old_comment_lines_count = len(old_comment_lines) + + files = find_file(root_src_dir, file_suffix, ignore_dirs, ignore_files) + + for file in files: + + with open(file, 'r', encoding = "utf-8") as f: + src_lines = f.readlines() + + with open(file, 'w', encoding = "utf-8") as f: + has_written_comments = False + is_update_license = False + line_index = 0 + + for line in src_lines: + + is_line_exist = False + + # 这一行是否存在久的文件中 + for old_comment_line in old_comment_lines: + if line == old_comment_line: + is_line_exist = True + break + + # 如果存在则不写进去 + if is_line_exist and len(line.strip()) > 0: + line_index += 1 + delete_count += 1 + if line_index <= old_comment_lines_count: + is_update_license = True + continue + + # 是否正则删除 + if is_match_anyone_regex(line, delete_lines): + print('ignore line: {line}'.format(line=line)) + continue + + if not has_written_comments: + if is_match_anyone_regex(line, keep_on_top_lines): + f.write(line) + else: + f.writelines(new_comment_content) + has_written_comments = True + f.write(line) + + else: + f.write(line) + + if delete_count != 0 and delete_count != old_comment_lines_count: + delete_files.append(file) + + delete_count = 0 + + if is_update_license: + print('processing with {file} ({flag})'.format(file=file, flag='update license')) + else: + print('processing with {file} ({flag})'.format(file=file, flag='add license')) + + +if __name__ == '__main__': + for rule in rules: + add_comment(rule) + if len(delete_files) > 0: + print('==================== 以下文件可能更新遇到问题,建议检查 ====================') + for delete_file in delete_files: + print(delete_file) + print('==================== 以上文件可能更新遇到问题,建议检查 ====================') + delete_files = [] diff --git a/QMUI/new_license_content.txt b/QMUI/new_license_content.txt new file mode 100644 index 00000000..be2085b1 --- /dev/null +++ b/QMUI/new_license_content.txt @@ -0,0 +1,7 @@ +/** + * 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. + */ diff --git a/QMUI/old_license_content.txt b/QMUI/old_license_content.txt new file mode 100644 index 00000000..be2085b1 --- /dev/null +++ b/QMUI/old_license_content.txt @@ -0,0 +1,7 @@ +/** + * 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. + */ diff --git a/QMUI/qmui.xcodeproj/project.pbxproj b/QMUI/qmui.xcodeproj/project.pbxproj index 239dbcf6..9381ac23 100644 --- a/QMUI/qmui.xcodeproj/project.pbxproj +++ b/QMUI/qmui.xcodeproj/project.pbxproj @@ -3,382 +3,502 @@ archiveVersion = 1; classes = { }; - objectVersion = 48; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ - 16F6B63A1DFD3AF500E58171 /* QMUIToastView.h in Headers */ = {isa = PBXBuildFile; fileRef = 16F6B6381DFD3AF500E58171 /* QMUIToastView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 16F6B63B1DFD3AF500E58171 /* QMUIToastView.m in Sources */ = {isa = PBXBuildFile; fileRef = 16F6B6391DFD3AF500E58171 /* QMUIToastView.m */; }; - 16F6B63C1DFD3AF500E58171 /* QMUIToastView.m in Sources */ = {isa = PBXBuildFile; fileRef = 16F6B6391DFD3AF500E58171 /* QMUIToastView.m */; }; - 16F6B6441DFD571200E58171 /* QMUIToastBackgroundView.h in Headers */ = {isa = PBXBuildFile; fileRef = 16F6B6421DFD571200E58171 /* QMUIToastBackgroundView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 16F6B6451DFD571200E58171 /* QMUIToastBackgroundView.m in Sources */ = {isa = PBXBuildFile; fileRef = 16F6B6431DFD571200E58171 /* QMUIToastBackgroundView.m */; }; - 16F6B6461DFD571300E58171 /* QMUIToastBackgroundView.m in Sources */ = {isa = PBXBuildFile; fileRef = 16F6B6431DFD571200E58171 /* QMUIToastBackgroundView.m */; }; - 16F6B64E1DFD9ADD00E58171 /* QMUIToastContentView.h in Headers */ = {isa = PBXBuildFile; fileRef = 16F6B64C1DFD9ADD00E58171 /* QMUIToastContentView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 16F6B64F1DFD9ADD00E58171 /* QMUIToastContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 16F6B64D1DFD9ADD00E58171 /* QMUIToastContentView.m */; }; - 16F6B6501DFD9ADD00E58171 /* QMUIToastContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 16F6B64D1DFD9ADD00E58171 /* QMUIToastContentView.m */; }; - 16F6B6531DFEC39F00E58171 /* QMUIToastAnimator.h in Headers */ = {isa = PBXBuildFile; fileRef = 16F6B6511DFEC39F00E58171 /* QMUIToastAnimator.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 16F6B6541DFEC39F00E58171 /* QMUIToastAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = 16F6B6521DFEC39F00E58171 /* QMUIToastAnimator.m */; }; - 16F6B6551DFEC39F00E58171 /* QMUIToastAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = 16F6B6521DFEC39F00E58171 /* QMUIToastAnimator.m */; }; - 36C72E101DE826B800F5F116 /* UINavigationBar+Transition.h in Headers */ = {isa = PBXBuildFile; fileRef = 36C72E0E1DE826B800F5F116 /* UINavigationBar+Transition.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 36C72E111DE826B800F5F116 /* UINavigationBar+Transition.m in Sources */ = {isa = PBXBuildFile; fileRef = 36C72E0F1DE826B800F5F116 /* UINavigationBar+Transition.m */; }; - 36C72E121DE826B800F5F116 /* UINavigationBar+Transition.m in Sources */ = {isa = PBXBuildFile; fileRef = 36C72E0F1DE826B800F5F116 /* UINavigationBar+Transition.m */; }; - CD27AB561ECC48C90000B4D0 /* QMUICommonDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = CD27AB501ECC48C90000B4D0 /* QMUICommonDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD27AB571ECC48C90000B4D0 /* QMUIConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = CD27AB511ECC48C90000B4D0 /* QMUIConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD27AB581ECC48C90000B4D0 /* QMUIConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = CD27AB521ECC48C90000B4D0 /* QMUIConfiguration.m */; }; - CD27AB591ECC48C90000B4D0 /* QMUIConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = CD27AB521ECC48C90000B4D0 /* QMUIConfiguration.m */; }; - CD27AB5A1ECC48C90000B4D0 /* QMUIConfigurationMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = CD27AB531ECC48C90000B4D0 /* QMUIConfigurationMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD27AB5B1ECC48C90000B4D0 /* QMUIHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = CD27AB541ECC48C90000B4D0 /* QMUIHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD27AB5C1ECC48C90000B4D0 /* QMUIHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CD27AB551ECC48C90000B4D0 /* QMUIHelper.m */; }; - CD27AB5D1ECC48C90000B4D0 /* QMUIHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CD27AB551ECC48C90000B4D0 /* QMUIHelper.m */; }; - CD27AB601ECC48D70000B4D0 /* QMUICore.h in Headers */ = {isa = PBXBuildFile; fileRef = CD27AB5E1ECC48D70000B4D0 /* QMUICore.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD2B01F31EDEB42D00183450 /* QMUIMarqueeLabel.h in Headers */ = {isa = PBXBuildFile; fileRef = CD2B01F11EDEB42D00183450 /* QMUIMarqueeLabel.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD2B01F41EDEB42D00183450 /* QMUIMarqueeLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2B01F21EDEB42D00183450 /* QMUIMarqueeLabel.m */; }; - CD2B01F51EDEB42D00183450 /* QMUIMarqueeLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2B01F21EDEB42D00183450 /* QMUIMarqueeLabel.m */; }; - CD5387711EE004A300654A73 /* QMUISlider.h in Headers */ = {isa = PBXBuildFile; fileRef = CD53876F1EE004A300654A73 /* QMUISlider.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD5387721EE004A300654A73 /* QMUISlider.m in Sources */ = {isa = PBXBuildFile; fileRef = CD5387701EE004A300654A73 /* QMUISlider.m */; }; - CD5387731EE004A300654A73 /* QMUISlider.m in Sources */ = {isa = PBXBuildFile; fileRef = CD5387701EE004A300654A73 /* QMUISlider.m */; }; - CD6CC63B1EF911E000602EDD /* QMUIStaticTableViewCellData.h in Headers */ = {isa = PBXBuildFile; fileRef = CD6CC6371EF911DF00602EDD /* QMUIStaticTableViewCellData.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD6CC63C1EF911E000602EDD /* QMUIStaticTableViewCellData.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6CC6381EF911DF00602EDD /* QMUIStaticTableViewCellData.m */; }; - CD6CC63D1EF911E000602EDD /* QMUIStaticTableViewCellData.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6CC6381EF911DF00602EDD /* QMUIStaticTableViewCellData.m */; }; - CD6CC63E1EF911E000602EDD /* QMUIStaticTableViewCellDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = CD6CC6391EF911DF00602EDD /* QMUIStaticTableViewCellDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD6CC63F1EF911E000602EDD /* QMUIStaticTableViewCellDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6CC63A1EF911DF00602EDD /* QMUIStaticTableViewCellDataSource.m */; }; - CD6CC6401EF911E000602EDD /* QMUIStaticTableViewCellDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6CC63A1EF911DF00602EDD /* QMUIStaticTableViewCellDataSource.m */; }; - CD6CC6431EF9123000602EDD /* UITableView+QMUIStaticCell.h in Headers */ = {isa = PBXBuildFile; fileRef = CD6CC6411EF9123000602EDD /* UITableView+QMUIStaticCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD6CC6441EF9123000602EDD /* UITableView+QMUIStaticCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6CC6421EF9123000602EDD /* UITableView+QMUIStaticCell.m */; }; - CD6CC6451EF9123000602EDD /* UITableView+QMUIStaticCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CD6CC6421EF9123000602EDD /* UITableView+QMUIStaticCell.m */; }; - CD78CBCA1DEE9D6300910DCE /* QMUIImagePreviewViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CD78CBC81DEE9D6300910DCE /* QMUIImagePreviewViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD78CBCB1DEE9D6300910DCE /* QMUIImagePreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD78CBC91DEE9D6300910DCE /* QMUIImagePreviewViewController.m */; }; - CD78CBCC1DEE9D6300910DCE /* QMUIImagePreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD78CBC91DEE9D6300910DCE /* QMUIImagePreviewViewController.m */; }; - CD78CBCF1DEE9E3500910DCE /* QMUIImagePreviewView.h in Headers */ = {isa = PBXBuildFile; fileRef = CD78CBCD1DEE9E3500910DCE /* QMUIImagePreviewView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD78CBD01DEE9E3500910DCE /* QMUIImagePreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = CD78CBCE1DEE9E3500910DCE /* QMUIImagePreviewView.m */; }; - CD78CBD11DEE9E3500910DCE /* QMUIImagePreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = CD78CBCE1DEE9E3500910DCE /* QMUIImagePreviewView.m */; }; + 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, ); }; }; + CD046C422018668900092035 /* QMUILogItem.m in Sources */ = {isa = PBXBuildFile; fileRef = CD046C402018668900092035 /* QMUILogItem.m */; }; + CD046C452018670900092035 /* QMUILogNameManager.h in Headers */ = {isa = PBXBuildFile; fileRef = CD046C432018670900092035 /* QMUILogNameManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD046C462018670900092035 /* QMUILogNameManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CD046C442018670900092035 /* QMUILogNameManager.m */; }; + 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, ); }; }; + CD18BC7421760D5900E2A4E6 /* QMUINavigationBarScrollingAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = CD18BC7221760D5900E2A4E6 /* QMUINavigationBarScrollingAnimator.m */; }; + 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 */; }; + 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, ); }; }; + CD745E2F21CA5B8F006EC132 /* QMUIImagePreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = CD745E2B21CA5B8E006EC132 /* QMUIImagePreviewView.m */; }; + CD745E3221CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.h in Headers */ = {isa = PBXBuildFile; fileRef = CD745E3021CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 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, ); }; }; + CD82C0B4206A82520046EED2 /* NSObject+QMUIMultipleDelegates.m in Sources */ = {isa = PBXBuildFile; fileRef = CD82C0B2206A82520046EED2 /* NSObject+QMUIMultipleDelegates.m */; }; CD84F31D1E52DBEA00546111 /* UITabBar+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD84F31B1E52DBEA00546111 /* UITabBar+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD84F31E1E52DBEA00546111 /* UITabBar+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD84F31C1E52DBEA00546111 /* UITabBar+QMUI.m */; }; CD84F31F1E52DBEA00546111 /* UITabBar+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD84F31C1E52DBEA00546111 /* UITabBar+QMUI.m */; }; - CD9D18FC1DD462200020F268 /* QMUIFloatLayoutView.h in Headers */ = {isa = PBXBuildFile; fileRef = CD9D18FA1DD462200020F268 /* QMUIFloatLayoutView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CD9D18FD1DD462200020F268 /* QMUIFloatLayoutView.m in Sources */ = {isa = PBXBuildFile; fileRef = CD9D18FB1DD462200020F268 /* QMUIFloatLayoutView.m */; }; - CD9D18FE1DD462200020F268 /* QMUIFloatLayoutView.m in Sources */ = {isa = PBXBuildFile; fileRef = CD9D18FB1DD462200020F268 /* QMUIFloatLayoutView.m */; }; + CD8AA7AB21E8B9D600BA7369 /* QMUIConsole.h in Headers */ = {isa = PBXBuildFile; fileRef = CD8AA7A921E8B9D600BA7369 /* QMUIConsole.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD8AA7AC21E8B9D600BA7369 /* QMUIConsole.m in Sources */ = {isa = PBXBuildFile; fileRef = CD8AA7AA21E8B9D600BA7369 /* QMUIConsole.m */; }; + CD8AA7AF21E8BF0B00BA7369 /* QMUIConsoleToolbar.h in Headers */ = {isa = PBXBuildFile; fileRef = CD8AA7AD21E8BF0B00BA7369 /* QMUIConsoleToolbar.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD8AA7B021E8BF0B00BA7369 /* QMUIConsoleToolbar.m in Sources */ = {isa = PBXBuildFile; fileRef = CD8AA7AE21E8BF0B00BA7369 /* QMUIConsoleToolbar.m */; }; + CD8AA7B321E8C0F300BA7369 /* QMUIConsoleViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CD8AA7B121E8C0F300BA7369 /* QMUIConsoleViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 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 */; }; + 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, ); }; }; - CDB8CAE51DCC870700769DF0 /* QMUIAsset.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA3E1DCC870700769DF0 /* QMUIAsset.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CAE71DCC870700769DF0 /* QMUIAsset.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA3F1DCC870700769DF0 /* QMUIAsset.m */; }; - CDB8CAE81DCC870700769DF0 /* QMUIAsset.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA3F1DCC870700769DF0 /* QMUIAsset.m */; }; - CDB8CAE91DCC870700769DF0 /* QMUIAssetsGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA401DCC870700769DF0 /* QMUIAssetsGroup.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CAEB1DCC870700769DF0 /* QMUIAssetsGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA411DCC870700769DF0 /* QMUIAssetsGroup.m */; }; - CDB8CAEC1DCC870700769DF0 /* QMUIAssetsGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA411DCC870700769DF0 /* QMUIAssetsGroup.m */; }; - CDB8CAED1DCC870700769DF0 /* QMUIAssetsManager.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA421DCC870700769DF0 /* QMUIAssetsManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CAEF1DCC870700769DF0 /* QMUIAssetsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA431DCC870700769DF0 /* QMUIAssetsManager.m */; }; - CDB8CAF01DCC870700769DF0 /* QMUIAssetsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA431DCC870700769DF0 /* QMUIAssetsManager.m */; }; - CDB8CAF11DCC870700769DF0 /* QMUIAlbumViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA451DCC870700769DF0 /* QMUIAlbumViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CAF31DCC870700769DF0 /* QMUIAlbumViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA461DCC870700769DF0 /* QMUIAlbumViewController.m */; }; - CDB8CAF41DCC870700769DF0 /* QMUIAlbumViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA461DCC870700769DF0 /* QMUIAlbumViewController.m */; }; - CDB8CAF51DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA471DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CAF71DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA481DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.m */; }; - CDB8CAF81DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA481DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.m */; }; - CDB8CAF91DCC870700769DF0 /* QMUIImagePickerHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA491DCC870700769DF0 /* QMUIImagePickerHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CAFB1DCC870700769DF0 /* QMUIImagePickerHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA4A1DCC870700769DF0 /* QMUIImagePickerHelper.m */; }; - CDB8CAFC1DCC870700769DF0 /* QMUIImagePickerHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA4A1DCC870700769DF0 /* QMUIImagePickerHelper.m */; }; - CDB8CAFD1DCC870700769DF0 /* QMUIImagePickerPreviewViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA4B1DCC870700769DF0 /* QMUIImagePickerPreviewViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CAFF1DCC870700769DF0 /* QMUIImagePickerPreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA4C1DCC870700769DF0 /* QMUIImagePickerPreviewViewController.m */; }; - CDB8CB001DCC870700769DF0 /* QMUIImagePickerPreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA4C1DCC870700769DF0 /* QMUIImagePickerPreviewViewController.m */; }; - CDB8CB011DCC870700769DF0 /* QMUIImagePickerViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA4D1DCC870700769DF0 /* QMUIImagePickerViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB031DCC870700769DF0 /* QMUIImagePickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA4E1DCC870700769DF0 /* QMUIImagePickerViewController.m */; }; - CDB8CB041DCC870700769DF0 /* QMUIImagePickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA4E1DCC870700769DF0 /* QMUIImagePickerViewController.m */; }; - CDB8CB051DCC870700769DF0 /* QMUIDialogViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA4F1DCC870700769DF0 /* QMUIDialogViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB071DCC870700769DF0 /* QMUIDialogViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA501DCC870700769DF0 /* QMUIDialogViewController.m */; }; - CDB8CB081DCC870700769DF0 /* QMUIDialogViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA501DCC870700769DF0 /* QMUIDialogViewController.m */; }; - CDB8CB091DCC870700769DF0 /* QMUIEmotionView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA511DCC870700769DF0 /* QMUIEmotionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB0B1DCC870700769DF0 /* QMUIEmotionView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA521DCC870700769DF0 /* QMUIEmotionView.m */; }; - CDB8CB0C1DCC870700769DF0 /* QMUIEmotionView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA521DCC870700769DF0 /* QMUIEmotionView.m */; }; - CDB8CB0D1DCC870700769DF0 /* QMUIEmptyView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA531DCC870700769DF0 /* QMUIEmptyView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB0F1DCC870700769DF0 /* QMUIEmptyView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA541DCC870700769DF0 /* QMUIEmptyView.m */; }; - CDB8CB101DCC870700769DF0 /* QMUIEmptyView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA541DCC870700769DF0 /* QMUIEmptyView.m */; }; - CDB8CB111DCC870700769DF0 /* QMUIGridView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA551DCC870700769DF0 /* QMUIGridView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB131DCC870700769DF0 /* QMUIGridView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA561DCC870700769DF0 /* QMUIGridView.m */; }; - CDB8CB141DCC870700769DF0 /* QMUIGridView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA561DCC870700769DF0 /* QMUIGridView.m */; }; - CDB8CB151DCC870700769DF0 /* QMUIModalPresentationViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA571DCC870700769DF0 /* QMUIModalPresentationViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB171DCC870700769DF0 /* QMUIModalPresentationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA581DCC870700769DF0 /* QMUIModalPresentationViewController.m */; }; - CDB8CB181DCC870700769DF0 /* QMUIModalPresentationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA581DCC870700769DF0 /* QMUIModalPresentationViewController.m */; }; - CDB8CB191DCC870700769DF0 /* QMUIMoreOperationController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA591DCC870700769DF0 /* QMUIMoreOperationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB1B1DCC870700769DF0 /* QMUIMoreOperationController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA5A1DCC870700769DF0 /* QMUIMoreOperationController.m */; }; - CDB8CB1C1DCC870700769DF0 /* QMUIMoreOperationController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA5A1DCC870700769DF0 /* QMUIMoreOperationController.m */; }; - CDB8CB1D1DCC870700769DF0 /* QMUINavigationTitleView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA5B1DCC870700769DF0 /* QMUINavigationTitleView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB1F1DCC870700769DF0 /* QMUINavigationTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA5C1DCC870700769DF0 /* QMUINavigationTitleView.m */; }; - CDB8CB201DCC870700769DF0 /* QMUINavigationTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA5C1DCC870700769DF0 /* QMUINavigationTitleView.m */; }; - CDB8CB211DCC870700769DF0 /* QMUIOrderedDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA5D1DCC870700769DF0 /* QMUIOrderedDictionary.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB231DCC870700769DF0 /* QMUIOrderedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA5E1DCC870700769DF0 /* QMUIOrderedDictionary.m */; }; - CDB8CB241DCC870700769DF0 /* QMUIOrderedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA5E1DCC870700769DF0 /* QMUIOrderedDictionary.m */; }; - CDB8CB251DCC870700769DF0 /* QMUIPieProgressView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA5F1DCC870700769DF0 /* QMUIPieProgressView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB271DCC870700769DF0 /* QMUIPieProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA601DCC870700769DF0 /* QMUIPieProgressView.m */; }; - CDB8CB281DCC870700769DF0 /* QMUIPieProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA601DCC870700769DF0 /* QMUIPieProgressView.m */; }; - CDB8CB291DCC870700769DF0 /* QMUIPopupContainerView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA611DCC870700769DF0 /* QMUIPopupContainerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB2B1DCC870700769DF0 /* QMUIPopupContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA621DCC870700769DF0 /* QMUIPopupContainerView.m */; }; - CDB8CB2C1DCC870700769DF0 /* QMUIPopupContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA621DCC870700769DF0 /* QMUIPopupContainerView.m */; }; - CDB8CB3D1DCC870700769DF0 /* QMUIQQEmotionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA6B1DCC870700769DF0 /* QMUIQQEmotionManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB3F1DCC870700769DF0 /* QMUIQQEmotionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA6C1DCC870700769DF0 /* QMUIQQEmotionManager.m */; }; - CDB8CB401DCC870700769DF0 /* QMUIQQEmotionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA6C1DCC870700769DF0 /* QMUIQQEmotionManager.m */; }; - CDB8CB411DCC870700769DF0 /* QMUITestView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA6D1DCC870700769DF0 /* QMUITestView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB431DCC870700769DF0 /* QMUITestView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA6E1DCC870700769DF0 /* QMUITestView.m */; }; - CDB8CB441DCC870700769DF0 /* QMUITestView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA6E1DCC870700769DF0 /* QMUITestView.m */; }; - CDB8CB451DCC870700769DF0 /* QMUITips.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA6F1DCC870700769DF0 /* QMUITips.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB471DCC870700769DF0 /* QMUITips.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA701DCC870700769DF0 /* QMUITips.m */; }; - CDB8CB481DCC870700769DF0 /* QMUITips.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA701DCC870700769DF0 /* QMUITips.m */; }; - CDB8CB491DCC870700769DF0 /* QMUIVisualEffectView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA711DCC870700769DF0 /* QMUIVisualEffectView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB4B1DCC870700769DF0 /* QMUIVisualEffectView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA721DCC870700769DF0 /* QMUIVisualEffectView.m */; }; - CDB8CB4C1DCC870700769DF0 /* QMUIVisualEffectView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA721DCC870700769DF0 /* QMUIVisualEffectView.m */; }; - CDB8CB4D1DCC870700769DF0 /* QMUIZoomImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA731DCC870700769DF0 /* QMUIZoomImageView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB4F1DCC870700769DF0 /* QMUIZoomImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA741DCC870700769DF0 /* QMUIZoomImageView.m */; }; - CDB8CB501DCC870700769DF0 /* QMUIZoomImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA741DCC870700769DF0 /* QMUIZoomImageView.m */; }; CDB8CB511DCC870700769DF0 /* CALayer+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA761DCC870700769DF0 /* CALayer+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB531DCC870700769DF0 /* CALayer+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA771DCC870700769DF0 /* CALayer+QMUI.m */; }; CDB8CB541DCC870700769DF0 /* CALayer+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA771DCC870700769DF0 /* CALayer+QMUI.m */; }; CDB8CB551DCC870700769DF0 /* NSAttributedString+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA781DCC870700769DF0 /* NSAttributedString+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB571DCC870700769DF0 /* NSAttributedString+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA791DCC870700769DF0 /* NSAttributedString+QMUI.m */; }; CDB8CB581DCC870700769DF0 /* NSAttributedString+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA791DCC870700769DF0 /* NSAttributedString+QMUI.m */; }; CDB8CB591DCC870700769DF0 /* NSObject+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA7A1DCC870700769DF0 /* NSObject+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB5B1DCC870700769DF0 /* NSObject+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA7B1DCC870700769DF0 /* NSObject+QMUI.m */; }; CDB8CB5C1DCC870700769DF0 /* NSObject+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA7B1DCC870700769DF0 /* NSObject+QMUI.m */; }; CDB8CB5D1DCC870700769DF0 /* NSParagraphStyle+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA7C1DCC870700769DF0 /* NSParagraphStyle+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB5F1DCC870700769DF0 /* NSParagraphStyle+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA7D1DCC870700769DF0 /* NSParagraphStyle+QMUI.m */; }; CDB8CB601DCC870700769DF0 /* NSParagraphStyle+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA7D1DCC870700769DF0 /* NSParagraphStyle+QMUI.m */; }; CDB8CB611DCC870700769DF0 /* NSString+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA7E1DCC870700769DF0 /* NSString+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB631DCC870700769DF0 /* NSString+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA7F1DCC870700769DF0 /* NSString+QMUI.m */; }; CDB8CB641DCC870700769DF0 /* NSString+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA7F1DCC870700769DF0 /* NSString+QMUI.m */; }; - CDB8CB651DCC870700769DF0 /* QMUIAlertController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA801DCC870700769DF0 /* QMUIAlertController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB671DCC870700769DF0 /* QMUIAlertController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA811DCC870700769DF0 /* QMUIAlertController.m */; }; - CDB8CB681DCC870700769DF0 /* QMUIAlertController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA811DCC870700769DF0 /* QMUIAlertController.m */; }; - CDB8CB691DCC870700769DF0 /* QMUIButton.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA821DCC870700769DF0 /* QMUIButton.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB6B1DCC870700769DF0 /* QMUIButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA831DCC870700769DF0 /* QMUIButton.m */; }; - CDB8CB6C1DCC870700769DF0 /* QMUIButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA831DCC870700769DF0 /* QMUIButton.m */; }; - CDB8CB6D1DCC870700769DF0 /* QMUICellHeightCache.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA841DCC870700769DF0 /* QMUICellHeightCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB6F1DCC870700769DF0 /* QMUICellHeightCache.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA851DCC870700769DF0 /* QMUICellHeightCache.m */; }; - CDB8CB701DCC870700769DF0 /* QMUICellHeightCache.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA851DCC870700769DF0 /* QMUICellHeightCache.m */; }; - CDB8CB711DCC870700769DF0 /* QMUICollectionViewPagingLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA861DCC870700769DF0 /* QMUICollectionViewPagingLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB731DCC870700769DF0 /* QMUICollectionViewPagingLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA871DCC870700769DF0 /* QMUICollectionViewPagingLayout.m */; }; - CDB8CB741DCC870700769DF0 /* QMUICollectionViewPagingLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA871DCC870700769DF0 /* QMUICollectionViewPagingLayout.m */; }; - CDB8CB751DCC870700769DF0 /* QMUILabel.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA881DCC870700769DF0 /* QMUILabel.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB771DCC870700769DF0 /* QMUILabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA891DCC870700769DF0 /* QMUILabel.m */; }; - CDB8CB781DCC870700769DF0 /* QMUILabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA891DCC870700769DF0 /* QMUILabel.m */; }; - CDB8CB791DCC870700769DF0 /* QMUISearchBar.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA8A1DCC870700769DF0 /* QMUISearchBar.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB7B1DCC870700769DF0 /* QMUISearchBar.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA8B1DCC870700769DF0 /* QMUISearchBar.m */; }; - CDB8CB7C1DCC870700769DF0 /* QMUISearchBar.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA8B1DCC870700769DF0 /* QMUISearchBar.m */; }; - CDB8CB7D1DCC870700769DF0 /* QMUISearchController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA8C1DCC870700769DF0 /* QMUISearchController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB7F1DCC870700769DF0 /* QMUISearchController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA8D1DCC870700769DF0 /* QMUISearchController.m */; }; - CDB8CB801DCC870700769DF0 /* QMUISearchController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA8D1DCC870700769DF0 /* QMUISearchController.m */; }; - CDB8CB811DCC870700769DF0 /* QMUISegmentedControl.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA8E1DCC870700769DF0 /* QMUISegmentedControl.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB831DCC870700769DF0 /* QMUISegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA8F1DCC870700769DF0 /* QMUISegmentedControl.m */; }; - CDB8CB841DCC870700769DF0 /* QMUISegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA8F1DCC870700769DF0 /* QMUISegmentedControl.m */; }; - CDB8CB891DCC870700769DF0 /* QMUITableView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA921DCC870700769DF0 /* QMUITableView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB8B1DCC870700769DF0 /* QMUITableView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA931DCC870700769DF0 /* QMUITableView.m */; }; - CDB8CB8C1DCC870700769DF0 /* QMUITableView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA931DCC870700769DF0 /* QMUITableView.m */; }; - CDB8CB8D1DCC870700769DF0 /* QMUITableViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA941DCC870700769DF0 /* QMUITableViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB8F1DCC870700769DF0 /* QMUITableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA951DCC870700769DF0 /* QMUITableViewCell.m */; }; - CDB8CB901DCC870700769DF0 /* QMUITableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA951DCC870700769DF0 /* QMUITableViewCell.m */; }; - CDB8CB911DCC870700769DF0 /* QMUITextField.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA961DCC870700769DF0 /* QMUITextField.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB931DCC870700769DF0 /* QMUITextField.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA971DCC870700769DF0 /* QMUITextField.m */; }; - CDB8CB941DCC870700769DF0 /* QMUITextField.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA971DCC870700769DF0 /* QMUITextField.m */; }; - CDB8CB951DCC870700769DF0 /* QMUITextView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA981DCC870700769DF0 /* QMUITextView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB971DCC870700769DF0 /* QMUITextView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA991DCC870700769DF0 /* QMUITextView.m */; }; - CDB8CB981DCC870700769DF0 /* QMUITextView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA991DCC870700769DF0 /* QMUITextView.m */; }; CDB8CB991DCC870700769DF0 /* UIActivityIndicatorView+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA9A1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB9B1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA9B1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.m */; }; CDB8CB9C1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA9B1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.m */; }; CDB8CB9D1DCC870700769DF0 /* UIBezierPath+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA9C1DCC870700769DF0 /* UIBezierPath+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CB9F1DCC870700769DF0 /* UIBezierPath+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA9D1DCC870700769DF0 /* UIBezierPath+QMUI.m */; }; CDB8CBA01DCC870700769DF0 /* UIBezierPath+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA9D1DCC870700769DF0 /* UIBezierPath+QMUI.m */; }; CDB8CBA11DCC870700769DF0 /* UIButton+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA9E1DCC870700769DF0 /* UIButton+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBA31DCC870700769DF0 /* UIButton+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA9F1DCC870700769DF0 /* UIButton+QMUI.m */; }; CDB8CBA41DCC870700769DF0 /* UIButton+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA9F1DCC870700769DF0 /* UIButton+QMUI.m */; }; CDB8CBA51DCC870700769DF0 /* UICollectionView+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAA01DCC870700769DF0 /* UICollectionView+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBA71DCC870700769DF0 /* UICollectionView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAA11DCC870700769DF0 /* UICollectionView+QMUI.m */; }; CDB8CBA81DCC870700769DF0 /* UICollectionView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAA11DCC870700769DF0 /* UICollectionView+QMUI.m */; }; CDB8CBA91DCC870700769DF0 /* UIColor+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAA21DCC870700769DF0 /* UIColor+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBAB1DCC870700769DF0 /* UIColor+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAA31DCC870700769DF0 /* UIColor+QMUI.m */; }; CDB8CBAC1DCC870800769DF0 /* UIColor+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAA31DCC870700769DF0 /* UIColor+QMUI.m */; }; CDB8CBAD1DCC870800769DF0 /* UIControl+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAA41DCC870700769DF0 /* UIControl+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBAF1DCC870800769DF0 /* UIControl+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAA51DCC870700769DF0 /* UIControl+QMUI.m */; }; CDB8CBB01DCC870800769DF0 /* UIControl+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAA51DCC870700769DF0 /* UIControl+QMUI.m */; }; CDB8CBB11DCC870800769DF0 /* UIFont+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAA61DCC870700769DF0 /* UIFont+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBB31DCC870800769DF0 /* UIFont+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAA71DCC870700769DF0 /* UIFont+QMUI.m */; }; CDB8CBB41DCC870800769DF0 /* UIFont+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAA71DCC870700769DF0 /* UIFont+QMUI.m */; }; CDB8CBB51DCC870800769DF0 /* UIImage+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAA81DCC870700769DF0 /* UIImage+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBB71DCC870800769DF0 /* UIImage+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAA91DCC870700769DF0 /* UIImage+QMUI.m */; }; CDB8CBB81DCC870800769DF0 /* UIImage+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAA91DCC870700769DF0 /* UIImage+QMUI.m */; }; CDB8CBB91DCC870800769DF0 /* UIImageView+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAAA1DCC870700769DF0 /* UIImageView+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBBB1DCC870800769DF0 /* UIImageView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAAB1DCC870700769DF0 /* UIImageView+QMUI.m */; }; CDB8CBBC1DCC870800769DF0 /* UIImageView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAAB1DCC870700769DF0 /* UIImageView+QMUI.m */; }; CDB8CBBD1DCC870800769DF0 /* UILabel+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAAC1DCC870700769DF0 /* UILabel+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBBF1DCC870800769DF0 /* UILabel+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAAD1DCC870700769DF0 /* UILabel+QMUI.m */; }; CDB8CBC01DCC870800769DF0 /* UILabel+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAAD1DCC870700769DF0 /* UILabel+QMUI.m */; }; - CDB8CBC11DCC870800769DF0 /* UINavigationController+NavigationBarTransition.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAAE1DCC870700769DF0 /* UINavigationController+NavigationBarTransition.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBC31DCC870800769DF0 /* UINavigationController+NavigationBarTransition.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAAF1DCC870700769DF0 /* UINavigationController+NavigationBarTransition.m */; }; - CDB8CBC41DCC870800769DF0 /* UINavigationController+NavigationBarTransition.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAAF1DCC870700769DF0 /* UINavigationController+NavigationBarTransition.m */; }; CDB8CBC51DCC870800769DF0 /* UINavigationController+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAB01DCC870700769DF0 /* UINavigationController+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBC71DCC870800769DF0 /* UINavigationController+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAB11DCC870700769DF0 /* UINavigationController+QMUI.m */; }; CDB8CBC81DCC870800769DF0 /* UINavigationController+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAB11DCC870700769DF0 /* UINavigationController+QMUI.m */; }; CDB8CBC91DCC870800769DF0 /* UIScrollView+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAB21DCC870700769DF0 /* UIScrollView+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBCB1DCC870800769DF0 /* UIScrollView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAB31DCC870700769DF0 /* UIScrollView+QMUI.m */; }; CDB8CBCC1DCC870800769DF0 /* UIScrollView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAB31DCC870700769DF0 /* UIScrollView+QMUI.m */; }; CDB8CBCD1DCC870800769DF0 /* UISearchBar+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAB41DCC870700769DF0 /* UISearchBar+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBCF1DCC870800769DF0 /* UISearchBar+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAB51DCC870700769DF0 /* UISearchBar+QMUI.m */; }; CDB8CBD01DCC870800769DF0 /* UISearchBar+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAB51DCC870700769DF0 /* UISearchBar+QMUI.m */; }; CDB8CBD11DCC870800769DF0 /* UITabBarItem+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAB61DCC870700769DF0 /* UITabBarItem+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBD31DCC870800769DF0 /* UITabBarItem+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAB71DCC870700769DF0 /* UITabBarItem+QMUI.m */; }; CDB8CBD41DCC870800769DF0 /* UITabBarItem+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAB71DCC870700769DF0 /* UITabBarItem+QMUI.m */; }; CDB8CBD51DCC870800769DF0 /* UITableView+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAB81DCC870700769DF0 /* UITableView+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBD71DCC870800769DF0 /* UITableView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAB91DCC870700769DF0 /* UITableView+QMUI.m */; }; CDB8CBD81DCC870800769DF0 /* UITableView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAB91DCC870700769DF0 /* UITableView+QMUI.m */; }; CDB8CBD91DCC870800769DF0 /* UIView+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CABA1DCC870700769DF0 /* UIView+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBDB1DCC870800769DF0 /* UIView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CABB1DCC870700769DF0 /* UIView+QMUI.m */; }; CDB8CBDC1DCC870800769DF0 /* UIView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CABB1DCC870700769DF0 /* UIView+QMUI.m */; }; CDB8CBDD1DCC870800769DF0 /* UIViewController+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CABC1DCC870700769DF0 /* UIViewController+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBDF1DCC870800769DF0 /* UIViewController+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CABD1DCC870700769DF0 /* UIViewController+QMUI.m */; }; 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, ); }; }; - CDB8CBE31DCC870800769DF0 /* UIWindow+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CABF1DCC870700769DF0 /* UIWindow+QMUI.m */; }; CDB8CBE41DCC870800769DF0 /* UIWindow+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CABF1DCC870700769DF0 /* UIWindow+QMUI.m */; }; - CDB8CBE51DCC870800769DF0 /* QMUICommonTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAC11DCC870700769DF0 /* QMUICommonTableViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBE71DCC870800769DF0 /* QMUICommonTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAC21DCC870700769DF0 /* QMUICommonTableViewController.m */; }; - CDB8CBE81DCC870800769DF0 /* QMUICommonTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAC21DCC870700769DF0 /* QMUICommonTableViewController.m */; }; - CDB8CBE91DCC870800769DF0 /* QMUICommonViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAC31DCC870700769DF0 /* QMUICommonViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBEB1DCC870800769DF0 /* QMUICommonViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAC41DCC870700769DF0 /* QMUICommonViewController.m */; }; - CDB8CBEC1DCC870800769DF0 /* QMUICommonViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAC41DCC870700769DF0 /* QMUICommonViewController.m */; }; - CDB8CBED1DCC870800769DF0 /* QMUINavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAC51DCC870700769DF0 /* QMUINavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBEF1DCC870800769DF0 /* QMUINavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAC61DCC870700769DF0 /* QMUINavigationController.m */; }; - CDB8CBF01DCC870800769DF0 /* QMUINavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAC61DCC870700769DF0 /* QMUINavigationController.m */; }; - CDB8CBF11DCC870800769DF0 /* QMUITabBarViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAC71DCC870700769DF0 /* QMUITabBarViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDB8CBF31DCC870800769DF0 /* QMUITabBarViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAC81DCC870700769DF0 /* QMUITabBarViewController.m */; }; - CDB8CBF41DCC870800769DF0 /* QMUITabBarViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAC81DCC870700769DF0 /* QMUITabBarViewController.m */; }; - CDB8CBF61DCC870800769DF0 /* QMUI_QQEmotion.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CDB8CACA1DCC870700769DF0 /* QMUI_QQEmotion.bundle */; }; - CDB8CBF81DCC870800769DF0 /* QMUIResources.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CDB8CACB1DCC870700769DF0 /* QMUIResources.bundle */; }; - CDBCD5451DFAA6E0009EFEF5 /* QMUITableViewProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = CDBCD53F1DFAA286009EFEF5 /* QMUITableViewProtocols.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDD495171E60151500829B7D /* QMUIPopupMenuView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDD495151E60151500829B7D /* QMUIPopupMenuView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CDD495181E60151500829B7D /* QMUIPopupMenuView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDD495161E60151500829B7D /* QMUIPopupMenuView.m */; }; - CDD495191E60151500829B7D /* QMUIPopupMenuView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDD495161E60151500829B7D /* QMUIPopupMenuView.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, ); }; }; + CDC86FBE1F68D617000E8829 /* QMUIAssetsGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F431F68D5F9000E8829 /* QMUIAssetsGroup.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FBF1F68D617000E8829 /* QMUIAssetsManager.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F451F68D5F9000E8829 /* QMUIAssetsManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FC01F68D617000E8829 /* QMUIAlbumViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F481F68D5F9000E8829 /* QMUIAlbumViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FC11F68D617000E8829 /* QMUIImagePickerCollectionViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F4A1F68D5F9000E8829 /* QMUIImagePickerCollectionViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 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 */; }; + 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, ); }; }; + CDC86FCB1F68D617000E8829 /* QMUIDialogViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F5F1F68D5F9000E8829 /* QMUIDialogViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FCC1F68D617000E8829 /* QMUIEmotionView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F611F68D5F9000E8829 /* QMUIEmotionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FCD1F68D617000E8829 /* QMUIEmptyView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F631F68D5F9000E8829 /* QMUIEmptyView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FCE1F68D617000E8829 /* QMUIFloatLayoutView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F651F68D5F9000E8829 /* QMUIFloatLayoutView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FCF1F68D617000E8829 /* QMUIGridView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F671F68D5F9000E8829 /* QMUIGridView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FD21F68D617000E8829 /* QMUIKeyboardManager.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F6D1F68D5F9000E8829 /* QMUIKeyboardManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FD31F68D617000E8829 /* QMUILabel.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F6F1F68D5F9000E8829 /* QMUILabel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FD41F68D617000E8829 /* QMUIMarqueeLabel.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F711F68D5F9000E8829 /* QMUIMarqueeLabel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FD51F68D617000E8829 /* QMUIModalPresentationViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F731F68D5F9000E8829 /* QMUIModalPresentationViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FD61F68D617000E8829 /* QMUIMoreOperationController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F751F68D5F9000E8829 /* QMUIMoreOperationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FD71F68D617000E8829 /* QMUINavigationTitleView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F771F68D5F9000E8829 /* QMUINavigationTitleView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FD81F68D617000E8829 /* QMUIOrderedDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F791F68D5F9000E8829 /* QMUIOrderedDictionary.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FD91F68D617000E8829 /* QMUIPieProgressView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F7B1F68D5F9000E8829 /* QMUIPieProgressView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FDA1F68D617000E8829 /* QMUIPopupContainerView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F7D1F68D5F9000E8829 /* QMUIPopupContainerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FDC1F68D617000E8829 /* QMUIEmotionInputManager.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F811F68D5F9000E8829 /* QMUIEmotionInputManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 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, ); }; }; + 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, ); }; }; + CDC86FE41F68D617000E8829 /* QMUITestView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F901F68D5F9000E8829 /* QMUITestView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FE51F68D617000E8829 /* QMUITextField.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F921F68D5F9000E8829 /* QMUITextField.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FE61F68D617000E8829 /* QMUITextView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F941F68D5F9000E8829 /* QMUITextView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FE71F68D617000E8829 /* QMUITips.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F961F68D5F9000E8829 /* QMUITips.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FE91F68D617000E8829 /* QMUIZoomImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F9A1F68D5F9000E8829 /* QMUIZoomImageView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FEA1F68D617000E8829 /* QMUIStaticTableViewCellData.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F9D1F68D5F9000E8829 /* QMUIStaticTableViewCellData.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FEB1F68D617000E8829 /* QMUIStaticTableViewCellDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F9F1F68D5F9000E8829 /* QMUIStaticTableViewCellDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FEC1F68D617000E8829 /* UITableView+QMUIStaticCell.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FA11F68D5F9000E8829 /* UITableView+QMUIStaticCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FED1F68D617000E8829 /* QMUIToastAnimator.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FA41F68D5F9000E8829 /* QMUIToastAnimator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FEE1F68D617000E8829 /* QMUIToastBackgroundView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FA61F68D5F9000E8829 /* QMUIToastBackgroundView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FEF1F68D617000E8829 /* QMUIToastContentView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FA81F68D5F9000E8829 /* QMUIToastContentView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FF01F68D617000E8829 /* QMUIToastView.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FAA1F68D5F9000E8829 /* QMUIToastView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FF11F68D617000E8829 /* QMUICommonDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FAD1F68D5F9000E8829 /* QMUICommonDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FF21F68D617000E8829 /* QMUIConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FAE1F68D5F9000E8829 /* QMUIConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FF31F68D617000E8829 /* QMUIConfigurationMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FB01F68D5F9000E8829 /* QMUIConfigurationMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FF41F68D617000E8829 /* QMUICore.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FB11F68D5F9000E8829 /* QMUICore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FF51F68D617000E8829 /* QMUIHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FB21F68D5F9000E8829 /* QMUIHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FF61F68D617000E8829 /* QMUICommonTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FB51F68D5F9000E8829 /* QMUICommonTableViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FF71F68D617000E8829 /* QMUICommonViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FB71F68D5F9000E8829 /* QMUICommonViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FF81F68D617000E8829 /* QMUINavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FB91F68D5F9000E8829 /* QMUINavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FF91F68D617000E8829 /* QMUITabBarViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86FBB1F68D5F9000E8829 /* QMUITabBarViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CDC86FFA1F68D63B000E8829 /* QMUIAsset.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F421F68D5F9000E8829 /* QMUIAsset.m */; }; + CDC86FFB1F68D63B000E8829 /* QMUIAssetsGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F441F68D5F9000E8829 /* QMUIAssetsGroup.m */; }; + CDC86FFC1F68D63B000E8829 /* QMUIAssetsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F461F68D5F9000E8829 /* QMUIAssetsManager.m */; }; + CDC86FFD1F68D63B000E8829 /* QMUIAlbumViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F491F68D5F9000E8829 /* QMUIAlbumViewController.m */; }; + CDC86FFE1F68D63B000E8829 /* QMUIImagePickerCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F4B1F68D5F9000E8829 /* QMUIImagePickerCollectionViewCell.m */; }; + CDC86FFF1F68D63B000E8829 /* QMUIImagePickerHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F4D1F68D5F9000E8829 /* QMUIImagePickerHelper.m */; }; + CDC870001F68D63B000E8829 /* QMUIImagePickerPreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F4F1F68D5F9000E8829 /* QMUIImagePickerPreviewViewController.m */; }; + CDC870011F68D63B000E8829 /* QMUIImagePickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F511F68D5F9000E8829 /* QMUIImagePickerViewController.m */; }; + CDC870021F68D63B000E8829 /* UINavigationBar+Transition.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F541F68D5F9000E8829 /* UINavigationBar+Transition.m */; }; + CDC870031F68D63B000E8829 /* UINavigationController+NavigationBarTransition.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F561F68D5F9000E8829 /* UINavigationController+NavigationBarTransition.m */; }; + CDC870041F68D63B000E8829 /* QMUIAlertController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F581F68D5F9000E8829 /* QMUIAlertController.m */; }; + CDC870071F68D63B000E8829 /* QMUICollectionViewPagingLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F5E1F68D5F9000E8829 /* QMUICollectionViewPagingLayout.m */; }; + CDC870081F68D63B000E8829 /* QMUIDialogViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F601F68D5F9000E8829 /* QMUIDialogViewController.m */; }; + CDC870091F68D63B000E8829 /* QMUIEmotionView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F621F68D5F9000E8829 /* QMUIEmotionView.m */; }; + CDC8700A1F68D63B000E8829 /* QMUIEmptyView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F641F68D5F9000E8829 /* QMUIEmptyView.m */; }; + CDC8700B1F68D63B000E8829 /* QMUIFloatLayoutView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F661F68D5F9000E8829 /* QMUIFloatLayoutView.m */; }; + CDC8700C1F68D63B000E8829 /* QMUIGridView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F681F68D5F9000E8829 /* QMUIGridView.m */; }; + CDC8700F1F68D63B000E8829 /* QMUIKeyboardManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F6E1F68D5F9000E8829 /* QMUIKeyboardManager.m */; }; + CDC870101F68D63B000E8829 /* QMUILabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F701F68D5F9000E8829 /* QMUILabel.m */; }; + CDC870111F68D63B000E8829 /* QMUIMarqueeLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F721F68D5F9000E8829 /* QMUIMarqueeLabel.m */; }; + CDC870121F68D63B000E8829 /* QMUIModalPresentationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F741F68D5F9000E8829 /* QMUIModalPresentationViewController.m */; }; + CDC870131F68D63B000E8829 /* QMUIMoreOperationController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F761F68D5F9000E8829 /* QMUIMoreOperationController.m */; }; + CDC870141F68D63B000E8829 /* QMUINavigationTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F781F68D5F9000E8829 /* QMUINavigationTitleView.m */; }; + CDC870151F68D63B000E8829 /* QMUIOrderedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F7A1F68D5F9000E8829 /* QMUIOrderedDictionary.m */; }; + CDC870161F68D63B000E8829 /* QMUIPieProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F7C1F68D5F9000E8829 /* QMUIPieProgressView.m */; }; + CDC870171F68D63B000E8829 /* QMUIPopupContainerView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F7E1F68D5F9000E8829 /* QMUIPopupContainerView.m */; }; + CDC870191F68D63B000E8829 /* QMUIEmotionInputManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F821F68D5F9000E8829 /* QMUIEmotionInputManager.m */; }; + 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 */; }; + 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 */; }; + CDC870211F68D63B000E8829 /* QMUITextField.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F931F68D5F9000E8829 /* QMUITextField.m */; }; + CDC870221F68D63B000E8829 /* QMUITextView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F951F68D5F9000E8829 /* QMUITextView.m */; }; + CDC870231F68D63B000E8829 /* QMUITips.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F971F68D5F9000E8829 /* QMUITips.m */; }; + CDC870251F68D63B000E8829 /* QMUIZoomImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F9B1F68D5F9000E8829 /* QMUIZoomImageView.m */; }; + CDC870261F68D63B000E8829 /* QMUIStaticTableViewCellData.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86F9E1F68D5F9000E8829 /* QMUIStaticTableViewCellData.m */; }; + CDC870271F68D63B000E8829 /* QMUIStaticTableViewCellDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FA01F68D5F9000E8829 /* QMUIStaticTableViewCellDataSource.m */; }; + CDC870281F68D63B000E8829 /* UITableView+QMUIStaticCell.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FA21F68D5F9000E8829 /* UITableView+QMUIStaticCell.m */; }; + CDC870291F68D63B000E8829 /* QMUIToastAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FA51F68D5F9000E8829 /* QMUIToastAnimator.m */; }; + CDC8702A1F68D63B000E8829 /* QMUIToastBackgroundView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FA71F68D5F9000E8829 /* QMUIToastBackgroundView.m */; }; + CDC8702B1F68D63B000E8829 /* QMUIToastContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FA91F68D5F9000E8829 /* QMUIToastContentView.m */; }; + CDC8702C1F68D63B000E8829 /* QMUIToastView.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FAB1F68D5F9000E8829 /* QMUIToastView.m */; }; + CDC8702D1F68D63B000E8829 /* QMUIConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FAF1F68D5F9000E8829 /* QMUIConfiguration.m */; }; + CDC8702E1F68D63B000E8829 /* QMUIHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FB31F68D5F9000E8829 /* QMUIHelper.m */; }; + CDC8702F1F68D63B000E8829 /* QMUICommonTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC86FB61F68D5F9000E8829 /* QMUICommonTableViewController.m */; }; + 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 */; }; + 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, ); }; }; + 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 */; }; - FE1FBCAC1E8BA73000C6C01A /* 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, ); }; }; - FE1FBCB01E8BA79000C6C01A /* UITextView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = FE1FBCAE1E8BA79000C6C01A /* UITextView+QMUI.m */; }; FE1FBCB11E8BA79000C6C01A /* UITextView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = FE1FBCAE1E8BA79000C6C01A /* UITextView+QMUI.m */; }; - FE43652C1E8CC9EF00B5D34B /* QMUIKeyboardManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FE43652A1E8CC9EF00B5D34B /* QMUIKeyboardManager.m */; }; - FE43652D1E8CC9EF00B5D34B /* QMUIKeyboardManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FE43652A1E8CC9EF00B5D34B /* QMUIKeyboardManager.m */; }; - FE43652E1E8CC9EF00B5D34B /* QMUIKeyboardManager.h in Headers */ = {isa = PBXBuildFile; fileRef = FE43652B1E8CC9EF00B5D34B /* QMUIKeyboardManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 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 PBXCopyFilesBuildPhase section */ - 16E46D481B00D8C1002B7DB8 /* Copy Files */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "include/$(PRODUCT_NAME)"; - dstSubfolderSpec = 16; - files = ( - ); - name = "Copy Files"; - runOnlyForDeploymentPostprocessing = 0; +/* Begin PBXContainerItemProxy section */ + CD4EA577228C401E00A55066 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CD44C1BD1956D5970098D0A2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = FE0AFAD01D82B9D8000D21D9; + remoteInfo = QMUIKit; }; -/* End PBXCopyFilesBuildPhase section */ +/* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 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 = ""; }; - 16E46D4A1B00D8C2002B7DB8 /* libQMUI.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libQMUI.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 16F6B6381DFD3AF500E58171 /* QMUIToastView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIToastView.h; sourceTree = ""; }; - 16F6B6391DFD3AF500E58171 /* QMUIToastView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIToastView.m; sourceTree = ""; }; - 16F6B6421DFD571200E58171 /* QMUIToastBackgroundView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIToastBackgroundView.h; sourceTree = ""; }; - 16F6B6431DFD571200E58171 /* QMUIToastBackgroundView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIToastBackgroundView.m; sourceTree = ""; }; - 16F6B64C1DFD9ADD00E58171 /* QMUIToastContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIToastContentView.h; sourceTree = ""; }; - 16F6B64D1DFD9ADD00E58171 /* QMUIToastContentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIToastContentView.m; sourceTree = ""; }; - 16F6B6511DFEC39F00E58171 /* QMUIToastAnimator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIToastAnimator.h; sourceTree = ""; }; - 16F6B6521DFEC39F00E58171 /* QMUIToastAnimator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIToastAnimator.m; sourceTree = ""; }; - 36C72E0E1DE826B800F5F116 /* UINavigationBar+Transition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UINavigationBar+Transition.h"; sourceTree = ""; }; - 36C72E0F1DE826B800F5F116 /* UINavigationBar+Transition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UINavigationBar+Transition.m"; sourceTree = ""; }; + 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 = ""; }; + 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; }; - CD27AB501ECC48C90000B4D0 /* QMUICommonDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUICommonDefines.h; sourceTree = ""; }; - CD27AB511ECC48C90000B4D0 /* QMUIConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIConfiguration.h; sourceTree = ""; }; - CD27AB521ECC48C90000B4D0 /* QMUIConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIConfiguration.m; sourceTree = ""; }; - CD27AB531ECC48C90000B4D0 /* QMUIConfigurationMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIConfigurationMacros.h; sourceTree = ""; }; - CD27AB541ECC48C90000B4D0 /* QMUIHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIHelper.h; sourceTree = ""; }; - CD27AB551ECC48C90000B4D0 /* QMUIHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIHelper.m; sourceTree = ""; }; - CD27AB5E1ECC48D70000B4D0 /* QMUICore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUICore.h; sourceTree = ""; }; - CD2B01F11EDEB42D00183450 /* QMUIMarqueeLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIMarqueeLabel.h; sourceTree = ""; }; - CD2B01F21EDEB42D00183450 /* QMUIMarqueeLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIMarqueeLabel.m; sourceTree = ""; }; + 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 = ""; }; + CD046C3F2018668900092035 /* QMUILogItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILogItem.h; sourceTree = ""; }; + CD046C402018668900092035 /* QMUILogItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUILogItem.m; sourceTree = ""; }; + CD046C432018670900092035 /* QMUILogNameManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILogNameManager.h; sourceTree = ""; }; + CD046C442018670900092035 /* QMUILogNameManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUILogNameManager.m; sourceTree = ""; }; + 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 = ""; }; + CD18BC7221760D5900E2A4E6 /* QMUINavigationBarScrollingAnimator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUINavigationBarScrollingAnimator.m; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; - CD44C1D21956D5970098D0A2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - CD44C1D41956D5970098D0A2 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - CD44C1D61956D5970098D0A2 /* qmui-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "qmui-Prefix.pch"; 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 = ""; }; - CD53876F1EE004A300654A73 /* QMUISlider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUISlider.h; sourceTree = ""; }; - CD5387701EE004A300654A73 /* QMUISlider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUISlider.m; sourceTree = ""; }; - CD6CC6371EF911DF00602EDD /* QMUIStaticTableViewCellData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIStaticTableViewCellData.h; sourceTree = ""; }; - CD6CC6381EF911DF00602EDD /* QMUIStaticTableViewCellData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIStaticTableViewCellData.m; sourceTree = ""; }; - CD6CC6391EF911DF00602EDD /* QMUIStaticTableViewCellDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIStaticTableViewCellDataSource.h; sourceTree = ""; }; - CD6CC63A1EF911DF00602EDD /* QMUIStaticTableViewCellDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIStaticTableViewCellDataSource.m; sourceTree = ""; }; - CD6CC6411EF9123000602EDD /* UITableView+QMUIStaticCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITableView+QMUIStaticCell.h"; sourceTree = ""; }; - CD6CC6421EF9123000602EDD /* UITableView+QMUIStaticCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITableView+QMUIStaticCell.m"; sourceTree = ""; }; - CD78CBC81DEE9D6300910DCE /* QMUIImagePreviewViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIImagePreviewViewController.h; sourceTree = ""; }; - CD78CBC91DEE9D6300910DCE /* QMUIImagePreviewViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePreviewViewController.m; sourceTree = ""; }; - CD78CBCD1DEE9E3500910DCE /* QMUIImagePreviewView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIImagePreviewView.h; sourceTree = ""; }; - CD78CBCE1DEE9E3500910DCE /* QMUIImagePreviewView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePreviewView.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 = ""; }; + CD745E2B21CA5B8E006EC132 /* QMUIImagePreviewView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePreviewView.m; sourceTree = ""; }; + CD745E3021CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIImagePreviewViewTransitionAnimator.h; sourceTree = ""; }; + 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 = ""; }; + CD82C0B2206A82520046EED2 /* NSObject+QMUIMultipleDelegates.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSObject+QMUIMultipleDelegates.m"; sourceTree = ""; }; CD84F31B1E52DBEA00546111 /* UITabBar+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITabBar+QMUI.h"; sourceTree = ""; }; CD84F31C1E52DBEA00546111 /* UITabBar+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UITabBar+QMUI.m"; sourceTree = ""; }; - CD9D18FA1DD462200020F268 /* QMUIFloatLayoutView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIFloatLayoutView.h; sourceTree = ""; }; - CD9D18FB1DD462200020F268 /* QMUIFloatLayoutView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIFloatLayoutView.m; sourceTree = ""; }; + CD8AA7A921E8B9D600BA7369 /* QMUIConsole.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIConsole.h; sourceTree = ""; }; + CD8AA7AA21E8B9D600BA7369 /* QMUIConsole.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIConsole.m; sourceTree = ""; }; + CD8AA7AD21E8BF0B00BA7369 /* QMUIConsoleToolbar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIConsoleToolbar.h; sourceTree = ""; }; + CD8AA7AE21E8BF0B00BA7369 /* QMUIConsoleToolbar.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIConsoleToolbar.m; sourceTree = ""; }; + CD8AA7B121E8C0F300BA7369 /* QMUIConsoleViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIConsoleViewController.h; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; - CDB8CA3E1DCC870700769DF0 /* QMUIAsset.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIAsset.h; sourceTree = ""; }; - CDB8CA3F1DCC870700769DF0 /* QMUIAsset.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIAsset.m; sourceTree = ""; }; - CDB8CA401DCC870700769DF0 /* QMUIAssetsGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIAssetsGroup.h; sourceTree = ""; }; - CDB8CA411DCC870700769DF0 /* QMUIAssetsGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIAssetsGroup.m; sourceTree = ""; }; - CDB8CA421DCC870700769DF0 /* QMUIAssetsManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIAssetsManager.h; sourceTree = ""; }; - CDB8CA431DCC870700769DF0 /* QMUIAssetsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIAssetsManager.m; sourceTree = ""; }; - CDB8CA451DCC870700769DF0 /* QMUIAlbumViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIAlbumViewController.h; sourceTree = ""; }; - CDB8CA461DCC870700769DF0 /* QMUIAlbumViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIAlbumViewController.m; sourceTree = ""; }; - CDB8CA471DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIImagePickerCollectionViewCell.h; sourceTree = ""; }; - CDB8CA481DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePickerCollectionViewCell.m; sourceTree = ""; }; - CDB8CA491DCC870700769DF0 /* QMUIImagePickerHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIImagePickerHelper.h; sourceTree = ""; }; - CDB8CA4A1DCC870700769DF0 /* QMUIImagePickerHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePickerHelper.m; sourceTree = ""; }; - CDB8CA4B1DCC870700769DF0 /* QMUIImagePickerPreviewViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIImagePickerPreviewViewController.h; sourceTree = ""; }; - CDB8CA4C1DCC870700769DF0 /* QMUIImagePickerPreviewViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePickerPreviewViewController.m; sourceTree = ""; }; - CDB8CA4D1DCC870700769DF0 /* QMUIImagePickerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIImagePickerViewController.h; sourceTree = ""; }; - CDB8CA4E1DCC870700769DF0 /* QMUIImagePickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePickerViewController.m; sourceTree = ""; }; - CDB8CA4F1DCC870700769DF0 /* QMUIDialogViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIDialogViewController.h; sourceTree = ""; }; - CDB8CA501DCC870700769DF0 /* QMUIDialogViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIDialogViewController.m; sourceTree = ""; }; - CDB8CA511DCC870700769DF0 /* QMUIEmotionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIEmotionView.h; sourceTree = ""; }; - CDB8CA521DCC870700769DF0 /* QMUIEmotionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIEmotionView.m; sourceTree = ""; }; - CDB8CA531DCC870700769DF0 /* QMUIEmptyView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIEmptyView.h; sourceTree = ""; }; - CDB8CA541DCC870700769DF0 /* QMUIEmptyView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIEmptyView.m; sourceTree = ""; }; - CDB8CA551DCC870700769DF0 /* QMUIGridView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIGridView.h; sourceTree = ""; }; - CDB8CA561DCC870700769DF0 /* QMUIGridView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIGridView.m; sourceTree = ""; }; - CDB8CA571DCC870700769DF0 /* QMUIModalPresentationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIModalPresentationViewController.h; sourceTree = ""; }; - CDB8CA581DCC870700769DF0 /* QMUIModalPresentationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIModalPresentationViewController.m; sourceTree = ""; }; - CDB8CA591DCC870700769DF0 /* QMUIMoreOperationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIMoreOperationController.h; sourceTree = ""; }; - CDB8CA5A1DCC870700769DF0 /* QMUIMoreOperationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIMoreOperationController.m; sourceTree = ""; }; - CDB8CA5B1DCC870700769DF0 /* QMUINavigationTitleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUINavigationTitleView.h; sourceTree = ""; }; - CDB8CA5C1DCC870700769DF0 /* QMUINavigationTitleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUINavigationTitleView.m; sourceTree = ""; }; - CDB8CA5D1DCC870700769DF0 /* QMUIOrderedDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIOrderedDictionary.h; sourceTree = ""; }; - CDB8CA5E1DCC870700769DF0 /* QMUIOrderedDictionary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIOrderedDictionary.m; sourceTree = ""; }; - CDB8CA5F1DCC870700769DF0 /* QMUIPieProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIPieProgressView.h; sourceTree = ""; }; - CDB8CA601DCC870700769DF0 /* QMUIPieProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIPieProgressView.m; sourceTree = ""; }; - CDB8CA611DCC870700769DF0 /* QMUIPopupContainerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIPopupContainerView.h; sourceTree = ""; }; - CDB8CA621DCC870700769DF0 /* QMUIPopupContainerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIPopupContainerView.m; sourceTree = ""; }; - CDB8CA6B1DCC870700769DF0 /* QMUIQQEmotionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIQQEmotionManager.h; sourceTree = ""; }; - CDB8CA6C1DCC870700769DF0 /* QMUIQQEmotionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIQQEmotionManager.m; sourceTree = ""; }; - CDB8CA6D1DCC870700769DF0 /* QMUITestView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUITestView.h; sourceTree = ""; }; - CDB8CA6E1DCC870700769DF0 /* QMUITestView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUITestView.m; sourceTree = ""; }; - CDB8CA6F1DCC870700769DF0 /* QMUITips.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUITips.h; sourceTree = ""; }; - CDB8CA701DCC870700769DF0 /* QMUITips.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUITips.m; sourceTree = ""; }; - CDB8CA711DCC870700769DF0 /* QMUIVisualEffectView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIVisualEffectView.h; sourceTree = ""; }; - CDB8CA721DCC870700769DF0 /* QMUIVisualEffectView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIVisualEffectView.m; sourceTree = ""; }; - CDB8CA731DCC870700769DF0 /* QMUIZoomImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIZoomImageView.h; sourceTree = ""; }; - CDB8CA741DCC870700769DF0 /* QMUIZoomImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIZoomImageView.m; sourceTree = ""; }; CDB8CA761DCC870700769DF0 /* CALayer+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CALayer+QMUI.h"; sourceTree = ""; }; CDB8CA771DCC870700769DF0 /* CALayer+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CALayer+QMUI.m"; sourceTree = ""; }; CDB8CA781DCC870700769DF0 /* NSAttributedString+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+QMUI.h"; sourceTree = ""; }; @@ -389,30 +509,6 @@ CDB8CA7D1DCC870700769DF0 /* NSParagraphStyle+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSParagraphStyle+QMUI.m"; sourceTree = ""; }; CDB8CA7E1DCC870700769DF0 /* NSString+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+QMUI.h"; sourceTree = ""; }; CDB8CA7F1DCC870700769DF0 /* NSString+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+QMUI.m"; sourceTree = ""; }; - CDB8CA801DCC870700769DF0 /* QMUIAlertController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIAlertController.h; sourceTree = ""; }; - CDB8CA811DCC870700769DF0 /* QMUIAlertController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIAlertController.m; sourceTree = ""; }; - CDB8CA821DCC870700769DF0 /* QMUIButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIButton.h; sourceTree = ""; }; - CDB8CA831DCC870700769DF0 /* QMUIButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIButton.m; sourceTree = ""; }; - CDB8CA841DCC870700769DF0 /* QMUICellHeightCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUICellHeightCache.h; sourceTree = ""; }; - CDB8CA851DCC870700769DF0 /* QMUICellHeightCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUICellHeightCache.m; sourceTree = ""; }; - CDB8CA861DCC870700769DF0 /* QMUICollectionViewPagingLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUICollectionViewPagingLayout.h; sourceTree = ""; }; - CDB8CA871DCC870700769DF0 /* QMUICollectionViewPagingLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUICollectionViewPagingLayout.m; sourceTree = ""; }; - CDB8CA881DCC870700769DF0 /* QMUILabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUILabel.h; sourceTree = ""; }; - CDB8CA891DCC870700769DF0 /* QMUILabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUILabel.m; sourceTree = ""; }; - CDB8CA8A1DCC870700769DF0 /* QMUISearchBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUISearchBar.h; sourceTree = ""; }; - CDB8CA8B1DCC870700769DF0 /* QMUISearchBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUISearchBar.m; sourceTree = ""; }; - CDB8CA8C1DCC870700769DF0 /* QMUISearchController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUISearchController.h; sourceTree = ""; }; - CDB8CA8D1DCC870700769DF0 /* QMUISearchController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUISearchController.m; sourceTree = ""; }; - CDB8CA8E1DCC870700769DF0 /* QMUISegmentedControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUISegmentedControl.h; sourceTree = ""; }; - CDB8CA8F1DCC870700769DF0 /* QMUISegmentedControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUISegmentedControl.m; sourceTree = ""; }; - CDB8CA921DCC870700769DF0 /* QMUITableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUITableView.h; sourceTree = ""; }; - CDB8CA931DCC870700769DF0 /* QMUITableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUITableView.m; sourceTree = ""; }; - CDB8CA941DCC870700769DF0 /* QMUITableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUITableViewCell.h; sourceTree = ""; }; - CDB8CA951DCC870700769DF0 /* QMUITableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUITableViewCell.m; sourceTree = ""; }; - CDB8CA961DCC870700769DF0 /* QMUITextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUITextField.h; sourceTree = ""; }; - CDB8CA971DCC870700769DF0 /* QMUITextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUITextField.m; sourceTree = ""; }; - CDB8CA981DCC870700769DF0 /* QMUITextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUITextView.h; sourceTree = ""; }; - CDB8CA991DCC870700769DF0 /* QMUITextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUITextView.m; sourceTree = ""; }; CDB8CA9A1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIActivityIndicatorView+QMUI.h"; sourceTree = ""; }; CDB8CA9B1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIActivityIndicatorView+QMUI.m"; sourceTree = ""; }; CDB8CA9C1DCC870700769DF0 /* UIBezierPath+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIBezierPath+QMUI.h"; sourceTree = ""; }; @@ -433,8 +529,6 @@ CDB8CAAB1DCC870700769DF0 /* UIImageView+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImageView+QMUI.m"; sourceTree = ""; }; CDB8CAAC1DCC870700769DF0 /* UILabel+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UILabel+QMUI.h"; sourceTree = ""; }; CDB8CAAD1DCC870700769DF0 /* UILabel+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UILabel+QMUI.m"; sourceTree = ""; }; - CDB8CAAE1DCC870700769DF0 /* UINavigationController+NavigationBarTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UINavigationController+NavigationBarTransition.h"; sourceTree = ""; }; - CDB8CAAF1DCC870700769DF0 /* UINavigationController+NavigationBarTransition.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UINavigationController+NavigationBarTransition.m"; sourceTree = ""; }; CDB8CAB01DCC870700769DF0 /* UINavigationController+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UINavigationController+QMUI.h"; sourceTree = ""; }; CDB8CAB11DCC870700769DF0 /* UINavigationController+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UINavigationController+QMUI.m"; sourceTree = ""; }; CDB8CAB21DCC870700769DF0 /* UIScrollView+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+QMUI.h"; sourceTree = ""; }; @@ -451,33 +545,195 @@ 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 = ""; }; - CDB8CAC11DCC870700769DF0 /* QMUICommonTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUICommonTableViewController.h; sourceTree = ""; }; - CDB8CAC21DCC870700769DF0 /* QMUICommonTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUICommonTableViewController.m; sourceTree = ""; }; - CDB8CAC31DCC870700769DF0 /* QMUICommonViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUICommonViewController.h; sourceTree = ""; }; - CDB8CAC41DCC870700769DF0 /* QMUICommonViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUICommonViewController.m; sourceTree = ""; }; - CDB8CAC51DCC870700769DF0 /* QMUINavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUINavigationController.h; sourceTree = ""; }; - CDB8CAC61DCC870700769DF0 /* QMUINavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUINavigationController.m; sourceTree = ""; }; - CDB8CAC71DCC870700769DF0 /* QMUITabBarViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUITabBarViewController.h; sourceTree = ""; }; - CDB8CAC81DCC870700769DF0 /* QMUITabBarViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUITabBarViewController.m; sourceTree = ""; }; - CDB8CACA1DCC870700769DF0 /* QMUI_QQEmotion.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = QMUI_QQEmotion.bundle; sourceTree = ""; }; - CDB8CACB1DCC870700769DF0 /* QMUIResources.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = QMUIResources.bundle; sourceTree = ""; }; - CDBCD53F1DFAA286009EFEF5 /* QMUITableViewProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUITableViewProtocols.h; sourceTree = ""; }; - CDD495151E60151500829B7D /* QMUIPopupMenuView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIPopupMenuView.h; sourceTree = ""; }; - CDD495161E60151500829B7D /* QMUIPopupMenuView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIPopupMenuView.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 = ""; }; + 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 = ""; }; + CDC86F441F68D5F9000E8829 /* QMUIAssetsGroup.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIAssetsGroup.m; sourceTree = ""; }; + CDC86F451F68D5F9000E8829 /* QMUIAssetsManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIAssetsManager.h; sourceTree = ""; }; + CDC86F461F68D5F9000E8829 /* QMUIAssetsManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIAssetsManager.m; sourceTree = ""; }; + CDC86F481F68D5F9000E8829 /* QMUIAlbumViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIAlbumViewController.h; sourceTree = ""; }; + CDC86F491F68D5F9000E8829 /* QMUIAlbumViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIAlbumViewController.m; sourceTree = ""; }; + CDC86F4A1F68D5F9000E8829 /* QMUIImagePickerCollectionViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIImagePickerCollectionViewCell.h; sourceTree = ""; }; + CDC86F4B1F68D5F9000E8829 /* QMUIImagePickerCollectionViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePickerCollectionViewCell.m; sourceTree = ""; }; + CDC86F4C1F68D5F9000E8829 /* QMUIImagePickerHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIImagePickerHelper.h; sourceTree = ""; }; + CDC86F4D1F68D5F9000E8829 /* QMUIImagePickerHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePickerHelper.m; sourceTree = ""; }; + CDC86F4E1F68D5F9000E8829 /* QMUIImagePickerPreviewViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIImagePickerPreviewViewController.h; sourceTree = ""; }; + CDC86F4F1F68D5F9000E8829 /* QMUIImagePickerPreviewViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePickerPreviewViewController.m; sourceTree = ""; }; + CDC86F501F68D5F9000E8829 /* QMUIImagePickerViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIImagePickerViewController.h; sourceTree = ""; }; + CDC86F511F68D5F9000E8829 /* QMUIImagePickerViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePickerViewController.m; sourceTree = ""; }; + CDC86F531F68D5F9000E8829 /* UINavigationBar+Transition.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UINavigationBar+Transition.h"; sourceTree = ""; }; + CDC86F541F68D5F9000E8829 /* UINavigationBar+Transition.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UINavigationBar+Transition.m"; sourceTree = ""; }; + CDC86F551F68D5F9000E8829 /* UINavigationController+NavigationBarTransition.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UINavigationController+NavigationBarTransition.h"; sourceTree = ""; }; + CDC86F561F68D5F9000E8829 /* UINavigationController+NavigationBarTransition.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UINavigationController+NavigationBarTransition.m"; sourceTree = ""; }; + CDC86F571F68D5F9000E8829 /* QMUIAlertController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIAlertController.h; sourceTree = ""; }; + CDC86F581F68D5F9000E8829 /* QMUIAlertController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIAlertController.m; sourceTree = ""; }; + CDC86F5D1F68D5F9000E8829 /* QMUICollectionViewPagingLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUICollectionViewPagingLayout.h; sourceTree = ""; }; + CDC86F5E1F68D5F9000E8829 /* QMUICollectionViewPagingLayout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUICollectionViewPagingLayout.m; sourceTree = ""; }; + CDC86F5F1F68D5F9000E8829 /* QMUIDialogViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIDialogViewController.h; sourceTree = ""; }; + CDC86F601F68D5F9000E8829 /* QMUIDialogViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIDialogViewController.m; sourceTree = ""; }; + CDC86F611F68D5F9000E8829 /* QMUIEmotionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIEmotionView.h; sourceTree = ""; }; + CDC86F621F68D5F9000E8829 /* QMUIEmotionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIEmotionView.m; sourceTree = ""; }; + CDC86F631F68D5F9000E8829 /* QMUIEmptyView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIEmptyView.h; sourceTree = ""; }; + CDC86F641F68D5F9000E8829 /* QMUIEmptyView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIEmptyView.m; sourceTree = ""; }; + CDC86F651F68D5F9000E8829 /* QMUIFloatLayoutView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIFloatLayoutView.h; sourceTree = ""; }; + CDC86F661F68D5F9000E8829 /* QMUIFloatLayoutView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIFloatLayoutView.m; sourceTree = ""; }; + CDC86F671F68D5F9000E8829 /* QMUIGridView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIGridView.h; sourceTree = ""; }; + CDC86F681F68D5F9000E8829 /* QMUIGridView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIGridView.m; sourceTree = ""; }; + CDC86F6D1F68D5F9000E8829 /* QMUIKeyboardManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIKeyboardManager.h; sourceTree = ""; }; + CDC86F6E1F68D5F9000E8829 /* QMUIKeyboardManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIKeyboardManager.m; sourceTree = ""; }; + CDC86F6F1F68D5F9000E8829 /* QMUILabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILabel.h; sourceTree = ""; }; + CDC86F701F68D5F9000E8829 /* QMUILabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUILabel.m; sourceTree = ""; }; + CDC86F711F68D5F9000E8829 /* QMUIMarqueeLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIMarqueeLabel.h; sourceTree = ""; }; + CDC86F721F68D5F9000E8829 /* QMUIMarqueeLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIMarqueeLabel.m; sourceTree = ""; }; + CDC86F731F68D5F9000E8829 /* QMUIModalPresentationViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIModalPresentationViewController.h; sourceTree = ""; }; + CDC86F741F68D5F9000E8829 /* QMUIModalPresentationViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIModalPresentationViewController.m; sourceTree = ""; }; + CDC86F751F68D5F9000E8829 /* QMUIMoreOperationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIMoreOperationController.h; sourceTree = ""; }; + CDC86F761F68D5F9000E8829 /* QMUIMoreOperationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIMoreOperationController.m; sourceTree = ""; }; + CDC86F771F68D5F9000E8829 /* QMUINavigationTitleView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUINavigationTitleView.h; sourceTree = ""; }; + CDC86F781F68D5F9000E8829 /* QMUINavigationTitleView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUINavigationTitleView.m; sourceTree = ""; }; + CDC86F791F68D5F9000E8829 /* QMUIOrderedDictionary.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIOrderedDictionary.h; sourceTree = ""; }; + CDC86F7A1F68D5F9000E8829 /* QMUIOrderedDictionary.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIOrderedDictionary.m; sourceTree = ""; }; + CDC86F7B1F68D5F9000E8829 /* QMUIPieProgressView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIPieProgressView.h; sourceTree = ""; }; + CDC86F7C1F68D5F9000E8829 /* QMUIPieProgressView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIPieProgressView.m; sourceTree = ""; }; + CDC86F7D1F68D5F9000E8829 /* QMUIPopupContainerView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIPopupContainerView.h; sourceTree = ""; }; + CDC86F7E1F68D5F9000E8829 /* QMUIPopupContainerView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIPopupContainerView.m; sourceTree = ""; }; + CDC86F811F68D5F9000E8829 /* QMUIEmotionInputManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIEmotionInputManager.h; sourceTree = ""; }; + CDC86F821F68D5F9000E8829 /* QMUIEmotionInputManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIEmotionInputManager.m; sourceTree = ""; }; + CDC86F831F68D5F9000E8829 /* QMUISearchBar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUISearchBar.h; sourceTree = ""; }; + CDC86F841F68D5F9000E8829 /* QMUISearchBar.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUISearchBar.m; sourceTree = ""; }; + CDC86F851F68D5F9000E8829 /* QMUISearchController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUISearchController.h; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + CDC86F8E1F68D5F9000E8829 /* QMUITableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUITableViewCell.m; sourceTree = ""; }; + CDC86F8F1F68D5F9000E8829 /* QMUITableViewProtocols.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITableViewProtocols.h; sourceTree = ""; }; + CDC86F901F68D5F9000E8829 /* QMUITestView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITestView.h; sourceTree = ""; }; + CDC86F911F68D5F9000E8829 /* QMUITestView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUITestView.m; sourceTree = ""; }; + CDC86F921F68D5F9000E8829 /* QMUITextField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITextField.h; sourceTree = ""; }; + CDC86F931F68D5F9000E8829 /* QMUITextField.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUITextField.m; sourceTree = ""; }; + CDC86F941F68D5F9000E8829 /* QMUITextView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITextView.h; sourceTree = ""; }; + CDC86F951F68D5F9000E8829 /* QMUITextView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUITextView.m; sourceTree = ""; }; + CDC86F961F68D5F9000E8829 /* QMUITips.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITips.h; sourceTree = ""; }; + CDC86F971F68D5F9000E8829 /* QMUITips.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUITips.m; sourceTree = ""; }; + CDC86F9A1F68D5F9000E8829 /* QMUIZoomImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIZoomImageView.h; sourceTree = ""; }; + CDC86F9B1F68D5F9000E8829 /* QMUIZoomImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIZoomImageView.m; sourceTree = ""; }; + CDC86F9D1F68D5F9000E8829 /* QMUIStaticTableViewCellData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIStaticTableViewCellData.h; sourceTree = ""; }; + CDC86F9E1F68D5F9000E8829 /* QMUIStaticTableViewCellData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIStaticTableViewCellData.m; sourceTree = ""; }; + CDC86F9F1F68D5F9000E8829 /* QMUIStaticTableViewCellDataSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIStaticTableViewCellDataSource.h; sourceTree = ""; }; + CDC86FA01F68D5F9000E8829 /* QMUIStaticTableViewCellDataSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIStaticTableViewCellDataSource.m; sourceTree = ""; }; + CDC86FA11F68D5F9000E8829 /* UITableView+QMUIStaticCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITableView+QMUIStaticCell.h"; sourceTree = ""; }; + CDC86FA21F68D5F9000E8829 /* UITableView+QMUIStaticCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITableView+QMUIStaticCell.m"; sourceTree = ""; }; + CDC86FA41F68D5F9000E8829 /* QMUIToastAnimator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIToastAnimator.h; sourceTree = ""; }; + CDC86FA51F68D5F9000E8829 /* QMUIToastAnimator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIToastAnimator.m; sourceTree = ""; }; + CDC86FA61F68D5F9000E8829 /* QMUIToastBackgroundView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIToastBackgroundView.h; sourceTree = ""; }; + CDC86FA71F68D5F9000E8829 /* QMUIToastBackgroundView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIToastBackgroundView.m; sourceTree = ""; }; + CDC86FA81F68D5F9000E8829 /* QMUIToastContentView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIToastContentView.h; sourceTree = ""; }; + CDC86FA91F68D5F9000E8829 /* QMUIToastContentView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIToastContentView.m; sourceTree = ""; }; + CDC86FAA1F68D5F9000E8829 /* QMUIToastView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIToastView.h; sourceTree = ""; }; + CDC86FAB1F68D5F9000E8829 /* QMUIToastView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIToastView.m; sourceTree = ""; }; + CDC86FAD1F68D5F9000E8829 /* QMUICommonDefines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUICommonDefines.h; sourceTree = ""; }; + CDC86FAE1F68D5F9000E8829 /* QMUIConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIConfiguration.h; sourceTree = ""; }; + CDC86FAF1F68D5F9000E8829 /* QMUIConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIConfiguration.m; sourceTree = ""; }; + CDC86FB01F68D5F9000E8829 /* QMUIConfigurationMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIConfigurationMacros.h; sourceTree = ""; }; + CDC86FB11F68D5F9000E8829 /* QMUICore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUICore.h; sourceTree = ""; }; + CDC86FB21F68D5F9000E8829 /* QMUIHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIHelper.h; sourceTree = ""; }; + CDC86FB31F68D5F9000E8829 /* QMUIHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIHelper.m; sourceTree = ""; }; + CDC86FB51F68D5F9000E8829 /* QMUICommonTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUICommonTableViewController.h; sourceTree = ""; }; + CDC86FB61F68D5F9000E8829 /* QMUICommonTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUICommonTableViewController.m; sourceTree = ""; }; + CDC86FB71F68D5F9000E8829 /* QMUICommonViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUICommonViewController.h; sourceTree = ""; }; + CDC86FB81F68D5F9000E8829 /* QMUICommonViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUICommonViewController.m; sourceTree = ""; }; + CDC86FB91F68D5F9000E8829 /* QMUINavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUINavigationController.h; sourceTree = ""; }; + 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 = ""; }; + 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; }; FE1FBCA71E8BA61300C6C01A /* UITextField+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITextField+QMUI.h"; sourceTree = ""; }; 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 = ""; }; - FE43652A1E8CC9EF00B5D34B /* QMUIKeyboardManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIKeyboardManager.m; sourceTree = ""; }; - FE43652B1E8CC9EF00B5D34B /* QMUIKeyboardManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIKeyboardManager.h; 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 */ - 16E46D471B00D8C1002B7DB8 /* Frameworks */ = { + CD4EA56E228C401E00A55066 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CD4EA576228C401E00A55066 /* QMUIKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -485,32 +741,59 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CDFF5FB62369926300B63B92 /* Photos.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - CD27AB4F1ECC48C90000B4D0 /* UICore */ = { + CD046C3E2018665F00092035 /* QMUILog */ = { + isa = PBXGroup; + children = ( + CD046C4B2018698200092035 /* QMUILog.h */, + CD046C3F2018668900092035 /* QMUILogItem.h */, + CD046C402018668900092035 /* QMUILogItem.m */, + CD046C432018670900092035 /* QMUILogNameManager.h */, + CD046C442018670900092035 /* QMUILogNameManager.m */, + CD046C472018688F00092035 /* QMUILogger.h */, + CD046C482018688F00092035 /* QMUILogger.m */, + ); + path = QMUILog; + sourceTree = ""; + }; + CD349BAA2160ADBC008653D4 /* QMUIScrollAnimator */ = { + isa = PBXGroup; + children = ( + CD18BC7121760D5900E2A4E6 /* QMUINavigationBarScrollingAnimator.h */, + CD18BC7221760D5900E2A4E6 /* QMUINavigationBarScrollingAnimator.m */, + CD349BB52160B83D008653D4 /* QMUINavigationBarScrollingSnapAnimator.h */, + CD349BB62160B83D008653D4 /* QMUINavigationBarScrollingSnapAnimator.m */, + CD349BAB2160AF75008653D4 /* QMUIScrollAnimator.h */, + CD349BAC2160AF75008653D4 /* QMUIScrollAnimator.m */, + ); + path = QMUIScrollAnimator; + sourceTree = ""; + }; + CD43CB14207B98A10090346B /* QMUIButton */ = { isa = PBXGroup; children = ( - CD27AB501ECC48C90000B4D0 /* QMUICommonDefines.h */, - CD27AB511ECC48C90000B4D0 /* QMUIConfiguration.h */, - CD27AB521ECC48C90000B4D0 /* QMUIConfiguration.m */, - CD27AB531ECC48C90000B4D0 /* QMUIConfigurationMacros.h */, - CD27AB5E1ECC48D70000B4D0 /* QMUICore.h */, - CD27AB541ECC48C90000B4D0 /* QMUIHelper.h */, - CD27AB551ECC48C90000B4D0 /* QMUIHelper.m */, + CD43CB15207B98A10090346B /* QMUIButton.h */, + CD43CB16207B98A10090346B /* QMUIButton.m */, + CD43CB19207B98B60090346B /* QMUINavigationButton.h */, + CD43CB1A207B98B60090346B /* QMUINavigationButton.m */, + CD43CB1D207B9A510090346B /* QMUIToolbarButton.h */, + CD43CB1E207B9A510090346B /* QMUIToolbarButton.m */, ); - path = UICore; + path = QMUIButton; sourceTree = ""; }; CD44C1BC1956D5970098D0A2 = { isa = PBXGroup; children = ( + CD4EA572228C401E00A55066 /* QMUIKitTests */, CD44C1C71956D5970098D0A2 /* Frameworks */, CD44C1C61956D5970098D0A2 /* Products */, - CD44C1CE1956D5970098D0A2 /* qmui */, CD4DA9BF1E8E3B0500836A1A /* QMUIConfigurationTemplate */, CDB8CA2D1DCC870700769DF0 /* QMUIKit */, ); @@ -519,8 +802,8 @@ CD44C1C61956D5970098D0A2 /* Products */ = { isa = PBXGroup; children = ( - 16E46D4A1B00D8C2002B7DB8 /* libQMUI.a */, FE0AFAD11D82B9D8000D21D9 /* QMUIKit.framework */, + CD4EA571228C401E00A55066 /* QMUIKitTests.xctest */, ); name = Products; sourceTree = ""; @@ -529,6 +812,7 @@ isa = PBXGroup; children = ( 6D03A56D1B53895D003BDDE4 /* Photos.framework */, + CDFF5FB52369926300B63B92 /* Photos.framework */, CD44C1C81956D5970098D0A2 /* Foundation.framework */, CD44C1CA1956D5970098D0A2 /* CoreGraphics.framework */, CD44C1CC1956D5970098D0A2 /* UIKit.framework */, @@ -537,154 +821,147 @@ 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 */, - CD44C1D61956D5970098D0A2 /* qmui-Prefix.pch */, - CD44C1D01956D5970098D0A2 /* qmui-Info.plist */, - CD44C1D11956D5970098D0A2 /* InfoPlist.strings */, + 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 = ""; }; - CD6CC6361EF911DF00602EDD /* StaticTableView */ = { + CD513E24283527AA004A549D /* QMUIBarProtocol */ = { isa = PBXGroup; children = ( - CD6CC6371EF911DF00602EDD /* QMUIStaticTableViewCellData.h */, - CD6CC6381EF911DF00602EDD /* QMUIStaticTableViewCellData.m */, - CD6CC6391EF911DF00602EDD /* QMUIStaticTableViewCellDataSource.h */, - CD6CC63A1EF911DF00602EDD /* QMUIStaticTableViewCellDataSource.m */, - CD6CC6411EF9123000602EDD /* UITableView+QMUIStaticCell.h */, - CD6CC6421EF9123000602EDD /* UITableView+QMUIStaticCell.m */, + 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 = StaticTableView; + path = QMUIBarProtocol; sourceTree = ""; }; - CDB8CA2D1DCC870700769DF0 /* QMUIKit */ = { + CD6BE1472058C61000BE093E /* QMUICellHeightKeyCache */ = { isa = PBXGroup; children = ( - CDB8CA2E1DCC870700769DF0 /* Info.plist */, - CDB8CA2F1DCC870700769DF0 /* QMUIKit.h */, - CDB8CA3C1DCC870700769DF0 /* UIComponents */, - CD27AB4F1ECC48C90000B4D0 /* UICore */, - CDB8CA751DCC870700769DF0 /* UIKitExtensions */, - CDB8CAC01DCC870700769DF0 /* UIMainFrame */, - CDB8CAC91DCC870700769DF0 /* UIResources */, + CD6BE14C2058C64E00BE093E /* QMUICellHeightKeyCache.h */, + CD6BE14D2058C64E00BE093E /* QMUICellHeightKeyCache.m */, + CD6BE1542058C73600BE093E /* UITableView+QMUICellHeightKeyCache.h */, + CD6BE1552058C73600BE093E /* UITableView+QMUICellHeightKeyCache.m */, ); - path = QMUIKit; + path = QMUICellHeightKeyCache; sourceTree = ""; }; - CDB8CA3C1DCC870700769DF0 /* UIComponents */ = { + CD745E2721CA5B8E006EC132 /* QMUIImagePreviewView */ = { isa = PBXGroup; children = ( - CDB8CA3D1DCC870700769DF0 /* AssetLibrary */, - CDB8CA441DCC870700769DF0 /* ImagePickerLibrary */, - CDB8CA4F1DCC870700769DF0 /* QMUIDialogViewController.h */, - CDB8CA501DCC870700769DF0 /* QMUIDialogViewController.m */, - CDB8CA511DCC870700769DF0 /* QMUIEmotionView.h */, - CDB8CA521DCC870700769DF0 /* QMUIEmotionView.m */, - CDB8CA531DCC870700769DF0 /* QMUIEmptyView.h */, - CDB8CA541DCC870700769DF0 /* QMUIEmptyView.m */, - CD9D18FA1DD462200020F268 /* QMUIFloatLayoutView.h */, - CD9D18FB1DD462200020F268 /* QMUIFloatLayoutView.m */, - CDB8CA551DCC870700769DF0 /* QMUIGridView.h */, - CDB8CA561DCC870700769DF0 /* QMUIGridView.m */, - CD78CBCD1DEE9E3500910DCE /* QMUIImagePreviewView.h */, - CD78CBCE1DEE9E3500910DCE /* QMUIImagePreviewView.m */, - CD78CBC81DEE9D6300910DCE /* QMUIImagePreviewViewController.h */, - CD78CBC91DEE9D6300910DCE /* QMUIImagePreviewViewController.m */, - FE43652B1E8CC9EF00B5D34B /* QMUIKeyboardManager.h */, - FE43652A1E8CC9EF00B5D34B /* QMUIKeyboardManager.m */, - CD2B01F11EDEB42D00183450 /* QMUIMarqueeLabel.h */, - CD2B01F21EDEB42D00183450 /* QMUIMarqueeLabel.m */, - CDB8CA571DCC870700769DF0 /* QMUIModalPresentationViewController.h */, - CDB8CA581DCC870700769DF0 /* QMUIModalPresentationViewController.m */, - CDB8CA591DCC870700769DF0 /* QMUIMoreOperationController.h */, - CDB8CA5A1DCC870700769DF0 /* QMUIMoreOperationController.m */, - CDB8CA5B1DCC870700769DF0 /* QMUINavigationTitleView.h */, - CDB8CA5C1DCC870700769DF0 /* QMUINavigationTitleView.m */, - CDB8CA5D1DCC870700769DF0 /* QMUIOrderedDictionary.h */, - CDB8CA5E1DCC870700769DF0 /* QMUIOrderedDictionary.m */, - CDB8CA5F1DCC870700769DF0 /* QMUIPieProgressView.h */, - CDB8CA601DCC870700769DF0 /* QMUIPieProgressView.m */, - CDB8CA611DCC870700769DF0 /* QMUIPopupContainerView.h */, - CDB8CA621DCC870700769DF0 /* QMUIPopupContainerView.m */, - CDD495151E60151500829B7D /* QMUIPopupMenuView.h */, - CDD495161E60151500829B7D /* QMUIPopupMenuView.m */, - CDB8CA6B1DCC870700769DF0 /* QMUIQQEmotionManager.h */, - CDB8CA6C1DCC870700769DF0 /* QMUIQQEmotionManager.m */, - CDB8CA6D1DCC870700769DF0 /* QMUITestView.h */, - CDB8CA6E1DCC870700769DF0 /* QMUITestView.m */, - CDB8CA6F1DCC870700769DF0 /* QMUITips.h */, - CDB8CA701DCC870700769DF0 /* QMUITips.m */, - 16F6B6511DFEC39F00E58171 /* QMUIToastAnimator.h */, - 16F6B6521DFEC39F00E58171 /* QMUIToastAnimator.m */, - 16F6B6421DFD571200E58171 /* QMUIToastBackgroundView.h */, - 16F6B6431DFD571200E58171 /* QMUIToastBackgroundView.m */, - 16F6B64C1DFD9ADD00E58171 /* QMUIToastContentView.h */, - 16F6B64D1DFD9ADD00E58171 /* QMUIToastContentView.m */, - 16F6B6381DFD3AF500E58171 /* QMUIToastView.h */, - 16F6B6391DFD3AF500E58171 /* QMUIToastView.m */, - CDB8CA711DCC870700769DF0 /* QMUIVisualEffectView.h */, - CDB8CA721DCC870700769DF0 /* QMUIVisualEffectView.m */, - CDB8CA731DCC870700769DF0 /* QMUIZoomImageView.h */, - CDB8CA741DCC870700769DF0 /* QMUIZoomImageView.m */, - CD6CC6361EF911DF00602EDD /* StaticTableView */, + CD745E2821CA5B8E006EC132 /* QMUIImagePreviewView.h */, + CD745E2B21CA5B8E006EC132 /* QMUIImagePreviewView.m */, + CD745E2A21CA5B8E006EC132 /* QMUIImagePreviewViewController.h */, + CD745E2921CA5B8E006EC132 /* QMUIImagePreviewViewController.m */, + CD745E3021CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.h */, + CD745E3121CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.m */, ); - path = UIComponents; + path = QMUIImagePreviewView; sourceTree = ""; }; - CDB8CA3D1DCC870700769DF0 /* AssetLibrary */ = { + CD7A9A0B22C4AA2F0093DAB4 /* Components */ = { isa = PBXGroup; children = ( - CDB8CA3E1DCC870700769DF0 /* QMUIAsset.h */, - CDB8CA3F1DCC870700769DF0 /* QMUIAsset.m */, - CDB8CA401DCC870700769DF0 /* QMUIAssetsGroup.h */, - CDB8CA411DCC870700769DF0 /* QMUIAssetsGroup.m */, - CDB8CA421DCC870700769DF0 /* QMUIAssetsManager.h */, - CDB8CA431DCC870700769DF0 /* QMUIAssetsManager.m */, + CD7A9A0C22C4AA2F0093DAB4 /* QMUIThemeTests.m */, ); - path = AssetLibrary; + path = Components; sourceTree = ""; }; - CDB8CA441DCC870700769DF0 /* ImagePickerLibrary */ = { + CD82C0A42069EB850046EED2 /* QMUIMultipleDelegates */ = { isa = PBXGroup; children = ( - CDB8CA451DCC870700769DF0 /* QMUIAlbumViewController.h */, - CDB8CA461DCC870700769DF0 /* QMUIAlbumViewController.m */, - CDB8CA471DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.h */, - CDB8CA481DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.m */, - CDB8CA491DCC870700769DF0 /* QMUIImagePickerHelper.h */, - CDB8CA4A1DCC870700769DF0 /* QMUIImagePickerHelper.m */, - CDB8CA4B1DCC870700769DF0 /* QMUIImagePickerPreviewViewController.h */, - CDB8CA4C1DCC870700769DF0 /* QMUIImagePickerPreviewViewController.m */, - CDB8CA4D1DCC870700769DF0 /* QMUIImagePickerViewController.h */, - CDB8CA4E1DCC870700769DF0 /* QMUIImagePickerViewController.m */, + CD82C0B1206A82520046EED2 /* NSObject+QMUIMultipleDelegates.h */, + CD82C0B2206A82520046EED2 /* NSObject+QMUIMultipleDelegates.m */, + CD82C0AD206A2C3D0046EED2 /* QMUIMultipleDelegates.h */, + CD82C0AE206A2C3D0046EED2 /* QMUIMultipleDelegates.m */, ); - path = ImagePickerLibrary; + path = QMUIMultipleDelegates; + sourceTree = ""; + }; + CD8AA7A821E8B9D600BA7369 /* QMUIConsole */ = { + isa = PBXGroup; + children = ( + CD8AA7A921E8B9D600BA7369 /* QMUIConsole.h */, + CD8AA7AA21E8B9D600BA7369 /* QMUIConsole.m */, + CD8AA7AD21E8BF0B00BA7369 /* QMUIConsoleToolbar.h */, + CD8AA7AE21E8BF0B00BA7369 /* QMUIConsoleToolbar.m */, + CD8AA7B121E8C0F300BA7369 /* QMUIConsoleViewController.h */, + CD8AA7B221E8C0F300BA7369 /* QMUIConsoleViewController.m */, + CD8AA7C021EDE06800BA7369 /* QMUILog+QMUIConsole.h */, + CD8AA7C121EDE06800BA7369 /* QMUILog+QMUIConsole.m */, + ); + 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 */, + ); + path = QMUIKit; sourceTree = ""; }; CDB8CA751DCC870700769DF0 /* UIKitExtensions */ = { @@ -692,113 +969,411 @@ children = ( CDB8CA761DCC870700769DF0 /* CALayer+QMUI.h */, CDB8CA771DCC870700769DF0 /* CALayer+QMUI.m */, + CDD12D3A1FBB320E00114EA9 /* NSArray+QMUI.h */, + CDD12D3B1FBB320E00114EA9 /* NSArray+QMUI.m */, CDB8CA781DCC870700769DF0 /* NSAttributedString+QMUI.h */, 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 */, + CD1817E22010CC4000F8CDEC /* NSNumber+QMUI.m */, CDB8CA7A1DCC870700769DF0 /* NSObject+QMUI.h */, CDB8CA7B1DCC870700769DF0 /* NSObject+QMUI.m */, CDB8CA7C1DCC870700769DF0 /* NSParagraphStyle+QMUI.h */, 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 */, - CDB8CA801DCC870700769DF0 /* QMUIAlertController.h */, - CDB8CA811DCC870700769DF0 /* QMUIAlertController.m */, - CDB8CA821DCC870700769DF0 /* QMUIButton.h */, - CDB8CA831DCC870700769DF0 /* QMUIButton.m */, - CDB8CA841DCC870700769DF0 /* QMUICellHeightCache.h */, - CDB8CA851DCC870700769DF0 /* QMUICellHeightCache.m */, - CDB8CA861DCC870700769DF0 /* QMUICollectionViewPagingLayout.h */, - CDB8CA871DCC870700769DF0 /* QMUICollectionViewPagingLayout.m */, - CDB8CA881DCC870700769DF0 /* QMUILabel.h */, - CDB8CA891DCC870700769DF0 /* QMUILabel.m */, - CDB8CA8A1DCC870700769DF0 /* QMUISearchBar.h */, - CDB8CA8B1DCC870700769DF0 /* QMUISearchBar.m */, - CDB8CA8C1DCC870700769DF0 /* QMUISearchController.h */, - CDB8CA8D1DCC870700769DF0 /* QMUISearchController.m */, - CDB8CA8E1DCC870700769DF0 /* QMUISegmentedControl.h */, - CDB8CA8F1DCC870700769DF0 /* QMUISegmentedControl.m */, - CD53876F1EE004A300654A73 /* QMUISlider.h */, - CD5387701EE004A300654A73 /* QMUISlider.m */, - CDB8CA921DCC870700769DF0 /* QMUITableView.h */, - CDB8CA931DCC870700769DF0 /* QMUITableView.m */, - CDB8CA941DCC870700769DF0 /* QMUITableViewCell.h */, - CDB8CA951DCC870700769DF0 /* QMUITableViewCell.m */, - CDBCD53F1DFAA286009EFEF5 /* QMUITableViewProtocols.h */, - CDB8CA961DCC870700769DF0 /* QMUITextField.h */, - CDB8CA971DCC870700769DF0 /* QMUITextField.m */, - CDB8CA981DCC870700769DF0 /* QMUITextView.h */, - CDB8CA991DCC870700769DF0 /* QMUITextView.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 */, CDB8CAA51DCC870700769DF0 /* UIControl+QMUI.m */, CDB8CAA61DCC870700769DF0 /* UIFont+QMUI.h */, CDB8CAA71DCC870700769DF0 /* UIFont+QMUI.m */, + CDEA6D061F4B07E700F627AF /* UIGestureRecognizer+QMUI.h */, + CDEA6D071F4B07E700F627AF /* UIGestureRecognizer+QMUI.m */, CDB8CAA81DCC870700769DF0 /* UIImage+QMUI.h */, CDB8CAA91DCC870700769DF0 /* UIImage+QMUI.m */, CDB8CAAA1DCC870700769DF0 /* UIImageView+QMUI.h */, CDB8CAAB1DCC870700769DF0 /* UIImageView+QMUI.m */, + D0FB669621CBF00F00806600 /* UIInterface+QMUI.h */, + D0FB669721CBF00F00806600 /* UIInterface+QMUI.m */, CDB8CAAC1DCC870700769DF0 /* UILabel+QMUI.h */, CDB8CAAD1DCC870700769DF0 /* UILabel+QMUI.m */, - 36C72E0E1DE826B800F5F116 /* UINavigationBar+Transition.h */, - 36C72E0F1DE826B800F5F116 /* UINavigationBar+Transition.m */, - CDB8CAAE1DCC870700769DF0 /* UINavigationController+NavigationBarTransition.h */, - CDB8CAAF1DCC870700769DF0 /* UINavigationController+NavigationBarTransition.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 */, CDB8CAB71DCC870700769DF0 /* UITabBarItem+QMUI.m */, CDB8CAB81DCC870700769DF0 /* UITableView+QMUI.h */, 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 */, ); path = UIKitExtensions; sourceTree = ""; }; - CDB8CAC01DCC870700769DF0 /* UIMainFrame */ = { + CDC86F3C1F68D5F8000E8829 /* QMUIResources */ = { + isa = PBXGroup; + children = ( + CD0BD68A234F6C34005E47CE /* Images.xcassets */, + ); + path = QMUIResources; + sourceTree = ""; + }; + CDC86F3F1F68D5F9000E8829 /* QMUIComponents */ = { + isa = PBXGroup; + children = ( + CDC86F401F68D5F9000E8829 /* AssetLibrary */, + 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 */, + CDC86F5F1F68D5F9000E8829 /* QMUIDialogViewController.h */, + CDC86F601F68D5F9000E8829 /* QMUIDialogViewController.m */, + CDC86F811F68D5F9000E8829 /* QMUIEmotionInputManager.h */, + CDC86F821F68D5F9000E8829 /* QMUIEmotionInputManager.m */, + CDC86F611F68D5F9000E8829 /* QMUIEmotionView.h */, + CDC86F621F68D5F9000E8829 /* QMUIEmotionView.m */, + CDC86F631F68D5F9000E8829 /* QMUIEmptyView.h */, + CDC86F641F68D5F9000E8829 /* QMUIEmptyView.m */, + CDC86F651F68D5F9000E8829 /* QMUIFloatLayoutView.h */, + CDC86F661F68D5F9000E8829 /* QMUIFloatLayoutView.m */, + CDC86F671F68D5F9000E8829 /* QMUIGridView.h */, + CDC86F681F68D5F9000E8829 /* QMUIGridView.m */, + CD745E2721CA5B8E006EC132 /* QMUIImagePreviewView */, + CDC86F6D1F68D5F9000E8829 /* QMUIKeyboardManager.h */, + CDC86F6E1F68D5F9000E8829 /* QMUIKeyboardManager.m */, + CDC86F6F1F68D5F9000E8829 /* QMUILabel.h */, + CDC86F701F68D5F9000E8829 /* QMUILabel.m */, + CDFCDD9D2B43FE41005E1219 /* QMUILayouter */, + CD046C3E2018665F00092035 /* QMUILog */, + CD9D6E6C210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.h */, + CD9D6E6D210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.m */, + CDC163C4204D441000E4CC13 /* QMUILogManagerViewController.h */, + CDC163C5204D441000E4CC13 /* QMUILogManagerViewController.m */, + CDC86F711F68D5F9000E8829 /* QMUIMarqueeLabel.h */, + CDC86F721F68D5F9000E8829 /* QMUIMarqueeLabel.m */, + CDC86F731F68D5F9000E8829 /* QMUIModalPresentationViewController.h */, + CDC86F741F68D5F9000E8829 /* QMUIModalPresentationViewController.m */, + CDC86F751F68D5F9000E8829 /* QMUIMoreOperationController.h */, + CDC86F761F68D5F9000E8829 /* QMUIMoreOperationController.m */, + CD82C0A42069EB850046EED2 /* QMUIMultipleDelegates */, + CDC86F771F68D5F9000E8829 /* QMUINavigationTitleView.h */, + CDC86F781F68D5F9000E8829 /* QMUINavigationTitleView.m */, + CDC86F791F68D5F9000E8829 /* QMUIOrderedDictionary.h */, + CDC86F7A1F68D5F9000E8829 /* QMUIOrderedDictionary.m */, + CDC86F7B1F68D5F9000E8829 /* QMUIPieProgressView.h */, + CDC86F7C1F68D5F9000E8829 /* QMUIPieProgressView.m */, + CDC86F7D1F68D5F9000E8829 /* QMUIPopupContainerView.h */, + CDC86F7E1F68D5F9000E8829 /* QMUIPopupContainerView.m */, + CDD7C2B3212C4DED00D6FA1E /* QMUIPopupMenuView */, + CD349BAA2160ADBC008653D4 /* QMUIScrollAnimator */, + CDC86F831F68D5F9000E8829 /* QMUISearchBar.h */, + CDC86F841F68D5F9000E8829 /* QMUISearchBar.m */, + CDC86F851F68D5F9000E8829 /* QMUISearchController.h */, + CDC86F861F68D5F9000E8829 /* QMUISearchController.m */, + CDC86F871F68D5F9000E8829 /* QMUISegmentedControl.h */, + CDC86F881F68D5F9000E8829 /* QMUISegmentedControl.m */, + CDCD27002B8E0B6200D3500A /* QMUISheetPresentation */, + CDC86F8B1F68D5F9000E8829 /* QMUITableView.h */, + CDC86F8C1F68D5F9000E8829 /* QMUITableView.m */, + CDC86F8D1F68D5F9000E8829 /* QMUITableViewCell.h */, + CDC86F8E1F68D5F9000E8829 /* QMUITableViewCell.m */, + CD6631D91FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.h */, + CD6631DA1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.m */, + CDC86F8F1F68D5F9000E8829 /* QMUITableViewProtocols.h */, + CDC86F901F68D5F9000E8829 /* QMUITestView.h */, + CDC86F911F68D5F9000E8829 /* QMUITestView.m */, + CDC86F921F68D5F9000E8829 /* QMUITextField.h */, + CDC86F931F68D5F9000E8829 /* QMUITextField.m */, + CDC86F941F68D5F9000E8829 /* QMUITextView.h */, + CDC86F951F68D5F9000E8829 /* QMUITextView.m */, + CDAA653322BBC1070004C6BB /* QMUITheme */, + CDC86F961F68D5F9000E8829 /* QMUITips.h */, + CDC86F971F68D5F9000E8829 /* QMUITips.m */, + AA8860B82107455C005E4054 /* QMUIWeakObjectContainer.h */, + AA8860B92107455C005E4054 /* QMUIWeakObjectContainer.m */, + FECD352A22BBC93500DC69DE /* QMUIWindowSizeMonitor.h */, + FECD352922BBC93400DC69DE /* QMUIWindowSizeMonitor.m */, + CDC86F9A1F68D5F9000E8829 /* QMUIZoomImageView.h */, + CDC86F9B1F68D5F9000E8829 /* QMUIZoomImageView.m */, + CDC86F9C1F68D5F9000E8829 /* StaticTableView */, + CDC86FA31F68D5F9000E8829 /* ToastView */, + ); + path = QMUIComponents; + sourceTree = ""; + }; + CDC86F401F68D5F9000E8829 /* AssetLibrary */ = { + isa = PBXGroup; + children = ( + CDC86F411F68D5F9000E8829 /* QMUIAsset.h */, + CDC86F421F68D5F9000E8829 /* QMUIAsset.m */, + CDC86F431F68D5F9000E8829 /* QMUIAssetsGroup.h */, + CDC86F441F68D5F9000E8829 /* QMUIAssetsGroup.m */, + CDC86F451F68D5F9000E8829 /* QMUIAssetsManager.h */, + CDC86F461F68D5F9000E8829 /* QMUIAssetsManager.m */, + ); + path = AssetLibrary; + sourceTree = ""; + }; + CDC86F471F68D5F9000E8829 /* ImagePickerLibrary */ = { + isa = PBXGroup; + children = ( + CDC86F481F68D5F9000E8829 /* QMUIAlbumViewController.h */, + CDC86F491F68D5F9000E8829 /* QMUIAlbumViewController.m */, + CDC86F4A1F68D5F9000E8829 /* QMUIImagePickerCollectionViewCell.h */, + CDC86F4B1F68D5F9000E8829 /* QMUIImagePickerCollectionViewCell.m */, + CDC86F4C1F68D5F9000E8829 /* QMUIImagePickerHelper.h */, + CDC86F4D1F68D5F9000E8829 /* QMUIImagePickerHelper.m */, + CDC86F4E1F68D5F9000E8829 /* QMUIImagePickerPreviewViewController.h */, + CDC86F4F1F68D5F9000E8829 /* QMUIImagePickerPreviewViewController.m */, + CDC86F501F68D5F9000E8829 /* QMUIImagePickerViewController.h */, + CDC86F511F68D5F9000E8829 /* QMUIImagePickerViewController.m */, + ); + path = ImagePickerLibrary; + sourceTree = ""; + }; + CDC86F521F68D5F9000E8829 /* NavigationBarTransition */ = { + isa = PBXGroup; + children = ( + CDC86F531F68D5F9000E8829 /* UINavigationBar+Transition.h */, + CDC86F541F68D5F9000E8829 /* UINavigationBar+Transition.m */, + CDC86F551F68D5F9000E8829 /* UINavigationController+NavigationBarTransition.h */, + CDC86F561F68D5F9000E8829 /* UINavigationController+NavigationBarTransition.m */, + ); + path = NavigationBarTransition; + sourceTree = ""; + }; + CDC86F9C1F68D5F9000E8829 /* StaticTableView */ = { + isa = PBXGroup; + children = ( + CDC86F9D1F68D5F9000E8829 /* QMUIStaticTableViewCellData.h */, + CDC86F9E1F68D5F9000E8829 /* QMUIStaticTableViewCellData.m */, + CDC86F9F1F68D5F9000E8829 /* QMUIStaticTableViewCellDataSource.h */, + CDC86FA01F68D5F9000E8829 /* QMUIStaticTableViewCellDataSource.m */, + CDC86FA11F68D5F9000E8829 /* UITableView+QMUIStaticCell.h */, + CDC86FA21F68D5F9000E8829 /* UITableView+QMUIStaticCell.m */, + ); + path = StaticTableView; + sourceTree = ""; + }; + CDC86FA31F68D5F9000E8829 /* ToastView */ = { + isa = PBXGroup; + children = ( + CDC86FA41F68D5F9000E8829 /* QMUIToastAnimator.h */, + CDC86FA51F68D5F9000E8829 /* QMUIToastAnimator.m */, + CDC86FA61F68D5F9000E8829 /* QMUIToastBackgroundView.h */, + CDC86FA71F68D5F9000E8829 /* QMUIToastBackgroundView.m */, + CDC86FA81F68D5F9000E8829 /* QMUIToastContentView.h */, + CDC86FA91F68D5F9000E8829 /* QMUIToastContentView.m */, + CDC86FAA1F68D5F9000E8829 /* QMUIToastView.h */, + CDC86FAB1F68D5F9000E8829 /* QMUIToastView.m */, + ); + path = ToastView; + sourceTree = ""; + }; + CDC86FAC1F68D5F9000E8829 /* QMUICore */ = { + isa = PBXGroup; + children = ( + CDC86FAD1F68D5F9000E8829 /* QMUICommonDefines.h */, + CDC86FAE1F68D5F9000E8829 /* QMUIConfiguration.h */, + CDC86FAF1F68D5F9000E8829 /* QMUIConfiguration.m */, + CDC86FB01F68D5F9000E8829 /* QMUIConfigurationMacros.h */, + CDC86FB11F68D5F9000E8829 /* QMUICore.h */, + CDC86FB21F68D5F9000E8829 /* QMUIHelper.h */, + CDC86FB31F68D5F9000E8829 /* QMUIHelper.m */, + CDD7C0D3212300A000D6FA1E /* QMUIRuntime.h */, + CD979995213F934700C00FDC /* QMUIRuntime.m */, + CD19F4D721E4AB3900BD4687 /* QMUILab.h */, + ); + path = QMUICore; + sourceTree = ""; + }; + CDC86FB41F68D5F9000E8829 /* QMUIMainFrame */ = { + isa = PBXGroup; + children = ( + CDC86FB51F68D5F9000E8829 /* QMUICommonTableViewController.h */, + CDC86FB61F68D5F9000E8829 /* QMUICommonTableViewController.m */, + CDC86FB71F68D5F9000E8829 /* QMUICommonViewController.h */, + CDC86FB81F68D5F9000E8829 /* QMUICommonViewController.m */, + CDC86FB91F68D5F9000E8829 /* QMUINavigationController.h */, + CDC86FBA1F68D5F9000E8829 /* QMUINavigationController.m */, + CDC86FBB1F68D5F9000E8829 /* QMUITabBarViewController.h */, + CDC86FBC1F68D5F9000E8829 /* QMUITabBarViewController.m */, + ); + 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 = ( + 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 = ( + D021DE39205E80EB00FFA408 /* QMUICellSizeKeyCache.h */, + D021DE3A205E80EB00FFA408 /* QMUICellSizeKeyCache.m */, + D021DE35205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.h */, + D021DE36205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.m */, + ); + path = QMUICellSizeKeyCache; + sourceTree = ""; + }; + 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 = ( - CDB8CAC11DCC870700769DF0 /* QMUICommonTableViewController.h */, - CDB8CAC21DCC870700769DF0 /* QMUICommonTableViewController.m */, - CDB8CAC31DCC870700769DF0 /* QMUICommonViewController.h */, - CDB8CAC41DCC870700769DF0 /* QMUICommonViewController.m */, - CDB8CAC51DCC870700769DF0 /* QMUINavigationController.h */, - CDB8CAC61DCC870700769DF0 /* QMUINavigationController.m */, - CDB8CAC71DCC870700769DF0 /* QMUITabBarViewController.h */, - CDB8CAC81DCC870700769DF0 /* QMUITabBarViewController.m */, + D0F0C7C1246A926600927A1A /* QMUICommonDefinesTests.m */, ); - path = UIMainFrame; + path = Core; sourceTree = ""; }; - CDB8CAC91DCC870700769DF0 /* UIResources */ = { + FECD351C22BBC3BB00DC69DE /* QMUIAnimation */ = { isa = PBXGroup; children = ( - CDB8CACA1DCC870700769DF0 /* QMUI_QQEmotion.bundle */, - CDB8CACB1DCC870700769DF0 /* QMUIResources.bundle */, + FECD352022BBC3BB00DC69DE /* QMUIAnimationHelper.h */, + FECD351D22BBC3BB00DC69DE /* QMUIAnimationHelper.m */, + FECD351F22BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.h */, + FECD352122BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.m */, + FECD351E22BBC3BB00DC69DE /* QMUIEasings.h */, ); - path = UIResources; + path = QMUIAnimation; sourceTree = ""; }; /* End PBXGroup section */ @@ -808,125 +1383,214 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - CD6CC6431EF9123000602EDD /* UITableView+QMUIStaticCell.h in Headers */, - CD6CC63E1EF911E000602EDD /* QMUIStaticTableViewCellDataSource.h in Headers */, - CD6CC63B1EF911E000602EDD /* QMUIStaticTableViewCellData.h in Headers */, - CDB8CB4D1DCC870700769DF0 /* QMUIZoomImageView.h in Headers */, + 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 */, + D0FB669821CBF00F00806600 /* UIInterface+QMUI.h in Headers */, + CD745E2C21CA5B8F006EC132 /* QMUIImagePreviewView.h in Headers */, + CD745E2E21CA5B8F006EC132 /* QMUIImagePreviewViewController.h in Headers */, + CD745E3221CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.h in Headers */, + 1178D5692198258700AA30E5 /* NSURL+QMUI.h in Headers */, + CD18BC7321760D5900E2A4E6 /* QMUINavigationBarScrollingAnimator.h in Headers */, + 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 */, + CDD7C0D4212300A000D6FA1E /* QMUIRuntime.h in Headers */, + CD9D6E6E210C8DEC0004E222 /* QMUILogger+QMUIConfigurationTemplate.h in Headers */, + CD18CDFE20EE167200EED53C /* UITableViewCell+QMUI.h in Headers */, + D0D0D81A20C2B973000A33D8 /* UIBarItem+QMUIBadge.h in Headers */, + CDF2D69C207F7E3F009E04DD /* NSPointerArray+QMUI.h in Headers */, + CD43CB1B207B98B60090346B /* QMUINavigationButton.h in Headers */, + CD43CB17207B98A10090346B /* QMUIButton.h in Headers */, + CD43CB1F207B9A510090346B /* QMUIToolbarButton.h in Headers */, + CDE418FB20761A0F002ED021 /* UIBarItem+QMUI.h in Headers */, + CD82C0B3206A82520046EED2 /* NSObject+QMUIMultipleDelegates.h in Headers */, + 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 */, + CDC163C6204D441000E4CC13 /* QMUILogManagerViewController.h in Headers */, + CD046C4D2018698200092035 /* QMUILog.h in Headers */, + CD1817E52010CC4000F8CDEC /* NSNumber+QMUI.h in Headers */, + CDB8CBC51DCC870800769DF0 /* UINavigationController+QMUI.h in Headers */, + CDB8CBBD1DCC870800769DF0 /* UILabel+QMUI.h in Headers */, + CDB8CBC91DCC870800769DF0 /* UIScrollView+QMUI.h in Headers */, + CDB8CBCD1DCC870800769DF0 /* UISearchBar+QMUI.h in Headers */, + CDB8CB511DCC870700769DF0 /* CALayer+QMUI.h in Headers */, + CDB8CBD91DCC870800769DF0 /* UIView+QMUI.h in Headers */, + CDB8CBA11DCC870700769DF0 /* UIButton+QMUI.h in Headers */, + CDB8CBE11DCC870800769DF0 /* UIWindow+QMUI.h in Headers */, + CD84F31D1E52DBEA00546111 /* UITabBar+QMUI.h in Headers */, + FE1FBCAF1E8BA79000C6C01A /* UITextView+QMUI.h in Headers */, + CD6631DB1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.h in Headers */, + CDD12D3C1FBB320E00114EA9 /* NSArray+QMUI.h in Headers */, + CDC86FBD1F68D617000E8829 /* QMUIAsset.h in Headers */, + CDC86FBE1F68D617000E8829 /* QMUIAssetsGroup.h in Headers */, + CDC86FBF1F68D617000E8829 /* QMUIAssetsManager.h in Headers */, + 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 */, + CDC86FC61F68D617000E8829 /* UINavigationController+NavigationBarTransition.h in Headers */, + CDC86FC71F68D617000E8829 /* QMUIAlertController.h in Headers */, + CDC86FCA1F68D617000E8829 /* QMUICollectionViewPagingLayout.h in Headers */, + CDC86FCB1F68D617000E8829 /* QMUIDialogViewController.h in Headers */, + CDC86FCC1F68D617000E8829 /* QMUIEmotionView.h in Headers */, + CDC86FCD1F68D617000E8829 /* QMUIEmptyView.h in Headers */, + CDC86FCE1F68D617000E8829 /* QMUIFloatLayoutView.h in Headers */, + CDC86FCF1F68D617000E8829 /* QMUIGridView.h in Headers */, + CDC86FD21F68D617000E8829 /* QMUIKeyboardManager.h in Headers */, + CDC86FD31F68D617000E8829 /* QMUILabel.h in Headers */, + CDC86FD41F68D617000E8829 /* QMUIMarqueeLabel.h in Headers */, + CDC86FD51F68D617000E8829 /* QMUIModalPresentationViewController.h in Headers */, + CDC86FD61F68D617000E8829 /* QMUIMoreOperationController.h in Headers */, + CDC86FD71F68D617000E8829 /* QMUINavigationTitleView.h in Headers */, + CDC86FD81F68D617000E8829 /* QMUIOrderedDictionary.h in Headers */, + CDC86FD91F68D617000E8829 /* QMUIPieProgressView.h in Headers */, + CDC86FDA1F68D617000E8829 /* QMUIPopupContainerView.h in Headers */, + CDC86FDC1F68D617000E8829 /* QMUIEmotionInputManager.h in Headers */, + CDC86FDD1F68D617000E8829 /* QMUISearchBar.h in Headers */, + CDC86FDE1F68D617000E8829 /* QMUISearchController.h in Headers */, + CDC86FDF1F68D617000E8829 /* QMUISegmentedControl.h in Headers */, + CDC86FE11F68D617000E8829 /* QMUITableView.h in Headers */, + CDC86FE21F68D617000E8829 /* QMUITableViewCell.h in Headers */, + CDC86FE31F68D617000E8829 /* QMUITableViewProtocols.h in Headers */, + CDC86FE41F68D617000E8829 /* QMUITestView.h in Headers */, + CDC86FE51F68D617000E8829 /* QMUITextField.h in Headers */, + CDC86FE61F68D617000E8829 /* QMUITextView.h in Headers */, + CDC86FE71F68D617000E8829 /* QMUITips.h in Headers */, + CDC86FE91F68D617000E8829 /* QMUIZoomImageView.h in Headers */, + CDC86FEA1F68D617000E8829 /* QMUIStaticTableViewCellData.h in Headers */, + CDC86FEB1F68D617000E8829 /* QMUIStaticTableViewCellDataSource.h in Headers */, + CDC86FEC1F68D617000E8829 /* UITableView+QMUIStaticCell.h in Headers */, + CDC86FED1F68D617000E8829 /* QMUIToastAnimator.h in Headers */, + CDC86FEE1F68D617000E8829 /* QMUIToastBackgroundView.h in Headers */, + CDC86FEF1F68D617000E8829 /* QMUIToastContentView.h in Headers */, + CDC86FF01F68D617000E8829 /* QMUIToastView.h in Headers */, + CDC86FF11F68D617000E8829 /* QMUICommonDefines.h in Headers */, + CDC86FF21F68D617000E8829 /* QMUIConfiguration.h in Headers */, + CDC86FF31F68D617000E8829 /* QMUIConfigurationMacros.h in Headers */, + CDC86FF41F68D617000E8829 /* QMUICore.h in Headers */, + CDC86FF51F68D617000E8829 /* QMUIHelper.h in Headers */, + CDC86FF61F68D617000E8829 /* QMUICommonTableViewController.h in Headers */, + CDC86FF71F68D617000E8829 /* QMUICommonViewController.h in Headers */, + CDC86FF81F68D617000E8829 /* QMUINavigationController.h in Headers */, + CDC86FF91F68D617000E8829 /* QMUITabBarViewController.h in Headers */, + CDEA6D081F4B07E700F627AF /* UIGestureRecognizer+QMUI.h in Headers */, FE1FBCA91E8BA61300C6C01A /* UITextField+QMUI.h in Headers */, CDB8CB611DCC870700769DF0 /* NSString+QMUI.h in Headers */, - CDB8CAF91DCC870700769DF0 /* QMUIImagePickerHelper.h in Headers */, CDB8CB9D1DCC870700769DF0 /* UIBezierPath+QMUI.h in Headers */, - CDB8CAF51DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.h in Headers */, - CDB8CB0D1DCC870700769DF0 /* QMUIEmptyView.h in Headers */, CDB8CB591DCC870700769DF0 /* NSObject+QMUI.h in Headers */, CDB8CBA91DCC870700769DF0 /* UIColor+QMUI.h in Headers */, - CDB8CB791DCC870700769DF0 /* QMUISearchBar.h in Headers */, - CDB8CB691DCC870700769DF0 /* QMUIButton.h in Headers */, CDB8CBB51DCC870800769DF0 /* UIImage+QMUI.h in Headers */, CDB8CBD11DCC870800769DF0 /* UITabBarItem+QMUI.h in Headers */, CDB8CBB91DCC870800769DF0 /* UIImageView+QMUI.h in Headers */, CDB8CB551DCC870700769DF0 /* NSAttributedString+QMUI.h in Headers */, - CDB8CAE91DCC870700769DF0 /* QMUIAssetsGroup.h in Headers */, - CDB8CB3D1DCC870700769DF0 /* QMUIQQEmotionManager.h in Headers */, - CDB8CB651DCC870700769DF0 /* QMUIAlertController.h in Headers */, CDB8CB5D1DCC870700769DF0 /* NSParagraphStyle+QMUI.h in Headers */, - CDB8CB251DCC870700769DF0 /* QMUIPieProgressView.h in Headers */, - CDB8CB751DCC870700769DF0 /* QMUILabel.h in Headers */, CDB8CB991DCC870700769DF0 /* UIActivityIndicatorView+QMUI.h in Headers */, - CDB8CBED1DCC870800769DF0 /* QMUINavigationController.h in Headers */, - CDB8CBC11DCC870800769DF0 /* UINavigationController+NavigationBarTransition.h in Headers */, - CDB8CB891DCC870700769DF0 /* QMUITableView.h in Headers */, - CDB8CB091DCC870700769DF0 /* QMUIEmotionView.h in Headers */, - CDB8CAED1DCC870700769DF0 /* QMUIAssetsManager.h in Headers */, CDB8CBA51DCC870700769DF0 /* UICollectionView+QMUI.h in Headers */, - CDB8CB291DCC870700769DF0 /* QMUIPopupContainerView.h in Headers */, CDB8CBAD1DCC870800769DF0 /* UIControl+QMUI.h in Headers */, - CDB8CB951DCC870700769DF0 /* QMUITextView.h in Headers */, - CDB8CB111DCC870700769DF0 /* QMUIGridView.h in Headers */, - CDB8CAFD1DCC870700769DF0 /* QMUIImagePickerPreviewViewController.h in Headers */, CDB8CBD51DCC870800769DF0 /* UITableView+QMUI.h in Headers */, - CDB8CB011DCC870700769DF0 /* QMUIImagePickerViewController.h in Headers */, - CDB8CB711DCC870700769DF0 /* QMUICollectionViewPagingLayout.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 */, - CDB8CB1D1DCC870700769DF0 /* QMUINavigationTitleView.h in Headers */, - CDB8CB051DCC870700769DF0 /* QMUIDialogViewController.h in Headers */, - CDB8CBC51DCC870800769DF0 /* UINavigationController+QMUI.h in Headers */, - CDB8CB6D1DCC870700769DF0 /* QMUICellHeightCache.h in Headers */, - CDB8CBE51DCC870800769DF0 /* QMUICommonTableViewController.h in Headers */, - CDB8CB7D1DCC870700769DF0 /* QMUISearchController.h in Headers */, - CDB8CB151DCC870700769DF0 /* QMUIModalPresentationViewController.h in Headers */, - CDB8CBE91DCC870800769DF0 /* QMUICommonViewController.h in Headers */, - CDB8CB491DCC870700769DF0 /* QMUIVisualEffectView.h in Headers */, - CDB8CAE51DCC870700769DF0 /* QMUIAsset.h in Headers */, - CDB8CBBD1DCC870800769DF0 /* UILabel+QMUI.h in Headers */, - CDB8CBC91DCC870800769DF0 /* UIScrollView+QMUI.h in Headers */, - CDB8CBCD1DCC870800769DF0 /* UISearchBar+QMUI.h in Headers */, - CDB8CB811DCC870700769DF0 /* QMUISegmentedControl.h in Headers */, - CDB8CB8D1DCC870700769DF0 /* QMUITableViewCell.h in Headers */, - CDB8CB511DCC870700769DF0 /* CALayer+QMUI.h in Headers */, - CDB8CBD91DCC870800769DF0 /* UIView+QMUI.h in Headers */, - CDB8CB211DCC870700769DF0 /* QMUIOrderedDictionary.h in Headers */, - CDB8CB451DCC870700769DF0 /* QMUITips.h in Headers */, - CDB8CB191DCC870700769DF0 /* QMUIMoreOperationController.h in Headers */, - CDB8CBA11DCC870700769DF0 /* UIButton+QMUI.h in Headers */, - CDB8CB911DCC870700769DF0 /* QMUITextField.h in Headers */, - CD9D18FC1DD462200020F268 /* QMUIFloatLayoutView.h in Headers */, - CD78CBCF1DEE9E3500910DCE /* QMUIImagePreviewView.h in Headers */, - CD78CBCA1DEE9D6300910DCE /* QMUIImagePreviewViewController.h in Headers */, - 36C72E101DE826B800F5F116 /* UINavigationBar+Transition.h in Headers */, - CDB8CBF11DCC870800769DF0 /* QMUITabBarViewController.h in Headers */, - 16F6B63A1DFD3AF500E58171 /* QMUIToastView.h in Headers */, - 16F6B6441DFD571200E58171 /* QMUIToastBackgroundView.h in Headers */, - 16F6B64E1DFD9ADD00E58171 /* QMUIToastContentView.h in Headers */, - 16F6B6531DFEC39F00E58171 /* QMUIToastAnimator.h in Headers */, - CDB8CB411DCC870700769DF0 /* QMUITestView.h in Headers */, - CDB8CBE11DCC870800769DF0 /* UIWindow+QMUI.h in Headers */, - CDD495171E60151500829B7D /* QMUIPopupMenuView.h in Headers */, - CD84F31D1E52DBEA00546111 /* UITabBar+QMUI.h in Headers */, - CDB8CAF11DCC870700769DF0 /* QMUIAlbumViewController.h in Headers */, - CD27AB601ECC48D70000B4D0 /* QMUICore.h in Headers */, - CD27AB5B1ECC48C90000B4D0 /* QMUIHelper.h in Headers */, - CD27AB5A1ECC48C90000B4D0 /* QMUIConfigurationMacros.h in Headers */, - CD27AB561ECC48C90000B4D0 /* QMUICommonDefines.h in Headers */, - CD27AB571ECC48C90000B4D0 /* QMUIConfiguration.h in Headers */, - FE43652E1E8CC9EF00B5D34B /* QMUIKeyboardManager.h in Headers */, - FE1FBCAF1E8BA79000C6C01A /* UITextView+QMUI.h in Headers */, - CD2B01F31EDEB42D00183450 /* QMUIMarqueeLabel.h in Headers */, - CD5387711EE004A300654A73 /* QMUISlider.h in Headers */, - CDBCD5451DFAA6E0009EFEF5 /* QMUITableViewProtocols.h in Headers */, + CD9F48AA22C3985200F5C5C2 /* QMUIThemePrivate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - 16E46D491B00D8C1002B7DB8 /* QMUI */ = { + CD4EA570228C401E00A55066 /* QMUIKitTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 16E46D5F1B00D8C3002B7DB8 /* Build configuration list for PBXNativeTarget "QMUI" */; + buildConfigurationList = CD4EA57B228C401E00A55066 /* Build configuration list for PBXNativeTarget "QMUIKitTests" */; buildPhases = ( - 16E46D461B00D8C1002B7DB8 /* Sources */, - 16E46D471B00D8C1002B7DB8 /* Frameworks */, - 16E46D481B00D8C1002B7DB8 /* Copy Files */, + CD4EA56D228C401E00A55066 /* Sources */, + CD4EA56E228C401E00A55066 /* Frameworks */, + CD4EA56F228C401E00A55066 /* Resources */, ); buildRules = ( ); dependencies = ( + CD4EA578228C401E00A55066 /* PBXTargetDependency */, ); - name = QMUI; - productName = QMUI; - productReference = 16E46D4A1B00D8C2002B7DB8 /* libQMUI.a */; - productType = "com.apple.product-type.library.static"; + 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" */; buildPhases = ( + FE0AFACE1D82B9D8000D21D9 /* Headers */, FE0AFACC1D82B9D8000D21D9 /* Sources */, FE0AFACD1D82B9D8000D21D9 /* Frameworks */, - FE0AFACE1D82B9D8000D21D9 /* Headers */, FE0AFACF1D82B9D8000D21D9 /* Resources */, + CDE38A6F1F70F745001ACF2C /* Create Umbrella Header File */, ); buildRules = ( ); @@ -944,11 +1608,11 @@ isa = PBXProject; attributes = { CLASSPREFIX = QMUI; - LastUpgradeCheck = 0820; + LastUpgradeCheck = 0940; ORGANIZATIONNAME = "QMUI Team"; TargetAttributes = { - 16E46D491B00D8C1002B7DB8 = { - CreatedOnToolsVersion = 6.3; + CD4EA570228C401E00A55066 = { + CreatedOnToolsVersion = 10.2.1; }; FE0AFAD01D82B9D8000D21D9 = { CreatedOnToolsVersion = 8.0; @@ -958,124 +1622,75 @@ }; }; buildConfigurationList = CD44C1C01956D5970098D0A2 /* Build configuration list for PBXProject "qmui" */; - compatibilityVersion = "Xcode 8.0"; - developmentRegion = English; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, + "zh-Hant", + "zh-Hans", ); mainGroup = CD44C1BC1956D5970098D0A2; productRefGroup = CD44C1C61956D5970098D0A2 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 16E46D491B00D8C1002B7DB8 /* QMUI */, 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 = ( - CDB8CBF81DCC870800769DF0 /* QMUIResources.bundle in Resources */, - CDB8CBF61DCC870800769DF0 /* QMUI_QQEmotion.bundle in Resources */, + 3CB960C42BB40725005626A6 /* PrivacyInfo.xcprivacy in Resources */, + CD0BD68B234F6C34005E47CE /* Images.xcassets in Resources */, + CDFE9575293FB1DE007AE1AA /* QMUIKit.podspec in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + CDE38A6F1F70F745001ACF2C /* Create Umbrella Header File */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Create Umbrella Header File"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + 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 */ - 16E46D461B00D8C1002B7DB8 /* Sources */ = { + CD4EA56D228C401E00A55066 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FE1FBCAC1E8BA73000C6C01A /* UITextField+QMUI.m in Sources */, - CDB8CB0F1DCC870700769DF0 /* QMUIEmptyView.m in Sources */, - CDB8CB5F1DCC870700769DF0 /* NSParagraphStyle+QMUI.m in Sources */, - CD9D18FD1DD462200020F268 /* QMUIFloatLayoutView.m in Sources */, - CDB8CB1F1DCC870700769DF0 /* QMUINavigationTitleView.m in Sources */, - CDB8CAEF1DCC870700769DF0 /* QMUIAssetsManager.m in Sources */, - CDB8CBEB1DCC870800769DF0 /* QMUICommonViewController.m in Sources */, - CDB8CBB71DCC870800769DF0 /* UIImage+QMUI.m in Sources */, - CDB8CBE71DCC870800769DF0 /* QMUICommonTableViewController.m in Sources */, - CDB8CAF31DCC870700769DF0 /* QMUIAlbumViewController.m in Sources */, - CDB8CB571DCC870700769DF0 /* NSAttributedString+QMUI.m in Sources */, - CDB8CB3F1DCC870700769DF0 /* QMUIQQEmotionManager.m in Sources */, - CDB8CB731DCC870700769DF0 /* QMUICollectionViewPagingLayout.m in Sources */, - CDB8CB5B1DCC870700769DF0 /* NSObject+QMUI.m in Sources */, - CDB8CBA31DCC870700769DF0 /* UIButton+QMUI.m in Sources */, - CDB8CB6F1DCC870700769DF0 /* QMUICellHeightCache.m in Sources */, - CDB8CBD71DCC870800769DF0 /* UITableView+QMUI.m in Sources */, - CDB8CBE31DCC870800769DF0 /* UIWindow+QMUI.m in Sources */, - CD6CC6441EF9123000602EDD /* UITableView+QMUIStaticCell.m in Sources */, - CD5387721EE004A300654A73 /* QMUISlider.m in Sources */, - CDB8CBC71DCC870800769DF0 /* UINavigationController+QMUI.m in Sources */, - FE43652C1E8CC9EF00B5D34B /* QMUIKeyboardManager.m in Sources */, - CDB8CB271DCC870700769DF0 /* QMUIPieProgressView.m in Sources */, - 16F6B63B1DFD3AF500E58171 /* QMUIToastView.m in Sources */, - CDB8CB7B1DCC870700769DF0 /* QMUISearchBar.m in Sources */, - CD78CBD01DEE9E3500910DCE /* QMUIImagePreviewView.m in Sources */, - CDB8CB471DCC870700769DF0 /* QMUITips.m in Sources */, - CDB8CBCB1DCC870800769DF0 /* UIScrollView+QMUI.m in Sources */, - CD78CBCB1DEE9D6300910DCE /* QMUIImagePreviewViewController.m in Sources */, - CDB8CBF31DCC870800769DF0 /* QMUITabBarViewController.m in Sources */, - 16F6B6451DFD571200E58171 /* QMUIToastBackgroundView.m in Sources */, - CDB8CBA71DCC870700769DF0 /* UICollectionView+QMUI.m in Sources */, - CDB8CB831DCC870700769DF0 /* QMUISegmentedControl.m in Sources */, - CDB8CB071DCC870700769DF0 /* QMUIDialogViewController.m in Sources */, - CDB8CBD31DCC870800769DF0 /* UITabBarItem+QMUI.m in Sources */, - CDB8CAE71DCC870700769DF0 /* QMUIAsset.m in Sources */, - CDB8CB1B1DCC870700769DF0 /* QMUIMoreOperationController.m in Sources */, - CDB8CBBF1DCC870800769DF0 /* UILabel+QMUI.m in Sources */, - CDB8CB2B1DCC870700769DF0 /* QMUIPopupContainerView.m in Sources */, - CDB8CBB31DCC870800769DF0 /* UIFont+QMUI.m in Sources */, - CDB8CB4F1DCC870700769DF0 /* QMUIZoomImageView.m in Sources */, - CD27AB5C1ECC48C90000B4D0 /* QMUIHelper.m in Sources */, - CDB8CBCF1DCC870800769DF0 /* UISearchBar+QMUI.m in Sources */, - CD6CC63F1EF911E000602EDD /* QMUIStaticTableViewCellDataSource.m in Sources */, - CD2B01F41EDEB42D00183450 /* QMUIMarqueeLabel.m in Sources */, - CDB8CB771DCC870700769DF0 /* QMUILabel.m in Sources */, - CDB8CB4B1DCC870700769DF0 /* QMUIVisualEffectView.m in Sources */, - CDB8CB7F1DCC870700769DF0 /* QMUISearchController.m in Sources */, - CDD495181E60151500829B7D /* QMUIPopupMenuView.m in Sources */, - 16F6B6541DFEC39F00E58171 /* QMUIToastAnimator.m in Sources */, - CD84F31E1E52DBEA00546111 /* UITabBar+QMUI.m in Sources */, - CDB8CB931DCC870700769DF0 /* QMUITextField.m in Sources */, - CDB8CB431DCC870700769DF0 /* QMUITestView.m in Sources */, - CDB8CB971DCC870700769DF0 /* QMUITextView.m in Sources */, - FE1FBCB01E8BA79000C6C01A /* UITextView+QMUI.m in Sources */, - CDB8CB131DCC870700769DF0 /* QMUIGridView.m in Sources */, - CDB8CAF71DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.m in Sources */, - CDB8CB8F1DCC870700769DF0 /* QMUITableViewCell.m in Sources */, - CDB8CBEF1DCC870800769DF0 /* QMUINavigationController.m in Sources */, - CDB8CBAB1DCC870700769DF0 /* UIColor+QMUI.m in Sources */, - CDB8CB0B1DCC870700769DF0 /* QMUIEmotionView.m in Sources */, - CDB8CB631DCC870700769DF0 /* NSString+QMUI.m in Sources */, - CDB8CB9B1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.m in Sources */, - CDB8CB9F1DCC870700769DF0 /* UIBezierPath+QMUI.m in Sources */, - CDB8CBAF1DCC870800769DF0 /* UIControl+QMUI.m in Sources */, - CDB8CAEB1DCC870700769DF0 /* QMUIAssetsGroup.m in Sources */, - CD27AB581ECC48C90000B4D0 /* QMUIConfiguration.m in Sources */, - CDB8CB6B1DCC870700769DF0 /* QMUIButton.m in Sources */, - CDB8CB8B1DCC870700769DF0 /* QMUITableView.m in Sources */, - CDB8CBDF1DCC870800769DF0 /* UIViewController+QMUI.m in Sources */, - CDB8CB171DCC870700769DF0 /* QMUIModalPresentationViewController.m in Sources */, - CDB8CB031DCC870700769DF0 /* QMUIImagePickerViewController.m in Sources */, - CDB8CBC31DCC870800769DF0 /* UINavigationController+NavigationBarTransition.m in Sources */, - CDB8CB231DCC870700769DF0 /* QMUIOrderedDictionary.m in Sources */, - CDB8CB531DCC870700769DF0 /* CALayer+QMUI.m in Sources */, - CDB8CB671DCC870700769DF0 /* QMUIAlertController.m in Sources */, - CDB8CBDB1DCC870800769DF0 /* UIView+QMUI.m in Sources */, - 16F6B64F1DFD9ADD00E58171 /* QMUIToastContentView.m in Sources */, - 36C72E111DE826B800F5F116 /* UINavigationBar+Transition.m in Sources */, - CDB8CAFB1DCC870700769DF0 /* QMUIImagePickerHelper.m in Sources */, - CD6CC63C1EF911E000602EDD /* QMUIStaticTableViewCellData.m in Sources */, - CDB8CAFF1DCC870700769DF0 /* QMUIImagePickerPreviewViewController.m in Sources */, - CDB8CBBB1DCC870800769DF0 /* UIImageView+QMUI.m in Sources */, + 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; }; @@ -1083,178 +1698,213 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CDB8CB101DCC870700769DF0 /* QMUIEmptyView.m in Sources */, + CDB8CBC81DCC870800769DF0 /* UINavigationController+QMUI.m in Sources */, + CDB8CBDC1DCC870800769DF0 /* UIView+QMUI.m in Sources */, + CDD12D3D1FBB320E00114EA9 /* NSArray+QMUI.m in Sources */, + CDE418FC20761A0F002ED021 /* UIBarItem+QMUI.m in Sources */, + D0FB669921CBF00F00806600 /* UIInterface+QMUI.m in Sources */, + CD349BAE2160AF75008653D4 /* QMUIScrollAnimator.m in Sources */, CDB8CB601DCC870700769DF0 /* NSParagraphStyle+QMUI.m in Sources */, - CD9D18FE1DD462200020F268 /* QMUIFloatLayoutView.m in Sources */, - CDB8CB201DCC870700769DF0 /* QMUINavigationTitleView.m in Sources */, - CDB8CAF01DCC870700769DF0 /* QMUIAssetsManager.m in Sources */, - CDB8CBEC1DCC870800769DF0 /* QMUICommonViewController.m in Sources */, CDB8CBB81DCC870800769DF0 /* UIImage+QMUI.m in Sources */, - CDB8CBE81DCC870800769DF0 /* QMUICommonTableViewController.m in Sources */, - CDB8CAF41DCC870700769DF0 /* QMUIAlbumViewController.m in Sources */, + CD81252B2A69CB5900132EC7 /* NSDictionary+QMUI.m in Sources */, + CD72E7C82B44AF2F00AC528A /* QMUILayouterLinearHorizontal.m in Sources */, CDB8CB581DCC870700769DF0 /* NSAttributedString+QMUI.m in Sources */, - CDB8CB401DCC870700769DF0 /* QMUIQQEmotionManager.m in Sources */, - CDB8CB741DCC870700769DF0 /* QMUICollectionViewPagingLayout.m in Sources */, CDB8CB5C1DCC870700769DF0 /* NSObject+QMUI.m in Sources */, CDB8CBA41DCC870700769DF0 /* UIButton+QMUI.m in Sources */, - 16F6B6461DFD571300E58171 /* QMUIToastBackgroundView.m in Sources */, - CDB8CB701DCC870700769DF0 /* QMUICellHeightCache.m in Sources */, CDB8CBD81DCC870800769DF0 /* UITableView+QMUI.m in Sources */, + CD82C0B0206A2C3D0046EED2 /* QMUIMultipleDelegates.m in Sources */, + CDD7C2C0212C528500D6FA1E /* QMUIPopupMenuView.m in Sources */, + CD18CDFF20EE167200EED53C /* UITableViewCell+QMUI.m in Sources */, CDB8CBE41DCC870800769DF0 /* UIWindow+QMUI.m in Sources */, - CD6CC6451EF9123000602EDD /* UITableView+QMUIStaticCell.m in Sources */, - CD5387731EE004A300654A73 /* QMUISlider.m in Sources */, - CDB8CBC81DCC870800769DF0 /* UINavigationController+QMUI.m in Sources */, - FE43652D1E8CC9EF00B5D34B /* QMUIKeyboardManager.m in Sources */, - CDB8CB281DCC870700769DF0 /* QMUIPieProgressView.m in Sources */, - CDB8CB7C1DCC870700769DF0 /* QMUISearchBar.m in Sources */, - CD78CBD11DEE9E3500910DCE /* QMUIImagePreviewView.m in Sources */, - CDB8CB481DCC870700769DF0 /* QMUITips.m in Sources */, + CD43CB18207B98A10090346B /* QMUIButton.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 */, - CD78CBCC1DEE9D6300910DCE /* QMUIImagePreviewViewController.m in Sources */, - CDB8CBF41DCC870800769DF0 /* QMUITabBarViewController.m in Sources */, CDB8CBA81DCC870700769DF0 /* UICollectionView+QMUI.m in Sources */, - CDB8CB841DCC870700769DF0 /* QMUISegmentedControl.m in Sources */, - CDB8CB081DCC870700769DF0 /* QMUIDialogViewController.m in Sources */, + CD979996213F934700C00FDC /* QMUIRuntime.m in Sources */, + CD70C43B276340B300D212F5 /* UISlider+QMUI.m in Sources */, CDB8CBD41DCC870800769DF0 /* UITabBarItem+QMUI.m in Sources */, - CDB8CAE81DCC870700769DF0 /* QMUIAsset.m in Sources */, - CDB8CB1C1DCC870700769DF0 /* QMUIMoreOperationController.m in Sources */, - 16F6B6501DFD9ADD00E58171 /* QMUIToastContentView.m in Sources */, + CD82C0B4206A82520046EED2 /* NSObject+QMUIMultipleDelegates.m in Sources */, CDB8CBC01DCC870800769DF0 /* UILabel+QMUI.m in Sources */, - CDB8CB2C1DCC870700769DF0 /* QMUIPopupContainerView.m in Sources */, + CDEA6D0A1F4B07E700F627AF /* UIGestureRecognizer+QMUI.m in Sources */, CDB8CBB41DCC870800769DF0 /* UIFont+QMUI.m in Sources */, - CDB8CB501DCC870700769DF0 /* QMUIZoomImageView.m in Sources */, CDB8CBD01DCC870800769DF0 /* UISearchBar+QMUI.m in Sources */, - CD27AB5D1ECC48C90000B4D0 /* QMUIHelper.m in Sources */, - CDB8CB781DCC870700769DF0 /* QMUILabel.m in Sources */, - CD6CC6401EF911E000602EDD /* QMUIStaticTableViewCellDataSource.m in Sources */, - CD2B01F51EDEB42D00183450 /* QMUIMarqueeLabel.m in Sources */, - CDB8CB4C1DCC870700769DF0 /* QMUIVisualEffectView.m in Sources */, + CD4002212C1F81CE003D2127 /* QMUIPopupMenuItemView.m in Sources */, CD84F31F1E52DBEA00546111 /* UITabBar+QMUI.m in Sources */, - CDB8CB801DCC870700769DF0 /* QMUISearchController.m in Sources */, - CDB8CB941DCC870700769DF0 /* QMUITextField.m in Sources */, - CDB8CB441DCC870700769DF0 /* QMUITestView.m in Sources */, - CDB8CB981DCC870700769DF0 /* QMUITextView.m in Sources */, - CDB8CB141DCC870700769DF0 /* QMUIGridView.m in Sources */, - CDD495191E60151500829B7D /* QMUIPopupMenuView.m in Sources */, - CDB8CAF81DCC870700769DF0 /* QMUIImagePickerCollectionViewCell.m in Sources */, - CDB8CB901DCC870700769DF0 /* QMUITableViewCell.m in Sources */, FE1FBCB11E8BA79000C6C01A /* UITextView+QMUI.m in Sources */, - CDB8CBF01DCC870800769DF0 /* QMUINavigationController.m in Sources */, + CD745E3321CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.m in Sources */, CDB8CBAC1DCC870800769DF0 /* UIColor+QMUI.m in Sources */, - CDB8CB0C1DCC870700769DF0 /* QMUIEmotionView.m in Sources */, 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 */, - CDB8CAEC1DCC870700769DF0 /* QMUIAssetsGroup.m in Sources */, - CDB8CB6C1DCC870700769DF0 /* QMUIButton.m in Sources */, - CDB8CB8C1DCC870700769DF0 /* QMUITableView.m in Sources */, - CD27AB591ECC48C90000B4D0 /* QMUIConfiguration.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 */, - CDB8CB181DCC870700769DF0 /* QMUIModalPresentationViewController.m in Sources */, - CDB8CB041DCC870700769DF0 /* QMUIImagePickerViewController.m in Sources */, - CDB8CBC41DCC870800769DF0 /* UINavigationController+NavigationBarTransition.m in Sources */, - CDB8CB241DCC870700769DF0 /* QMUIOrderedDictionary.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 */, + CD8AA7AC21E8B9D600BA7369 /* QMUIConsole.m in Sources */, CDB8CB541DCC870700769DF0 /* CALayer+QMUI.m in Sources */, - CDB8CB681DCC870700769DF0 /* QMUIAlertController.m in Sources */, - 16F6B6551DFEC39F00E58171 /* QMUIToastAnimator.m in Sources */, - CDB8CBDC1DCC870800769DF0 /* UIView+QMUI.m in Sources */, - 36C72E121DE826B800F5F116 /* UINavigationBar+Transition.m in Sources */, - 16F6B63C1DFD3AF500E58171 /* QMUIToastView.m in Sources */, - CDB8CAFC1DCC870700769DF0 /* QMUIImagePickerHelper.m in Sources */, - CD6CC63D1EF911E000602EDD /* QMUIStaticTableViewCellData.m in Sources */, - CDB8CB001DCC870700769DF0 /* QMUIImagePickerPreviewViewController.m in Sources */, CDB8CBBC1DCC870800769DF0 /* UIImageView+QMUI.m in Sources */, + 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 */, + CDC870081F68D63B000E8829 /* QMUIDialogViewController.m in Sources */, + CD8AA7B021E8BF0B00BA7369 /* QMUIConsoleToolbar.m in Sources */, + CD18BC7421760D5900E2A4E6 /* QMUINavigationBarScrollingAnimator.m in Sources */, + 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 */, + CDC870101F68D63B000E8829 /* QMUILabel.m in Sources */, + CDC870111F68D63B000E8829 /* QMUIMarqueeLabel.m in Sources */, + CDF2D69D207F7E3F009E04DD /* NSPointerArray+QMUI.m in Sources */, + CD6631DC1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.m in Sources */, + CDC870121F68D63B000E8829 /* QMUIModalPresentationViewController.m in Sources */, + 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 */, + 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 */, + CD6BE1572058C73600BE093E /* UITableView+QMUICellHeightKeyCache.m in Sources */, + CDC8702B1F68D63B000E8829 /* QMUIToastContentView.m in Sources */, + 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 */, + D03102B624A8CB410095C232 /* UIView+QMUIBorder.m in Sources */, + CD513E32283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXVariantGroup section */ - CD44C1D11956D5970098D0A2 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - CD44C1D21956D5970098D0A2 /* en */, - ); - name = InfoPlist.strings; - sourceTree = ""; +/* Begin PBXTargetDependency section */ + CD4EA578228C401E00A55066 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FE0AFAD01D82B9D8000D21D9 /* QMUIKit */; + targetProxy = CD4EA577228C401E00A55066 /* PBXContainerItemProxy */; }; -/* End PBXVariantGroup section */ +/* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 16E46D5B1B00D8C3002B7DB8 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_WARN_UNREACHABLE_CODE = YES; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DYLIB_CURRENT_VERSION = 1; - ENABLE_STRICT_OBJC_MSGSEND = YES; - FRAMEWORK_VERSION = 1; - GCC_NO_COMMON_BLOCKS = YES; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "qmui/qmui-Prefix.pch"; - GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; - IPHONEOS_DEPLOYMENT_TARGET = 6.0; - MTL_ENABLE_DEBUG_INFO = YES; - OTHER_LDFLAGS = ( - "-ObjC", - "-all_load", - ); - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 16E46D5C1B00D8C3002B7DB8 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_WARN_UNREACHABLE_CODE = YES; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DYLIB_CURRENT_VERSION = 1; - ENABLE_STRICT_OBJC_MSGSEND = YES; - FRAMEWORK_VERSION = 1; - GCC_NO_COMMON_BLOCKS = YES; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = "qmui/qmui-Prefix.pch"; - GCC_PREPROCESSOR_DEFINITIONS = ""; - IPHONEOS_DEPLOYMENT_TARGET = 6.0; - MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = ( - "-ObjC", - "-all_load", - ); - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; CD44C1EF1956D5970098D0A2 /* 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*]" = ""; COPY_PHASE_STRIP = NO; + ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -1273,7 +1923,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; @@ -1283,23 +1933,33 @@ 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*]" = ""; COPY_PHASE_STRIP = YES; + ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -1311,12 +1971,73 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.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 = { @@ -1334,14 +2055,21 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; + GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; INFOPLIST_FILE = QMUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + 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)"; @@ -1349,6 +2077,7 @@ TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + WARNING_CFLAGS = ""; }; name = Debug; }; @@ -1370,14 +2099,21 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = ""; + GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; INFOPLIST_FILE = QMUIKit/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + 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)"; @@ -1385,26 +2121,27 @@ TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; + WARNING_CFLAGS = ""; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 16E46D5F1B00D8C3002B7DB8 /* Build configuration list for PBXNativeTarget "QMUI" */ = { + CD44C1C01956D5970098D0A2 /* Build configuration list for PBXProject "qmui" */ = { isa = XCConfigurationList; buildConfigurations = ( - 16E46D5B1B00D8C3002B7DB8 /* Debug */, - 16E46D5C1B00D8C3002B7DB8 /* Release */, + CD44C1EF1956D5970098D0A2 /* Debug */, + CD44C1F01956D5970098D0A2 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - CD44C1C01956D5970098D0A2 /* Build configuration list for PBXProject "qmui" */ = { + CD4EA57B228C401E00A55066 /* Build configuration list for PBXNativeTarget "QMUIKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - CD44C1EF1956D5970098D0A2 /* Debug */, - CD44C1F01956D5970098D0A2 /* Release */, + CD4EA579228C401E00A55066 /* Debug */, + CD4EA57A228C401E00A55066 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/QMUI/qmui.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/QMUI/qmui.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/QMUI/qmui.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/QMUI/qmui.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/QMUI/qmui.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/QMUI/qmui.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/QMUI/qmui.xcodeproj/project.xcworkspace/xcuserdata/molice.xcuserdatad/UserInterfaceState.xcuserstate b/QMUI/qmui.xcodeproj/project.xcworkspace/xcuserdata/molice.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..b98ecfc4 Binary files /dev/null and b/QMUI/qmui.xcodeproj/project.xcworkspace/xcuserdata/molice.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/QMUI/qmui.xcodeproj/xcshareddata/xcschemes/QMUI.xcscheme b/QMUI/qmui.xcodeproj/xcshareddata/xcschemes/QMUI.xcscheme deleted file mode 100644 index 1b5b5498..00000000 --- a/QMUI/qmui.xcodeproj/xcshareddata/xcschemes/QMUI.xcscheme +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/QMUI/qmui.xcodeproj/xcshareddata/xcschemes/QMUIKit.xcscheme b/QMUI/qmui.xcodeproj/xcshareddata/xcschemes/QMUIKit.xcscheme index 16b6eb39..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/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.h b/QMUI/qmui/QMUIAppDelegate.h deleted file mode 100644 index 976ac649..00000000 --- a/QMUI/qmui/QMUIAppDelegate.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// QMUIAppDelegate.h -// qmui -// -// Created by MoLice on 14-6-22. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import - -@interface QMUIAppDelegate : UIResponder - -@property (strong, nonatomic) UIWindow *window; - -@end diff --git a/QMUI/qmui/QMUIAppDelegate.m b/QMUI/qmui/QMUIAppDelegate.m deleted file mode 100644 index 19afe713..00000000 --- a/QMUI/qmui/QMUIAppDelegate.m +++ /dev/null @@ -1,44 +0,0 @@ -// -// QMUIAppDelegate.m -// qmui -// -// Created by MoLice on 14-6-22. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#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.h b/QMUI/qmui/QMUIViewController.h deleted file mode 100644 index 4ec7db36..00000000 --- a/QMUI/qmui/QMUIViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QMUIViewController.h -// qmui -// -// Created by ZhoonChen on 15/4/13. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#import "QMUICommonViewController.h" - -@interface QMUIViewController : QMUICommonViewController - -@end diff --git a/QMUI/qmui/QMUIViewController.m b/QMUI/qmui/QMUIViewController.m deleted file mode 100644 index 63442c82..00000000 --- a/QMUI/qmui/QMUIViewController.m +++ /dev/null @@ -1,50 +0,0 @@ -// -// QMUIViewController.m -// qmui -// -// Created by ZhoonChen on 15/4/13. -// Copyright (c) 2015年 QMUI Team. All rights reserved. -// - -#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项目进行查看。\n\n\n如有问题请联系RTX:\nzhoonchen\nmolicechen"; - [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, NavigationContentTop + 50, homeLabelLimitWidth, homeLabelSize.height); -} - -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; - self.title = @"QMUI"; -} - -@end diff --git a/QMUI/qmui/main.m b/QMUI/qmui/main.m deleted file mode 100644 index dd00939d..00000000 --- a/QMUI/qmui/main.m +++ /dev/null @@ -1,17 +0,0 @@ -// -// main.m -// qmui -// -// Created by MoLice on 14-6-22. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import -#import "QMUIAppDelegate.h" - -int main(int argc, char * argv[]) -{ - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([QMUIAppDelegate class])); - } -} 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/qmui/qmui-Prefix.pch b/QMUI/qmui/qmui-Prefix.pch deleted file mode 100644 index 8087dd7b..00000000 --- a/QMUI/qmui/qmui-Prefix.pch +++ /dev/null @@ -1,20 +0,0 @@ -// -// Prefix header -// -// The contents of this file are implicitly included at the beginning of every source file. -// - -#import - -#ifndef __IPHONE_3_0 -#warning "This project uses features only available in iOS SDK 3.0 and later." -#endif - -#ifdef __OBJC__ - - #import - #import - - #import - -#endif diff --git a/QMUI/umbrellaHeaderFileCreator.py b/QMUI/umbrellaHeaderFileCreator.py new file mode 100644 index 00000000..66df3f7a --- /dev/null +++ b/QMUI/umbrellaHeaderFileCreator.py @@ -0,0 +1,78 @@ +#!/usr/bin/python +#coding:utf-8 + +import os +try: + import xml.etree.cElementTree as ET +except ImportError: + import xml.etree.ElementTree as ET + +# 从 Info.plist 中读取 QMUIKit 的版本号,将其定义为一个 static const 常量以便代码里获取 +infoFilePath = str(os.getenv('SRCROOT')) + '/QMUIKit/Info.plist' +infoTree = ET.parse(infoFilePath) +infoDictList = list(infoTree.find('dict')) +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) +umbrellaHeaderFileName = 'QMUIKit.h' +umbrellaHeaderFilePath = str(os.getenv('SRCROOT')) + '/QMUIKit/' + umbrellaHeaderFileName +print('umbrella creator: umbrellaHeaderFilePath = ' + umbrellaHeaderFilePath) +umbrellaFileContent = '''/** + * 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. + */ + +/// Automatically created by script in Build Phases + +#import + +#ifndef QMUIKit_h +#define QMUIKit_h + +static NSString * const QMUI_VERSION = @"%s"; + +''' % (versionString) + +onlyfiles = [ f for f in os.listdir(publicHeaderFilePath) if os.path.isfile(os.path.join(publicHeaderFilePath, f))] +onlyfiles.sort() +for filename in onlyfiles: + if filename != umbrellaHeaderFileName: + umbrellaFileContent += '''#if __has_include("%s") +#import "%s" +#endif + +''' % (filename, filename) + +umbrellaFileContent += '#endif /* QMUIKit_h */' + +umbrellaFileContent = umbrellaFileContent.strip() + +f = open(umbrellaHeaderFilePath, 'r+') +f.seek(0) +oldFileContent = f.read().strip() +if oldFileContent == umbrellaFileContent: + print('umbrella creator: ' + umbrellaHeaderFileName + '的内容没有变化,不需要重写') +else: + print('umbrella creator: ' + umbrellaHeaderFileName + '的内容发生变化,开始重写') + print('umbrella creator: umbrellaFileContent = ' + umbrellaFileContent) + + f.seek(0) + f.write(umbrellaFileContent) + f.truncate() + +f.close() + 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 b0f14f3e..13bf832d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,18 @@ # QMUIDemo_iOS -Sample Code for QMUI iOS https://github.com/QMUI/QMUI_iOS +Sample Code for QMUI iOS https://github.com/Tencent/QMUI_iOS -支持 iOS 版本:iOS 8.0+ +支持 iOS 版本:iOS 13.0+ -建议用 Xcode 8.0 以上版本打开,因为项目的 `Project Format` 为 `Xcode 8.0-compatible`,如需在旧版 Xcode 中运行,需要先将 `Project Format` 改为更旧的选项。 +## Sketch Files + +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 3aa6c2c6..63ee53c0 100644 --- a/qmuidemo.xcodeproj/project.pbxproj +++ b/qmuidemo.xcodeproj/project.pbxproj @@ -3,10 +3,11 @@ archiveVersion = 1; classes = { }; - objectVersion = 48; + objectVersion = 60; objects = { /* 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 */; }; @@ -16,26 +17,51 @@ 1679E32E1DFFC0AD0072B8A1 /* QDCustomToastAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = 1679E32D1DFFC0AD0072B8A1 /* QDCustomToastAnimator.m */; }; 1679E3311DFFC4610072B8A1 /* QDCustomToastContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1679E3301DFFC4610072B8A1 /* QDCustomToastContentView.m */; }; 16F42C121B1C60A70038B28F /* image0@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 16F42C111B1C60A70038B28F /* image0@2x.png */; }; - CD1229A71DF677AC003B9649 /* QDImagePreviewExampleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD1229A61DF677AC003B9649 /* QDImagePreviewExampleViewController.m */; }; + 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 */; }; CD1229AF1DF678FB003B9649 /* QDImagePreviewViewController1.m in Sources */ = {isa = PBXBuildFile; fileRef = CD1229AE1DF678FB003B9649 /* QDImagePreviewViewController1.m */; }; CD1229B21DF67922003B9649 /* QDImagePreviewViewController2.m in Sources */ = {isa = PBXBuildFile; fileRef = CD1229B11DF67922003B9649 /* QDImagePreviewViewController2.m */; }; - CD1A8A721EC3110300B81693 /* QDThemeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD1A8A711EC3110300B81693 /* QDThemeViewController.m */; }; + CD18BD8221870EFD00E2A4E6 /* QDNavigationBarScrollingAnimatorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD18BD8121870EFD00E2A4E6 /* QDNavigationBarScrollingAnimatorViewController.m */; }; + CD18BD8521871DCE00E2A4E6 /* QDNavigationBarScrollingSnapAnimatorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD18BD8421871DCE00E2A4E6 /* QDNavigationBarScrollingSnapAnimatorViewController.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 */; }; + CD3CDD4F1F39E977008529DA /* QDUIViewBorderViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD3CDD4E1F39E977008529DA /* QDUIViewBorderViewController.m */; }; + CD3CDD521F39F077008529DA /* QDUIViewDebugViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD3CDD511F39F077008529DA /* QDUIViewDebugViewController.m */; }; + CD3CDD551F3AAD42008529DA /* QDUIViewLayoutViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD3CDD541F3AAD42008529DA /* QDUIViewLayoutViewController.m */; }; CD4013061EFCFA300022FE2A /* QDOrientationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4013051EFCFA300022FE2A /* QDOrientationViewController.m */; }; CD4421CF1E84C74C001B8C2A /* QDObjectViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4421CE1E84C74C001B8C2A /* QDObjectViewController.m */; }; CD4421D21E85071A001B8C2A /* QDObjectMethodsListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4421D11E85071A001B8C2A /* QDObjectMethodsListViewController.m */; }; + CD4A05F220288980000159E9 /* QDNavigationTransitionViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CD4A05F120288980000159E9 /* QDNavigationTransitionViewController.m */; }; 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 */; }; - CD75935E1EF7A31F004B5819 /* QMUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD75935B1EF7A2EC004B5819 /* QMUIKit.framework */; }; - CD75935F1EF7A340004B5819 /* QMUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CD75935B1EF7A2EC004B5819 /* QMUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 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 */; }; + 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 */; }; + 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 */; }; @@ -64,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 */; }; @@ -76,29 +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 */; }; - CDB8CA041DCC815A00769DF0 /* QDTableViewCellDynamicHeightViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8C9A71DCC815A00769DF0 /* QDTableViewCellDynamicHeightViewController.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 */; }; @@ -109,21 +129,53 @@ CDE50AD91DAA6ED9000D5414 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDE50AD81DAA6ED9000D5414 /* Security.framework */; }; CDE50ADB1DAA6EE5000D5414 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDE50ADA1DAA6EE5000D5414 /* SystemConfiguration.framework */; }; CDE50ADD1DAA6EEA000D5414 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDE50ADC1DAA6EEA000D5414 /* CFNetwork.framework */; }; - CDE50ADF1DAA7059000D5414 /* libstdc++.6.0.9.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CDE50ADE1DAA7059000D5414 /* libstdc++.6.0.9.tbd */; }; + 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 */; }; + 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 */ - CD7593581EF7A2EC004B5819 /* PBXContainerItemProxy */ = { + 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 = 16E46D4A1B00D8C2002B7DB8; - remoteInfo = QMUI; + remoteGlobalIDString = CD4EA571228C401E00A55066; + remoteInfo = QMUIKitTests; }; CD75935A1EF7A2EC004B5819 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -132,32 +184,52 @@ remoteGlobalIDString = FE0AFAD11D82B9D8000D21D9; remoteInfo = QMUIKit; }; - CD75935C1EF7A304004B5819 /* PBXContainerItemProxy */ = { + CDE85CB71F95E21500E622E8 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = CD7593531EF7A2EC004B5819 /* qmui.xcodeproj */; proxyType = 1; remoteGlobalIDString = FE0AFAD01D82B9D8000D21D9; remoteInfo = QMUIKit; }; + CDFD573F26DE132800603D1E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 165F42E81ADB60180057EF6A /* Project object */; + proxyType = 1; + remoteGlobalIDString = CDFD573826DE132700603D1E; + remoteInfo = QMUIKeyboard; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - CDD0D8741E0D2BC600A38B96 /* Embed Frameworks */ = { + CDE85CB91F95E21500E622E8 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - CD75935F1EF7A340004B5819 /* QMUIKit.framework in Embed Frameworks */, + 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; }; - 165F42F41ADB60180057EF6A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 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 = ""; }; 165F42F81ADB60180057EF6A /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -172,42 +244,99 @@ 1679E32F1DFFC4610072B8A1 /* QDCustomToastContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDCustomToastContentView.h; sourceTree = ""; }; 1679E3301DFFC4610072B8A1 /* QDCustomToastContentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDCustomToastContentView.m; sourceTree = ""; }; 16F42C111B1C60A70038B28F /* image0@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "image0@2x.png"; 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 = ""; }; + 4CD323892109A55E007033C9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/LaunchScreen.strings; sourceTree = ""; }; + 4CD3238A2109A564007033C9 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 4CD3238C2109A7AE007033C9 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "zh-Hans"; path = "zh-Hans.lproj/Info.plist"; sourceTree = ""; }; + 4CD3238E2109A810007033C9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Base; path = Base.lproj/Info.plist; sourceTree = ""; }; + 4CD323902109A84D007033C9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = en.lproj/Info.plist; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; CD1229B11DF67922003B9649 /* QDImagePreviewViewController2.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDImagePreviewViewController2.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 = ""; }; + CD18BD8021870EFD00E2A4E6 /* QDNavigationBarScrollingAnimatorViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDNavigationBarScrollingAnimatorViewController.h; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; CD1A8AA31EC4045500B81693 /* QMUIConfigurationTemplatePinkRose.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIConfigurationTemplatePinkRose.m; sourceTree = ""; }; + CD2FEE5A2260F6AC00298BF5 /* QDNavigationBarMaxYViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDNavigationBarMaxYViewController.h; sourceTree = ""; }; + CD2FEE5B2260F6AC00298BF5 /* QDNavigationBarMaxYViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDNavigationBarMaxYViewController.m; sourceTree = ""; }; + CD3CDD4D1F39E977008529DA /* QDUIViewBorderViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDUIViewBorderViewController.h; sourceTree = ""; }; + CD3CDD4E1F39E977008529DA /* QDUIViewBorderViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDUIViewBorderViewController.m; sourceTree = ""; }; + CD3CDD501F39F077008529DA /* QDUIViewDebugViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDUIViewDebugViewController.h; sourceTree = ""; }; + CD3CDD511F39F077008529DA /* QDUIViewDebugViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDUIViewDebugViewController.m; sourceTree = ""; }; + CD3CDD531F3AAD42008529DA /* QDUIViewLayoutViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDUIViewLayoutViewController.h; sourceTree = ""; }; + CD3CDD541F3AAD42008529DA /* QDUIViewLayoutViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDUIViewLayoutViewController.m; sourceTree = ""; }; CD4013041EFCFA300022FE2A /* QDOrientationViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDOrientationViewController.h; sourceTree = ""; }; CD4013051EFCFA300022FE2A /* QDOrientationViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDOrientationViewController.m; sourceTree = ""; }; CD4421CD1E84C74C001B8C2A /* QDObjectViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDObjectViewController.h; sourceTree = ""; }; CD4421CE1E84C74C001B8C2A /* QDObjectViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDObjectViewController.m; sourceTree = ""; }; CD4421D01E85071A001B8C2A /* QDObjectMethodsListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDObjectMethodsListViewController.h; sourceTree = ""; }; CD4421D11E85071A001B8C2A /* QDObjectMethodsListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDObjectMethodsListViewController.m; sourceTree = ""; }; + CD4A05F020288980000159E9 /* QDNavigationTransitionViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QDNavigationTransitionViewController.h; sourceTree = ""; }; + CD4A05F120288980000159E9 /* QDNavigationTransitionViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDNavigationTransitionViewController.m; sourceTree = ""; }; CD4D339C1F010FD100FB81B0 /* QDStaticTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDStaticTableViewController.h; sourceTree = ""; }; CD4D339D1F010FD100FB81B0 /* QDStaticTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDStaticTableViewController.m; sourceTree = ""; }; + 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 = ""; }; CD6CC62C1EF7A6EA00602EDD /* QDTableViewCellAccessoryTypeViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QDTableViewCellAccessoryTypeViewController.m; sourceTree = ""; }; - CD7593501EF7A263004B5819 /* RevealServer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RevealServer.framework; path = "../../../../../../../Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries/RevealServer.framework"; sourceTree = ""; }; CD7593531EF7A2EC004B5819 /* qmui.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = qmui.xcodeproj; path = QMUI/qmui.xcodeproj; sourceTree = SOURCE_ROOT; }; 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; }; + 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 = ""; }; + 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 = ""; }; CDA1CC941EC1BF5200AB8A0F /* QMUIConfigurationTemplateGrapefruit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIConfigurationTemplateGrapefruit.m; sourceTree = ""; }; CDA1CC971EC1C34300AB8A0F /* QDThemeManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDThemeManager.h; sourceTree = ""; }; CDA1CC981EC1C34300AB8A0F /* QDThemeManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDThemeManager.m; sourceTree = ""; }; 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 = ""; }; @@ -264,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 = ""; }; @@ -288,20 +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 = ""; }; - CDB8C9A61DCC815A00769DF0 /* QDTableViewCellDynamicHeightViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QDTableViewCellDynamicHeightViewController.h; sourceTree = ""; }; - CDB8C9A71DCC815A00769DF0 /* QDTableViewCellDynamicHeightViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QDTableViewCellDynamicHeightViewController.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 = ""; }; @@ -310,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 = ""; }; @@ -320,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 = ""; }; @@ -332,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 = ""; }; @@ -349,10 +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 = ""; }; + 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 = ""; }; @@ -364,18 +532,32 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CDE85CB51F95E21500E622E8 /* QMUIKit.framework in Frameworks */, CD7593651EF7A39A004B5819 /* MediaPlayer.framework in Frameworks */, CD7593631EF7A388004B5819 /* Photos.framework in Frameworks */, CD7593621EF7A380004B5819 /* libxml2.tbd in Frameworks */, - CD75935E1EF7A31F004B5819 /* QMUIKit.framework in Frameworks */, - CDE50ADF1DAA7059000D5414 /* libstdc++.6.0.9.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 */ @@ -383,6 +565,8 @@ isa = PBXGroup; children = ( 165F42F21ADB60180057EF6A /* qmuidemo */, + CD4EA596228D87B000A55066 /* QMUIDemoUITests */, + CDFD573A26DE132700603D1E /* QMUIKeyboard */, 165F42F11ADB60180057EF6A /* Products */, CDE50AD11DAA6E6F000D5414 /* Frameworks */, ); @@ -392,6 +576,8 @@ isa = PBXGroup; children = ( 165F42F01ADB60180057EF6A /* qmuidemo.app */, + CD4EA595228D87B000A55066 /* QMUIDemoUITests.xctest */, + CDFD573926DE132700603D1E /* QMUIKeyboard.appex */, ); name = Products; sourceTree = ""; @@ -399,9 +585,11 @@ 165F42F21ADB60180057EF6A /* qmuidemo */ = { isa = PBXGroup; children = ( + CDFE9573293FA58A007AE1AA /* qmuidemo.entitlements */, 165F42F71ADB60180057EF6A /* AppDelegate.h */, 165F42F81ADB60180057EF6A /* AppDelegate.m */, 165F43001ADB60180057EF6A /* Images.xcassets */, + 4CD323972109B362007033C9 /* Localizable.strings */, 165F43021ADB60180057EF6A /* LaunchScreen.xib */, CD75934F1EF7A25C004B5819 /* Frameworks */, 165F431A1ADB62C00057EF6A /* Resources */, @@ -414,7 +602,7 @@ 165F42F31ADB60180057EF6A /* Supporting Files */ = { isa = PBXGroup; children = ( - 165F42F41ADB60180057EF6A /* Info.plist */, + 4CD3238D2109A7AE007033C9 /* Info.plist */, 165F42F51ADB60180057EF6A /* main.m */, 165F43DB1ADB74E90057EF6A /* PrefixHeader.pch */, ); @@ -441,6 +629,7 @@ 165F43551ADB71E90057EF6A /* images */ = { isa = PBXGroup; children = ( + CD4EA4B32275D58400A55066 /* animatedImage.gif */, 16F42C111B1C60A70038B28F /* image0@2x.png */, 165F43561ADB71FF0057EF6A /* image1@2x.png */, CDD0D81A1E0A88B600A38B96 /* image2@2x.png */, @@ -452,11 +641,30 @@ 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 = ( CD7593531EF7A2EC004B5819 /* qmui.xcodeproj */, - CD7593501EF7A263004B5819 /* RevealServer.framework */, ); path = Frameworks; sourceTree = ""; @@ -464,17 +672,27 @@ CD7593541EF7A2EC004B5819 /* Products */ = { isa = PBXGroup; children = ( - CD7593591EF7A2EC004B5819 /* libQMUI.a */, 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; @@ -488,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 */, @@ -515,6 +735,8 @@ CDB8C9451DCC815A00769DF0 /* QDNavigationController.m */, CDB8C9461DCC815A00769DF0 /* QDTabBarViewController.h */, CDB8C9471DCC815A00769DF0 /* QDTabBarViewController.m */, + D004543024B6014B00F723E0 /* QDStyleSelectableTableViewController.h */, + D004543124B6014B00F723E0 /* QDStyleSelectableTableViewController.m */, ); path = Controllers; sourceTree = ""; @@ -548,14 +770,32 @@ children = ( CDB8C9511DCC815A00769DF0 /* QDAssetsManagerViewController.h */, CDB8C9521DCC815A00769DF0 /* QDAssetsManagerViewController.m */, + D0283ECA20C2F2290090DD45 /* QDBadgeViewController.h */, + D0283ECB20C2F2290090DD45 /* QDBadgeViewController.m */, + CD0C342321E5E2D800B781AD /* QDCellHeightCacheViewController.h */, + CD0C342421E5E2D800B781AD /* QDCellHeightCacheViewController.m */, + CD6BE203205BF41500BE093E /* QDCellHeightKeyCacheViewController.h */, + CD6BE202205BF41500BE093E /* QDCellHeightKeyCacheViewController.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 */, + D052631421EE080E00D4F57A /* QDConsoleViewController.m */, 1679E32C1DFFC0AD0072B8A1 /* QDCustomToastAnimator.h */, 1679E32D1DFFC0AD0072B8A1 /* QDCustomToastAnimator.m */, 1679E32F1DFFC4610072B8A1 /* QDCustomToastContentView.h */, 1679E3301DFFC4610072B8A1 /* QDCustomToastContentView.m */, CDB8C9551DCC815A00769DF0 /* QDDialogViewController.h */, CDB8C9561DCC815A00769DF0 /* QDDialogViewController.m */, + CD0C342021E5E25200B781AD /* QDDynamicHeightTableViewCell.h */, + CD0C342121E5E25200B781AD /* QDDynamicHeightTableViewCell.m */, CDB8C9571DCC815A00769DF0 /* QDEmotionsViewController.h */, CDB8C9581DCC815A00769DF0 /* QDEmotionsViewController.m */, CDB8C9591DCC815A00769DF0 /* QDEmptyViewController.h */, @@ -566,22 +806,28 @@ 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 */, CDB8C9601DCC815A00769DF0 /* QDModalPresentationViewController.m */, CDB8C9611DCC815A00769DF0 /* QDMoreOperationViewController.h */, CDB8C9621DCC815A00769DF0 /* QDMoreOperationViewController.m */, + CDA74F9A206BC1D000AE3830 /* QDMultipleDelegatesViewController.h */, + CDA74F9B206BC1D000AE3830 /* QDMultipleDelegatesViewController.m */, CDB8C9631DCC815A00769DF0 /* QDMultipleImagePickerPreviewViewController.h */, CDB8C9641DCC815A00769DF0 /* QDMultipleImagePickerPreviewViewController.m */, + CD18BD8021870EFD00E2A4E6 /* QDNavigationBarScrollingAnimatorViewController.h */, + CD18BD8121870EFD00E2A4E6 /* QDNavigationBarScrollingAnimatorViewController.m */, + CD18BD8321871DCE00E2A4E6 /* QDNavigationBarScrollingSnapAnimatorViewController.h */, + CD18BD8421871DCE00E2A4E6 /* QDNavigationBarScrollingSnapAnimatorViewController.m */, CDB8C9651DCC815A00769DF0 /* QDNavigationTitleViewController.h */, CDB8C9661DCC815A00769DF0 /* QDNavigationTitleViewController.m */, CDB8C9671DCC815A00769DF0 /* QDPieProgressViewController.h */, @@ -592,12 +838,22 @@ CDB8C9721DCC815A00769DF0 /* QDSaveImageToSpecifiedAlbumViewController.m */, CDB8C9731DCC815A00769DF0 /* QDSaveVideoToSpecifiedAlbumViewController.h */, CDB8C9741DCC815A00769DF0 /* QDSaveVideoToSpecifiedAlbumViewController.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 = ""; @@ -608,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 = ""; @@ -621,12 +890,12 @@ CDB8C9781DCC815A00769DF0 /* Animation */ = { isa = PBXGroup; children = ( - FE775E3A1DF95728007A341F /* QDActivityIndicator.m */, FE775E3B1DF95728007A341F /* QDActivityIndicator.h */, + 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 */, @@ -642,60 +911,70 @@ children = ( CDB8C9961DCC815A00769DF0 /* QDAlertController.h */, CDB8C9971DCC815A00769DF0 /* QDAlertController.m */, + CD05C20A2750D70F0001C5E0 /* QDBlurEffectViewController.h */, + CD05C20B2750D70F0001C5E0 /* QDBlurEffectViewController.m */, + CDF79AAD1F16531100EE8DE9 /* QDButtonEdgeInsetsViewController.h */, + CDF79AAE1F16531100EE8DE9 /* QDButtonEdgeInsetsViewController.m */, CDB8C9981DCC815A00769DF0 /* QDButtonViewController.h */, CDB8C9991DCC815A00769DF0 /* QDButtonViewController.m */, + CD9D6EC4211064060004E222 /* QDCAAnimationViewController.h */, + CD9D6EC5211064060004E222 /* QDCAAnimationViewController.m */, CDB8C99A1DCC815A00769DF0 /* QDChangeNavBarStyleViewController.h */, CDB8C99B1DCC815A00769DF0 /* QDChangeNavBarStyleViewController.m */, - CDB8C99E1DCC815A00769DF0 /* QDCollectionDemoViewController.h */, - CDB8C99F1DCC815A00769DF0 /* QDCollectionDemoViewController.m */, - CDB8C9A01DCC815A00769DF0 /* QDCollectionListViewController.h */, - CDB8C9A11DCC815A00769DF0 /* QDCollectionListViewController.m */, - CDB8C99C1DCC815A00769DF0 /* QDCollectionStackDemoViewController.h */, - CDB8C99D1DCC815A00769DF0 /* QDCollectionStackDemoViewController.m */, - CDB8C9A21DCC815A00769DF0 /* QDCollectionViewDemoCell.h */, - CDB8C9A31DCC815A00769DF0 /* QDCollectionViewDemoCell.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 */, CDB8C9BB1DCC815A00769DF0 /* QDNavigationListViewController.m */, + CD4A05F020288980000159E9 /* QDNavigationTransitionViewController.h */, + CD4A05F120288980000159E9 /* QDNavigationTransitionViewController.m */, CDB8C9BC1DCC815A00769DF0 /* QDNormalButtonViewController.h */, CDB8C9BD1DCC815A00769DF0 /* QDNormalButtonViewController.m */, CD4421D01E85071A001B8C2A /* QDObjectMethodsListViewController.h */, CD4421D11E85071A001B8C2A /* QDObjectMethodsListViewController.m */, CD4421CD1E84C74C001B8C2A /* QDObjectViewController.h */, 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 */, - CDB8C9A61DCC815A00769DF0 /* QDTableViewCellDynamicHeightViewController.h */, - CDB8C9A71DCC815A00769DF0 /* QDTableViewCellDynamicHeightViewController.m */, CDB8C9B01DCC815A00769DF0 /* QDTableViewCellInsetsViewController.h */, CDB8C9B11DCC815A00769DF0 /* QDTableViewCellInsetsViewController.m */, - CDB8C9C41DCC815A00769DF0 /* QDTableViewCellViewController.h */, - CDB8C9C51DCC815A00769DF0 /* QDTableViewCellViewController.m */, + CDC73B432942344F00F1D584 /* QDTableViewCellReorderStyleViewController.h */, + CDC73B442942344F00F1D584 /* QDTableViewCellReorderStyleViewController.m */, + D03102B724AB1DE30095C232 /* QDTableViewCellSeparatorInsetsViewController.h */, + D03102B824AB1DE30095C232 /* QDTableViewCellSeparatorInsetsViewController.m */, + CDA05DA61FB1913400606756 /* QDTableViewHeaderFooterViewController.h */, + CDA05DA71FB1913400606756 /* QDTableViewHeaderFooterViewController.m */, CDB8C9C61DCC815A00769DF0 /* QDTextFieldViewController.h */, CDB8C9C71DCC815A00769DF0 /* QDTextFieldViewController.m */, CDB8C9C81DCC815A00769DF0 /* QDTextViewController.h */, @@ -704,12 +983,12 @@ CDB8C9CB1DCC815A00769DF0 /* QDToolBarButtonViewController.m */, CDB8C9CC1DCC815A00769DF0 /* QDUIKitViewController.h */, CDB8C9CD1DCC815A00769DF0 /* QDUIKitViewController.m */, - CDB8C9CE1DCC815A00769DF0 /* QDUIViewQMUIViewController.h */, - CDB8C9CF1DCC815A00769DF0 /* QDUIViewQMUIViewController.m */, - CD4013041EFCFA300022FE2A /* QDOrientationViewController.h */, - CD4013051EFCFA300022FE2A /* QDOrientationViewController.m */, - CDF79AAD1F16531100EE8DE9 /* QDButtonEdgeInsetsViewController.h */, - CDF79AAE1F16531100EE8DE9 /* QDButtonEdgeInsetsViewController.m */, + CD3CDD4D1F39E977008529DA /* QDUIViewBorderViewController.h */, + CD3CDD4E1F39E977008529DA /* QDUIViewBorderViewController.m */, + CD3CDD501F39F077008529DA /* QDUIViewDebugViewController.h */, + CD3CDD511F39F077008529DA /* QDUIViewDebugViewController.m */, + CD3CDD531F3AAD42008529DA /* QDUIViewLayoutViewController.h */, + CD3CDD541F3AAD42008529DA /* QDUIViewLayoutViewController.m */, ); path = UIKit; sourceTree = ""; @@ -726,6 +1005,8 @@ CDE50AD11DAA6E6F000D5414 /* Frameworks */ = { isa = PBXGroup; children = ( + 8F732791262757E20051952C /* LookinServer.xcframework */, + CD4EA5A9228EDBFF00A55066 /* XCTest.framework */, CD7593641EF7A391004B5819 /* MediaPlayer.framework */, CD7593611EF7A362004B5819 /* Photos.framework */, CD7593601EF7A35B004B5819 /* libxml2.tbd */, @@ -739,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 */ @@ -749,20 +1094,55 @@ 165F42EC1ADB60180057EF6A /* Sources */, 165F42ED1ADB60180057EF6A /* Frameworks */, 165F42EE1ADB60180057EF6A /* Resources */, - CD454B891D913A2E00BE3804 /* Run Script */, - CDD0D8741E0D2BC600A38B96 /* Embed Frameworks */, - CDD4942D1E5E8C3900829B7D /* Integrate Reveal Server */, + CDE85CB91F95E21500E622E8 /* Embed Frameworks */, + CDFD574226DE132800603D1E /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( - CD75935D1EF7A304004B5819 /* PBXTargetDependency */, + 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 */ @@ -770,24 +1150,32 @@ isa = PBXProject; attributes = { CLASSPREFIX = QD; - LastUpgradeCheck = 0820; + DefaultBuildSystemTypeForWorkspace = Latest; + LastUpgradeCheck = 1130; ORGANIZATIONNAME = "QMUI Team"; TargetAttributes = { 165F42EF1ADB60180057EF6A = { CreatedOnToolsVersion = 6.3; - DevelopmentTeam = FP989NU38H; DevelopmentTeamName = "沛钞 陈 (Personal Team)"; - ProvisioningStyle = Manual; + ProvisioningStyle = Automatic; + }; + CD4EA594228D87B000A55066 = { + CreatedOnToolsVersion = 10.2.1; + TestTargetID = 165F42EF1ADB60180057EF6A; + }; + CDFD573826DE132700603D1E = { + CreatedOnToolsVersion = 12.5.1; }; }; }; buildConfigurationList = 165F42EB1ADB60180057EF6A /* Build configuration list for PBXProject "qmuidemo" */; - compatibilityVersion = "Xcode 8.0"; - developmentRegion = English; + compatibilityVersion = "Xcode 15.0"; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( - en, Base, + en, + "zh-Hans", ); mainGroup = 165F42E71ADB60180057EF6A; productRefGroup = 165F42F11ADB60180057EF6A /* Products */; @@ -801,16 +1189,18 @@ projectRoot = ""; targets = ( 165F42EF1ADB60180057EF6A /* qmuidemo */, + CD4EA594228D87B000A55066 /* QMUIDemoUITests */, + CDFD573826DE132700603D1E /* QMUIKeyboard */, ); }; /* End PBXProject section */ /* Begin PBXReferenceProxy section */ - CD7593591EF7A2EC004B5819 /* libQMUI.a */ = { + CD4EA5A1228D87B000A55066 /* QMUIKitTests.xctest */ = { isa = PBXReferenceProxy; - fileType = archive.ar; - path = libQMUI.a; - remoteRef = CD7593581EF7A2EC004B5819 /* PBXContainerItemProxy */; + fileType = wrapper.cfbundle; + path = QMUIKitTests.xctest; + remoteRef = CD4EA5A0228D87B000A55066 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; CD75935B1EF7A2EC004B5819 /* QMUIKit.framework */ = { @@ -832,45 +1222,30 @@ 165F435F1ADB71FF0057EF6A /* image1@2x.png in Resources */, 165F43041ADB60180057EF6A /* LaunchScreen.xib in Resources */, CDD0D8231E0A88B600A38B96 /* image6@2x.png in Resources */, + 4CD323992109B362007033C9 /* Localizable.strings in Resources */, CDD0D8221E0A88B600A38B96 /* image5@2x.png in Resources */, CDD0D8211E0A88B600A38B96 /* image4@2x.png in Resources */, 16F42C121B1C60A70038B28F /* image0@2x.png in Resources */, + CD4EA4B42275D58400A55066 /* animatedImage.gif in Resources */, 165F43011ADB60180057EF6A /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - CD454B891D913A2E00BE3804 /* Run Script */ = { - isa = PBXShellScriptBuildPhase; + CD4EA593228D87B000A55066 /* Resources */ = { + isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# 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\ncd $CONFIGURATION_BUILD_DIR\n\n# zip up the qmuidemo directory\n\n/usr/bin/zip -r qmuidemo.ipa Payload iTunesArtwork"; }; - CDD4942D1E5E8C3900829B7D /* Integrate Reveal Server */ = { - isa = PBXShellScriptBuildPhase; + CDFD573726DE132700603D1E /* Resources */ = { + isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - ); - name = "Integrate Reveal Server"; - 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}\" != \"Debug\" ] && 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 \"Cannot find RevealServer.framework, so Reveal Server will not be started for your app.\"\nfi"; }; -/* End PBXShellScriptBuildPhase section */ +/* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 165F42EC1ADB60180057EF6A /* Sources */ = { @@ -878,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 */, @@ -889,8 +1264,11 @@ CDB8C9D01DCC815A00769DF0 /* QMUIConfigurationTemplate.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 */, 1679E3261DFF9E300072B8A1 /* QDToastListViewController.m in Sources */, CDA1CC991EC1C34300AB8A0F /* QDThemeManager.m in Sources */, FE775E3C1DF95728007A341F /* QDActivityIndicator.m in Sources */, @@ -901,45 +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 */, 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 */, CDB8C9DB1DCC815A00769DF0 /* QDAssetsManagerViewController.m in Sources */, - CDB8CA041DCC815A00769DF0 /* QDTableViewCellDynamicHeightViewController.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 */, - CDB8CA181DCC815A00769DF0 /* QDUIViewQMUIViewController.m in Sources */, + CDA74F9C206BC1D000AE3830 /* QDMultipleDelegatesViewController.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 */, 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 */, @@ -947,35 +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 */, - CDB8CA0C1DCC815A00769DF0 /* QDLinkButtonViewController.m in Sources */, + CD3CDD551F3AAD42008529DA /* QDUIViewLayoutViewController.m in Sources */, + CD3CDD4F1F39E977008529DA /* QDUIViewBorderViewController.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 */, - CD1A8A721EC3110300B81693 /* QDThemeViewController.m in Sources */, + CD18BD8221870EFD00E2A4E6 /* QDNavigationBarScrollingAnimatorViewController.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 */ - CD75935D1EF7A304004B5819 /* PBXTargetDependency */ = { + CD4EA59B228D87B000A55066 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 165F42EF1ADB60180057EF6A /* qmuidemo */; + targetProxy = CD4EA59A228D87B000A55066 /* PBXContainerItemProxy */; + }; + CDE85CB81F95E21500E622E8 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = QMUIKit; - targetProxy = CD75935C1EF7A304004B5819 /* PBXContainerItemProxy */; + targetProxy = CDE85CB71F95E21500E622E8 /* PBXContainerItemProxy */; + }; + CDFD574026DE132800603D1E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = CDFD573826DE132700603D1E /* QMUIKeyboard */; + targetProxy = CDFD573F26DE132800603D1E /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -984,10 +1425,32 @@ isa = PBXVariantGroup; children = ( 165F43031ADB60180057EF6A /* Base */, + 4CD323892109A55E007033C9 /* en */, + 4CD3238A2109A564007033C9 /* zh-Hans */, ); name = LaunchScreen.xib; sourceTree = ""; }; + 4CD3238D2109A7AE007033C9 /* Info.plist */ = { + isa = PBXVariantGroup; + children = ( + 4CD3238C2109A7AE007033C9 /* zh-Hans */, + 4CD3238E2109A810007033C9 /* Base */, + 4CD323902109A84D007033C9 /* en */, + ); + name = Info.plist; + sourceTree = ""; + }; + 4CD323972109B362007033C9 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 4CD323982109B362007033C9 /* Base */, + 4CD3239A2109B364007033C9 /* en */, + 4CD3239B2109B366007033C9 /* zh-Hans */, + ); + name = Localizable.strings; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -995,18 +1458,27 @@ 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; @@ -1031,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; @@ -1043,18 +1515,27 @@ 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; @@ -1071,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"; @@ -1084,34 +1565,48 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_DISPLAY_NAME = "QMUI (Debug)"; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO; + CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CODE_SIGN_ENTITLEMENTS = ""; - CODE_SIGN_IDENTITY = "iPhone Developer: Peichao Chen (S96GZGJFS6)"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: Peichao Chen (S96GZGJFS6)"; - DEVELOPMENT_TEAM = FP989NU38H; + CODE_SIGN_ENTITLEMENTS = qmuidemo/qmuidemo.entitlements; + 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; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", - "$(SRCROOT)", "$(PROJECT_DIR)/qmuidemo/Frameworks", ); GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = ./qmuidemo/PrefixHeader.pch; HEADER_SEARCH_PATHS = ""; INFOPLIST_FILE = qmuidemo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + 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 = "20f9eca6-025b-4e80-9ba3-52d1d9a44e5b"; - PROVISIONING_PROFILE_SPECIFIER = qmuidemo; + 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; }; @@ -1120,10 +1615,16 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_DISPLAY_NAME = QMUI; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = NO; + CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CODE_SIGN_IDENTITY = "iPhone Developer: Peichao Chen (S96GZGJFS6)"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: Peichao Chen (S96GZGJFS6)"; - DEVELOPMENT_TEAM = FP989NU38H; + CODE_SIGN_ENTITLEMENTS = qmuidemo/qmuidemo.entitlements; + 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; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", @@ -1133,91 +1634,153 @@ GCC_PREFIX_HEADER = ./qmuidemo/PrefixHeader.pch; HEADER_SEARCH_PATHS = ""; INFOPLIST_FILE = qmuidemo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + 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 = "20f9eca6-025b-4e80-9ba3-52d1d9a44e5b"; - PROVISIONING_PROFILE_SPECIFIER = qmuidemo; + 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; }; - CD6DFFEA1E9630BD0010C216 /* RDM */ = { + CD4EA59C228D87B000A55066 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = 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_OBJC_ROOT_CLASS = YES_ERROR; - 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; }; - CD6DFFEB1E9630BD0010C216 /* RDM */ = { + CD4EA59D228D87B000A55066 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - BUNDLE_DISPLAY_NAME = QMUI; + 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 Developer: Peichao Chen (S96GZGJFS6)"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: Peichao Chen (S96GZGJFS6)"; - DEVELOPMENT_TEAM = FP989NU38H; - FRAMEWORK_SEARCH_PATHS = ( + 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)", - "$(PROJECT_DIR)", - "$(PROJECT_DIR)/qmuidemo/Frameworks", + "@executable_path/Frameworks", + "@loader_path/Frameworks", ); - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = ./qmuidemo/PrefixHeader.pch; - HEADER_SEARCH_PATHS = "$(SRCROOT)/QMUI/**"; - INFOPLIST_FILE = qmuidemo/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - OTHER_LDFLAGS = ( + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.qmui.QMUIDemoUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = qmuidemo; + }; + name = Release; + }; + 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)", - "-ObjC", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.qmuidemo.qmui; + 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)"; - PROVISIONING_PROFILE = "20f9eca6-025b-4e80-9ba3-52d1d9a44e5b"; - PROVISIONING_PROFILE_SPECIFIER = qmuidemo; - SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 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 = RDM; + name = Release; }; /* End XCBuildConfiguration section */ @@ -1227,20 +1790,36 @@ buildConfigurations = ( 165F43111ADB60180057EF6A /* Debug */, 165F43121ADB60180057EF6A /* Release */, - CD6DFFEA1E9630BD0010C216 /* RDM */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = RDM; + defaultConfigurationName = Debug; }; 165F43131ADB60180057EF6A /* Build configuration list for PBXNativeTarget "qmuidemo" */ = { isa = XCConfigurationList; buildConfigurations = ( 165F43141ADB60180057EF6A /* Debug */, 165F43151ADB60180057EF6A /* Release */, - CD6DFFEB1E9630BD0010C216 /* 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; + }; + CDFD574726DE132800603D1E /* Build configuration list for PBXNativeTarget "QMUIKeyboard" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CDFD574326DE132800603D1E /* Debug */, + CDFD574426DE132800603D1E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; }; /* End XCConfigurationList section */ }; diff --git a/qmuidemo.xcodeproj/xcshareddata/xcschemes/qmuidemo.xcscheme b/qmuidemo.xcodeproj/xcshareddata/xcschemes/qmuidemo.xcscheme new file mode 100644 index 00000000..681f95ed --- /dev/null +++ b/qmuidemo.xcodeproj/xcshareddata/xcschemes/qmuidemo.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qmuidemo.xcodeproj/xcuserdata/zhoon.xcuserdatad/xcschemes/qmuidemo.xcscheme b/qmuidemo.xcodeproj/xcuserdata/zhoon.xcuserdatad/xcschemes/qmuidemo.xcscheme deleted file mode 100644 index 40e5d872..00000000 --- a/qmuidemo.xcodeproj/xcuserdata/zhoon.xcuserdatad/xcschemes/qmuidemo.xcscheme +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/qmuidemo.xcodeproj/xcuserdata/zhoon.xcuserdatad/xcschemes/xcschememanagement.plist b/qmuidemo.xcodeproj/xcuserdata/zhoon.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 1104cd4e..00000000 --- a/qmuidemo.xcodeproj/xcuserdata/zhoon.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,27 +0,0 @@ - - - - - SchemeUserState - - qmuidemo.xcscheme - - orderHint - 0 - - - SuppressBuildableAutocreation - - 165F42EF1ADB60180057EF6A - - primary - - - 165F43081ADB60180057EF6A - - primary - - - - - diff --git a/qmuidemo/AppDelegate.h b/qmuidemo/AppDelegate.h index 1d0de44f..714dcd80 100644 --- a/qmuidemo/AppDelegate.h +++ b/qmuidemo/AppDelegate.h @@ -2,14 +2,14 @@ // AppDelegate.h // qmuidemo // -// Created by ZhoonChen on 15/4/13. +// Created by QMUI Team on 15/4/13. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import -@interface AppDelegate : UIResponder +@interface AppDelegate : UIResponder -@property (strong, nonatomic) UIWindow *window; +@property(strong, nonatomic) UIWindow *window; @end diff --git a/qmuidemo/AppDelegate.m b/qmuidemo/AppDelegate.m index 103f2a1f..aac6aeaa 100644 --- a/qmuidemo/AppDelegate.m +++ b/qmuidemo/AppDelegate.m @@ -2,7 +2,7 @@ // AppDelegate.m // qmuidemo // -// Created by ZhoonChen on 15/4/13. +// Created by QMUI Team on 15/4/13. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -14,66 +14,125 @@ #import "QDUIKitViewController.h" #import "QDComponentsViewController.h" #import "QDLabViewController.h" -#import "QMUIConfigurationTemplate.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 { - // 应用 QMUI Demo 皮肤 - NSString *themeClassName = [[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeClassName] ?: NSStringFromClass([QMUIConfigurationTemplate class]); - [QDThemeManager sharedInstance].currentTheme = [[NSClassFromString(themeClassName) alloc] init]; + // 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; // QD自定义的全局样式渲染 [QDCommonUI renderGlobalAppearances]; - // 预加载 QQ 表情,避免第一次使用时卡顿(可选) - dispatch_async(dispatch_get_global_queue(0, 0), ^{ - [QMUIQQEmotionManager emotionsForQQ]; + // 预加载 QQ 表情,避免第一次使用时卡顿 + 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]; @@ -102,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:^{ @@ -112,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 1d6098ec..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/Base.lproj/Localizable.strings b/qmuidemo/Base.lproj/Localizable.strings new file mode 100644 index 00000000..1830de8e --- /dev/null +++ b/qmuidemo/Base.lproj/Localizable.strings @@ -0,0 +1,11 @@ +/* + Localizable.strings + qmuidemo + + Created by QMUI Team on 07/26/2018. + Copyright © 2018 QMUI Team. All rights reserved. +*/ +"QMUIButton_Normal_Button_Title" = "QMUIButton_Normal_Button_Title"; +"QMUIButton_Bordered_Button_Title" = "QMUIButton_Bordered_Button_Title"; +"QMUIButton_Image_Position_Button_Title_1" = "QMUIButton_Image_Position_Button_Title_1"; +"QMUIButton_Image_Position_Button_Title_2" = "QMUIButton_Image_Position_Button_Title_2"; 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 f6296a21..00000000 --- a/qmuidemo/Frameworks/RevealServer.framework/Headers/RevealServer.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// RevealServer.h -// RevealServer -// -// Created by Tony Arnold on 25/11/2015. -// 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[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/qmuidemo/Frameworks/RevealServer.framework/Info.plist b/qmuidemo/Frameworks/RevealServer.framework/Info.plist deleted file mode 100644 index 7f297cfa..00000000 Binary files a/qmuidemo/Frameworks/RevealServer.framework/Info.plist and /dev/null differ 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 49d763b3..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 ebfa23fb..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}/${REVEAL_SERVER_FILENAME}" -} - -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 80f896d0..00000000 --- a/qmuidemo/Frameworks/RevealServer.framework/_CodeSignature/CodeResources +++ /dev/null @@ -1,166 +0,0 @@ - - - - - files - - Headers/RevealServer.h - - e2S6Vuf8iJXurblvYWL8e3IMO7E= - - Info.plist - - hD3V7tiXGKcqJIFK4LKpeO/AGbs= - - Modules/module.modulemap - - EuDEeG1dcC1sd+hIW2SkUAImUg8= - - Scripts/copy_and_codesign_revealserver.sh - - aQbLdf9lVnmDd2BfBMGsVPBPdb8= - - - files2 - - Headers/RevealServer.h - - hash - - e2S6Vuf8iJXurblvYWL8e3IMO7E= - - hash2 - - i4zuiS2fsgwsoicYEzHuBx32JYfKW38gkopt/7FdINY= - - - Modules/module.modulemap - - hash - - EuDEeG1dcC1sd+hIW2SkUAImUg8= - - hash2 - - tstqiJpIPr4iEd3MDHClLuTB/ciSC/zNlke1AjfSVuU= - - - Scripts/copy_and_codesign_revealserver.sh - - hash - - aQbLdf9lVnmDd2BfBMGsVPBPdb8= - - hash2 - - UYSfYiTYxoDhVX7UwCYvPXGOWQ4Yrm5DzFoomMlSIMs= - - - - rules - - ^ - - ^.*\.lproj/ - - optional - - weight - 1000 - - ^.*\.lproj/locversion.plist$ - - omit - - weight - 1100 - - ^Base\.lproj/ - - weight - 1010 - - ^version.plist$ - - - rules2 - - .*\.dSYM($|/) - - weight - 11 - - ^ - - weight - 20 - - ^(.*/)?\.DS_Store$ - - omit - - weight - 2000 - - ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ - - nested - - weight - 10 - - ^.* - - ^.*\.lproj/ - - optional - - weight - 1000 - - ^.*\.lproj/locversion.plist$ - - omit - - weight - 1100 - - ^Base\.lproj/ - - weight - 1010 - - ^Info\.plist$ - - omit - - weight - 20 - - ^PkgInfo$ - - omit - - weight - 20 - - ^[^/]+$ - - nested - - weight - 10 - - ^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 b31e8849..d5932d15 100644 --- a/qmuidemo/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/qmuidemo/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,119 +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" + "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-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-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-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-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/emotion_01.imageset/Contents.json b/qmuidemo/Images.xcassets/emotion_01.imageset/Contents.json new file mode 100644 index 00000000..68ba8168 --- /dev/null +++ b/qmuidemo/Images.xcassets/emotion_01.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "emotion_01.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/emotion_01.imageset/emotion_01.pdf b/qmuidemo/Images.xcassets/emotion_01.imageset/emotion_01.pdf new file mode 100644 index 00000000..3ec008d3 Binary files /dev/null and b/qmuidemo/Images.xcassets/emotion_01.imageset/emotion_01.pdf differ diff --git a/qmuidemo/Images.xcassets/emotion_02.imageset/Contents.json b/qmuidemo/Images.xcassets/emotion_02.imageset/Contents.json new file mode 100644 index 00000000..43313ec9 --- /dev/null +++ b/qmuidemo/Images.xcassets/emotion_02.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "emotion_02.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/emotion_02.imageset/emotion_02.pdf b/qmuidemo/Images.xcassets/emotion_02.imageset/emotion_02.pdf new file mode 100644 index 00000000..8f3e795a Binary files /dev/null and b/qmuidemo/Images.xcassets/emotion_02.imageset/emotion_02.pdf differ diff --git a/qmuidemo/Images.xcassets/emotion_03.imageset/Contents.json b/qmuidemo/Images.xcassets/emotion_03.imageset/Contents.json new file mode 100644 index 00000000..d20a9317 --- /dev/null +++ b/qmuidemo/Images.xcassets/emotion_03.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "emotion_03.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/emotion_03.imageset/emotion_03.pdf b/qmuidemo/Images.xcassets/emotion_03.imageset/emotion_03.pdf new file mode 100644 index 00000000..52c1a088 Binary files /dev/null and b/qmuidemo/Images.xcassets/emotion_03.imageset/emotion_03.pdf differ diff --git a/qmuidemo/Images.xcassets/emotion_04.imageset/Contents.json b/qmuidemo/Images.xcassets/emotion_04.imageset/Contents.json new file mode 100644 index 00000000..61ee821d --- /dev/null +++ b/qmuidemo/Images.xcassets/emotion_04.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "emotion_04.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/emotion_04.imageset/emotion_04.pdf b/qmuidemo/Images.xcassets/emotion_04.imageset/emotion_04.pdf new file mode 100644 index 00000000..3e2ac460 Binary files /dev/null and b/qmuidemo/Images.xcassets/emotion_04.imageset/emotion_04.pdf differ diff --git a/qmuidemo/Images.xcassets/emotion_05.imageset/Contents.json b/qmuidemo/Images.xcassets/emotion_05.imageset/Contents.json new file mode 100644 index 00000000..2fceb4dc --- /dev/null +++ b/qmuidemo/Images.xcassets/emotion_05.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "emotion_05.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/emotion_05.imageset/emotion_05.pdf b/qmuidemo/Images.xcassets/emotion_05.imageset/emotion_05.pdf new file mode 100644 index 00000000..9ce61a73 Binary files /dev/null and b/qmuidemo/Images.xcassets/emotion_05.imageset/emotion_05.pdf differ diff --git a/qmuidemo/Images.xcassets/emotion_06.imageset/Contents.json b/qmuidemo/Images.xcassets/emotion_06.imageset/Contents.json new file mode 100644 index 00000000..fe21372b --- /dev/null +++ b/qmuidemo/Images.xcassets/emotion_06.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "emotion_06.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/emotion_06.imageset/emotion_06.pdf b/qmuidemo/Images.xcassets/emotion_06.imageset/emotion_06.pdf new file mode 100644 index 00000000..5176d29d Binary files /dev/null and b/qmuidemo/Images.xcassets/emotion_06.imageset/emotion_06.pdf differ diff --git a/qmuidemo/Images.xcassets/emotion_07.imageset/Contents.json b/qmuidemo/Images.xcassets/emotion_07.imageset/Contents.json new file mode 100644 index 00000000..87b34847 --- /dev/null +++ b/qmuidemo/Images.xcassets/emotion_07.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "emotion_07.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/emotion_07.imageset/emotion_07.pdf b/qmuidemo/Images.xcassets/emotion_07.imageset/emotion_07.pdf new file mode 100644 index 00000000..0739277f Binary files /dev/null and b/qmuidemo/Images.xcassets/emotion_07.imageset/emotion_07.pdf differ diff --git a/qmuidemo/Images.xcassets/emotion_08.imageset/Contents.json b/qmuidemo/Images.xcassets/emotion_08.imageset/Contents.json new file mode 100644 index 00000000..d09eb2e5 --- /dev/null +++ b/qmuidemo/Images.xcassets/emotion_08.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "emotion_08.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/emotion_08.imageset/emotion_08.pdf b/qmuidemo/Images.xcassets/emotion_08.imageset/emotion_08.pdf new file mode 100644 index 00000000..0cbf6aa9 Binary files /dev/null and b/qmuidemo/Images.xcassets/emotion_08.imageset/emotion_08.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_grid_badge.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_badge.imageset/Contents.json new file mode 100644 index 00000000..37a28457 --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_badge.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon_grid_badge.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/icon_grid_tabBarItem.imageset/icon_grid_tabBarItem.pdf b/qmuidemo/Images.xcassets/icon_grid_badge.imageset/icon_grid_badge.pdf similarity index 100% rename from qmuidemo/Images.xcassets/icon_grid_tabBarItem.imageset/icon_grid_tabBarItem.pdf rename to qmuidemo/Images.xcassets/icon_grid_badge.imageset/icon_grid_badge.pdf 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_caanimation.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_caanimation.imageset/Contents.json new file mode 100644 index 00000000..c27cf072 --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_caanimation.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon_grid_caanimation.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/icon_grid_caanimation.imageset/icon_grid_caanimation.pdf b/qmuidemo/Images.xcassets/icon_grid_caanimation.imageset/icon_grid_caanimation.pdf new file mode 100644 index 00000000..49c76cf7 Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_caanimation.imageset/icon_grid_caanimation.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_grid_cellKeyCache.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_cellKeyCache.imageset/Contents.json new file mode 100644 index 00000000..87df8bbb --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_cellKeyCache.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon_grid_cellKeyCache.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/icon_grid_cellKeyCache.imageset/icon_grid_cellKeyCache.pdf b/qmuidemo/Images.xcassets/icon_grid_cellKeyCache.imageset/icon_grid_cellKeyCache.pdf new file mode 100644 index 00000000..66dfeb13 Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_cellKeyCache.imageset/icon_grid_cellKeyCache.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_console.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_console.imageset/Contents.json new file mode 100644 index 00000000..1c73c9bf --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_console.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon_grid_console.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/icon_grid_console.imageset/icon_grid_console.pdf b/qmuidemo/Images.xcassets/icon_grid_console.imageset/icon_grid_console.pdf new file mode 100644 index 00000000..f47d82ed Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_console.imageset/icon_grid_console.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_imageView.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_imageView.imageset/Contents.json new file mode 100644 index 00000000..12c3db10 --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_imageView.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon_grid_imageView.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/icon_grid_imageView.imageset/icon_grid_imageView.pdf b/qmuidemo/Images.xcassets/icon_grid_imageView.imageset/icon_grid_imageView.pdf new file mode 100644 index 00000000..416af08e Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_imageView.imageset/icon_grid_imageView.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_grid_multipleDelegates.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_multipleDelegates.imageset/Contents.json new file mode 100644 index 00000000..026c548d --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_multipleDelegates.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon_grid_multipleDelegates.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/icon_grid_multipleDelegates.imageset/icon_grid_multipleDelegates.pdf b/qmuidemo/Images.xcassets/icon_grid_multipleDelegates.imageset/icon_grid_multipleDelegates.pdf new file mode 100644 index 00000000..7e16c8c7 Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_multipleDelegates.imageset/icon_grid_multipleDelegates.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_grid_scrollAnimator.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_scrollAnimator.imageset/Contents.json new file mode 100644 index 00000000..b424b8f5 --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_grid_scrollAnimator.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon_grid_scrollAnimator.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/icon_grid_scrollAnimator.imageset/icon_grid_scrollAnimator.pdf b/qmuidemo/Images.xcassets/icon_grid_scrollAnimator.imageset/icon_grid_scrollAnimator.pdf new file mode 100644 index 00000000..7fa0abc7 Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_scrollAnimator.imageset/icon_grid_scrollAnimator.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_tabBar.imageset/icon_grid_tabBar.pdf b/qmuidemo/Images.xcassets/icon_grid_tabBar.imageset/icon_grid_tabBar.pdf new file mode 100644 index 00000000..f779f0f6 Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_grid_tabBar.imageset/icon_grid_tabBar.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_grid_tabBarItem.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_grid_tabBarItem.imageset/Contents.json deleted file mode 100644 index e08bcab3..00000000 --- a/qmuidemo/Images.xcassets/icon_grid_tabBarItem.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon_grid_tabBarItem.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file 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/icon_moreOperation_add.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_moreOperation_add.imageset/Contents.json new file mode 100644 index 00000000..503551bb --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_moreOperation_add.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon_moreOperation_add.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/icon_moreOperation_add.imageset/icon_moreOperation_add.pdf b/qmuidemo/Images.xcassets/icon_moreOperation_add.imageset/icon_moreOperation_add.pdf new file mode 100644 index 00000000..21eb6051 Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_moreOperation_add.imageset/icon_moreOperation_add.pdf differ diff --git a/qmuidemo/Images.xcassets/icon_moreOperation_remove.imageset/Contents.json b/qmuidemo/Images.xcassets/icon_moreOperation_remove.imageset/Contents.json new file mode 100644 index 00000000..ad9e3849 --- /dev/null +++ b/qmuidemo/Images.xcassets/icon_moreOperation_remove.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon_moreOperation_remove.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/icon_moreOperation_remove.imageset/icon_moreOperation_remove.pdf b/qmuidemo/Images.xcassets/icon_moreOperation_remove.imageset/icon_moreOperation_remove.pdf new file mode 100644 index 00000000..5298b1eb Binary files /dev/null and b/qmuidemo/Images.xcassets/icon_moreOperation_remove.imageset/icon_moreOperation_remove.pdf differ diff --git a/qmuidemo/Images.xcassets/launch_background.imageset/Contents.json b/qmuidemo/Images.xcassets/launch_background.imageset/Contents.json deleted file mode 100644 index 6d88cc86..00000000 --- a/qmuidemo/Images.xcassets/launch_background.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "launch_background.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/qmuidemo/Images.xcassets/launch_background.imageset/launch_background.pdf b/qmuidemo/Images.xcassets/launch_background.imageset/launch_background.pdf deleted file mode 100644 index a6657d18..00000000 Binary files a/qmuidemo/Images.xcassets/launch_background.imageset/launch_background.pdf and /dev/null 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/navigationbar_background.imageset/Contents.json b/qmuidemo/Images.xcassets/navigationbar_background.imageset/Contents.json deleted file mode 100644 index f382e290..00000000 --- a/qmuidemo/Images.xcassets/navigationbar_background.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "navigationbar_background.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/qmuidemo/Images.xcassets/navigationbar_background.imageset/navigationbar_background.pdf deleted file mode 100644 index f087b1e3..00000000 Binary files a/qmuidemo/Images.xcassets/navigationbar_background.imageset/navigationbar_background.pdf and /dev/null 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 12581791..eff2ac4a 100644 --- a/qmuidemo/Info.plist +++ b/qmuidemo/Info.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - zh_CN + en_US CFBundleDisplayName $(BUNDLE_DISPLAY_NAME) CFBundleExecutable @@ -17,19 +17,21 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.7.4 + $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion - 17 + $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS NSCameraUsageDescription - $(PRODUCT_NAME) 想要使用摄像头 + $(PRODUCT_NAME) wants to use your camera NSMicrophoneUsageDescription - $(PRODUCT_NAME) 想要使用麦克风 + $(PRODUCT_NAME) wants to use your microphone + NSPhotoLibraryAddUsageDescription + $(PRODUCT_NAME) wants to add photos to your photo library NSPhotoLibraryUsageDescription - $(PRODUCT_NAME) 想要访问照片库 + $(PRODUCT_NAME) wants to use your photo library UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -53,6 +55,6 @@ UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance - + diff --git a/qmuidemo/Modules/Common/Configuration/QDThemeManager.h b/qmuidemo/Modules/Common/Configuration/QDThemeManager.h index 8b8042b6..345fa0a0 100644 --- a/qmuidemo/Modules/Common/Configuration/QDThemeManager.h +++ b/qmuidemo/Modules/Common/Configuration/QDThemeManager.h @@ -2,29 +2,48 @@ // QDThemeManager.h // qmuidemo // -// Created by MoLice on 2017/5/9. +// Created by QMUI Team on 2017/5/9. // Copyright © 2017年 QMUI Team. All rights reserved. // #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 -/// 主题发生改变后的值,类型为 NSObject,可能为 NSNull -extern NSString *const QDThemeAfterChangedName; +@interface UIColor (QDTheme) -/** - * QMUI Demo 的皮肤管理器,当需要换肤时,请为 currentTheme 赋值;当需要获取当前皮肤时,可访问 currentTheme 属性。 - * 可通过监听 QDThemeChangedNotification 通知来捕获换肤事件,默认地,QDCommonViewController 及 QDCommonTableViewController 均已支持响应换肤,其响应方法是通过 QDChangingThemeDelegate 接口来实现的。 - */ -@interface QDThemeManager : NSObject +@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 + +@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 9d73cf61..af6e0bea 100644 --- a/qmuidemo/Modules/Common/Configuration/QDThemeManager.m +++ b/qmuidemo/Modules/Common/Configuration/QDThemeManager.m @@ -2,15 +2,40 @@ // QDThemeManager.m // qmuidemo // -// Created by MoLice on 2017/5/9. +// Created by QMUI Team on 2017/5/9. // Copyright © 2017年 QMUI Team. All rights reserved. // #import "QDThemeManager.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 @@ -27,17 +52,178 @@ + (id)allocWithZone:(struct _NSZone *)zone{ return [self sharedInstance]; } -- (void)setCurrentTheme:(NSObject *)currentTheme { - BOOL isThemeChanged = _currentTheme != currentTheme; - NSObject *themeBeforeChanged = nil; - if (isThemeChanged) { - themeBeforeChanged = _currentTheme; - } - _currentTheme = currentTheme; - if (isThemeChanged) { - [currentTheme setupConfigurationTemplate]; - [[NSNotificationCenter defaultCenter] postNotificationName:QDThemeChangedNotification object:self userInfo:@{QDThemeBeforeChangedName: themeBeforeChanged ?: [NSNull null], QDThemeAfterChangedName: currentTheme ?: [NSNull null]}]; +- (instancetype)init { + if (self = [super init]) { + 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; +} + ++ (NSObject *)currentTheme { + return QMUIThemeManagerCenter.defaultThemeManager.currentTheme; +} + +@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; +} + ++ (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) + ++ (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 8ead09cd..a636faf6 100644 --- a/qmuidemo/Modules/Common/Configuration/QDThemeProtocol.h +++ b/qmuidemo/Modules/Common/Configuration/QDThemeProtocol.h @@ -2,35 +2,50 @@ // QDThemeProtocol.h // qmuidemo // -// Created by MoLice on 2017/5/9. +// Created by QMUI Team on 2017/5/9. // Copyright © 2017年 QMUI Team. All rights reserved. // #import /// 所有主题均应实现这个协议,规定了 QMUI Demo 里常用的几个关键外观属性 -@protocol QDThemeProtocol +@protocol QDThemeProtocol @required -/// 来自于 QMUIConfigurationTemplate 里的自带方法,用于应用配置表里的设置 -- (void)setupConfigurationTemplate; +/// 界面背景色 +- (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.h b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplate.h index b658dc74..fa10ac69 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplate.h +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplate.h @@ -1,7 +1,7 @@ // // QMUIConfigurationTemplate.h // -// Created by QQMail on 15/3/29. +// Created by QMUI Team on 15/3/29. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -9,10 +9,12 @@ #import "QDThemeProtocol.h" /** - * QMUIConfigurationTemplate 是一份配置表,用于配合 QMUIKit 来管理整个 App 的全局样式,使用方式如下: - * 1. 在 QMUI 项目代码的文件夹里找到 QMUIConfigurationTemplate 目录,把里面所有文件复制到自己项目里。 - * 2. 在自己项目的 AppDelegate 里 #import "QMUIConfigurationTemplate.h",然后在 application:didFinishLaunchingWithOptions: 里调用 [QMUIConfigurationTemplate setupConfigurationTemplate],即可让配置表生效。 - * 3. 更新 QMUIKit 的版本时,请留意 Release Log 里是否有提醒更新配置表,请尽量保持自己项目里的配置表与 QMUIKit 里的配置表一致,避免遗漏新的属性。 + * QMUIConfigurationTemplate 是一份配置表,用于配合 QMUIConfiguration 来管理整个 App 的全局样式,使用方式: + * 在 QMUI 项目代码的文件夹里找到 QMUIConfigurationTemplate 目录,把里面所有文件复制到自己项目里,保证能被编译到即可,不需要在某些地方 import,也不需要手动运行。 + * + * @warning 更新 QMUIKit 的版本时,请留意 Release Log 里是否有提醒更新配置表,请尽量保持自己项目里的配置表与 QMUIKit 里的配置表一致,避免遗漏新的属性。 + * @warning 配置表的 class 名必须以 QMUIConfigurationTemplate 开头,并且实现 ,因为这两者是 QMUI 识别该 NSObject 是否为一份配置表的条件。 + * @warning QMUI 2.3.0 之后,配置表改为自动运行,不需要再在某个地方手动运行了。 */ @interface QMUIConfigurationTemplate : NSObject diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplate.m b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplate.m index 60f56bc2..e2d4da30 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplate.m +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplate.m @@ -2,16 +2,17 @@ // QMUIConfigurationTemplate.m // qmui // -// Created by QQMail on 15/3/29. +// Created by QMUI Team on 15/3/29. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import "QMUIConfigurationTemplate.h" -#import @implementation QMUIConfigurationTemplate -- (void)setupConfigurationTemplate { +#pragma mark - + +- (void)applyConfigurationTemplate { // === 修改配置值 === // @@ -21,27 +22,32 @@ - (void)setupConfigurationTemplate { 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 @@ -51,155 +57,296 @@ - (void)setupConfigurationTemplate { #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.buttonTintColor = UIColor.qd_tintColor; // ButtonTintColor : QMUIButton 默认的 tintColor,不影响系统的 UIButton - 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 的输入框 +#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 : QMUINavigationButton 的字体 - QMUICMI.navBarButtonFontBold = UIFontBoldMake(17); // NavBarButtonFontBold : QMUINavigationButtonTypeBold 的字体 - QMUICMI.navBarBackgroundImage = [UIImageMake(@"navigationbar_background") resizableImageWithCapInsets:UIEdgeInsetsMake(0, 2, 0, 2)]; // NavBarBackgroundImage : UINavigationBar 的背景图 - QMUICMI.navBarShadowImage = [UIImage new]; // 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 : UINavigationBar 的 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 在大标题模式下的标题颜色 + QMUICMI.navBarLargeTitleFont = nil; // NavBarLargeTitleFont : UINavigationBar 在大标题模式下的标题字体 QMUICMI.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; // NavBarBarBackButtonTitlePositionAdjustment : 导航栏返回按钮的文字偏移 - QMUICMI.navBarBackIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavBack size:CGSizeMake(12, 20) tintColor:NavBarTintColor]; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片 - 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:UIColorWhite]; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 + QMUICMI.navBarAccessoryViewTypeDisclosureIndicatorImage = UIImage.qd_navigationBarDisclosureIndicatorImage; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 + #pragma mark - TabBar - QMUICMI.tabBarBackgroundImage = [UIImage qmui_imageWithColor:UIColorMake(249, 249, 249)]; // TabBarBackgroundImage : UITabBar 的背景图 - 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 = UIColorWhite; // SearchBarTextFieldBackground : QMUISearchBar 里的文本框的背景颜色 - QMUICMI.searchBarTextFieldBorderColor = UIColorMake(205, 208, 210); // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色 - QMUICMI.searchBarBottomBorderColor = UIColorMake(205, 208, 210); // SearchBarBottomBorderColor : QMUISearchBar 底部分隔线颜色 - QMUICMI.searchBarBarTintColor = UIColorMake(247, 247, 247); // SearchBarBarTintColor : QMUISearchBar 的 barTintColor,也即背景色 - QMUICMI.searchBarTintColor = self.themeTintColor; // SearchBarTintColor : QMUISearchBar 的 tintColor,也即上面的操作控件的主题色 - QMUICMI.searchBarTextColor = UIColorBlack; // SearchBarTextColor : QMUISearchBar 里的文本框的文字颜色 - QMUICMI.searchBarPlaceholderColor = UIColorPlaceholder; // SearchBarPlaceholderColor : QMUISearchBar 里的文本框的 placeholder 颜色 + QMUICMI.searchBarTextFieldBackgroundImage = UIImage.qd_searchBarTextFieldBackgroundImage; // SearchBarTextFieldBackgroundImage : QMUISearchBar 里的文本框的背景图,图片高度会决定输入框的高度 + QMUICMI.searchBarTextFieldBorderColor = nil; // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色 + 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 = 2.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小 -#pragma mark - TableView / TableViewCell +#pragma mark - Plain TableView - QMUICMI.tableViewBackgroundColor = nil; // TableViewBackgroundColor : Plain 类型的 QMUITableView 的背景色颜色 - QMUICMI.tableViewGroupedBackgroundColor = UIColorMake(246, 246, 246); // TableViewGroupedBackgroundColor : Grouped 类型的 QMUITableView 的背景色 + QMUICMI.tableViewEstimatedHeightEnabled = YES; // TableViewEstimatedHeightEnabled : 是否要开启全局 QMUITableView 和 UITableView 的 estimatedRow(Section/Footer)Height + + 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 : 列表默认的 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.tableViewCellNormalHeight = 56; // TableViewCellNormalHeight : QMUITableView 的默认 cell 高度 + 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 里的文字颜色 QMUICMI.tableViewSectionFooterTextColor = UIColorGray; // TableViewSectionFooterTextColor : Plain 类型的 QMUITableView sectionFooter 里的文字颜色 - QMUICMI.tableViewSectionHeaderHeight = 20; // TableViewSectionHeaderHeight : Plain 类型的 QMUITableView sectionHeader 的默认高度 - QMUICMI.tableViewSectionFooterHeight = 0; // TableViewSectionFooterHeight : 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 + 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.tableViewGroupedSectionHeaderHeight = 15; // TableViewGroupedSectionHeaderHeight : Grouped 类型的 QMUITableView sectionHeader 的默认高度 - QMUICMI.tableViewGroupedSectionFooterHeight = 1; // TableViewGroupedSectionFooterHeight : Grouped 类型的 QMUITableView sectionFooter 的默认高度 - QMUICMI.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, PreferredVarForDevices(20, 15, 15, 15), 8, PreferredVarForDevices(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 - QMUICMI.windowLevelQMUIImagePreviewView = UIWindowLevelStatusBar + 1.0; // UIWindowLevelQMUIImagePreviewView : QMUIImagePreviewViewController 里使用的 UIWindow 的 windowLevel + QMUICMI.windowLevelQMUIConsole = 1; // UIWindowLevelQMUIConsole : QMUIConsole 内部的 UIWindow 的 windowLevel + +#pragma mark - QMUIBadge + + 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 : 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.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 { + [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) { + 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 62dd5cba..70b73cdd 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.h +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.h @@ -1,21 +1,12 @@ // // QMUIConfigurationTemplate.h // -// Created by QQMail on 15/3/29. +// Created by QMUI Team on 15/3/29. // Copyright (c) 2015年 QMUI Team. All rights reserved. // -#import -#import "QDThemeProtocol.h" +#import "QMUIConfigurationTemplate.h" -/** - * QMUIConfigurationTemplate 是一份配置表,用于配合 QMUIKit 来管理整个 App 的全局样式,使用方式如下: - * 1. 在 QMUI 项目代码的文件夹里找到 QMUIConfigurationTemplate 目录,把里面所有文件复制到自己项目里。 - * 2. 在自己项目的 AppDelegate 里 #import "QMUIConfigurationTemplate.h",然后在 application:didFinishLaunchingWithOptions: 里调用 [QMUIConfigurationTemplate setupConfigurationTemplate],即可让配置表生效。 - * 3. 默认情况下配置表里的所有赋值都被注释,表示使用 QMUI 的默认值,你可以把你想修改的表达式取消注释,并改为想要的值即可。 - * 4. 注意如果修改了属性 A,则请搜索整个文件里所有用到 A 的地方,把那个地方的注释也打开,否则使用的是 A 在 QMUI 里的默认值,而不是你修改后的值。 - * 5. 更新 QMUIKit 的版本时,请留意 Release Log 里是否有提醒更新配置表,请尽量保持自己项目里的配置表与 QMUIKit 里的配置表一致,避免遗漏新的属性。 - */ -@interface QMUIConfigurationTemplateGrapefruit : NSObject +@interface QMUIConfigurationTemplateGrapefruit : QMUIConfigurationTemplate @end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.m b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.m index e17c3ba6..9356b92f 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.m +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrapefruit.m @@ -2,182 +2,26 @@ // QMUIConfigurationTemplate.m // qmui // -// Created by QQMail on 15/3/29. +// Created by QMUI Team on 15/3/29. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import "QMUIConfigurationTemplateGrapefruit.h" -#import @implementation QMUIConfigurationTemplateGrapefruit -- (void)setupConfigurationTemplate { - - // === 修改配置值 === // - -#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 : QMUINavigationButton 的字体 - QMUICMI.navBarButtonFontBold = UIFontBoldMake(17); // NavBarButtonFontBold : QMUINavigationButtonTypeBold 的字体 - QMUICMI.navBarBackgroundImage = [QDUIHelper navigationBarBackgroundImageWithThemeColor:self.themeTintColor]; // NavBarBackgroundImage : UINavigationBar 的背景图 - QMUICMI.navBarShadowImage = [UIImage new]; // 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.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; // NavBarBarBackButtonTitlePositionAdjustment : 导航栏返回按钮的文字偏移 - QMUICMI.navBarBackIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavBack size:CGSizeMake(12, 20) tintColor:NavBarTintColor]; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片 - 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:UIColorWhite]; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 - -#pragma mark - TabBar - - QMUICMI.tabBarBackgroundImage = [UIImage qmui_imageWithColor:UIColorMake(249, 249, 249)]; // TabBarBackgroundImage : UITabBar 的背景图 - 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 = UIColorWhite; // SearchBarTextFieldBackground : QMUISearchBar 里的文本框的背景颜色 - QMUICMI.searchBarTextFieldBorderColor = UIColorMake(205, 208, 210); // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色 - QMUICMI.searchBarBottomBorderColor = UIColorMake(205, 208, 210); // SearchBarBottomBorderColor : QMUISearchBar 底部分隔线颜色 - QMUICMI.searchBarBarTintColor = UIColorMake(247, 247, 247); // SearchBarBarTintColor : QMUISearchBar 的 barTintColor,也即背景色 - QMUICMI.searchBarTintColor = self.themeTintColor; // SearchBarTintColor : QMUISearchBar 的 tintColor,也即上面的操作控件的主题色 - QMUICMI.searchBarTextColor = UIColorBlack; // SearchBarTextColor : QMUISearchBar 里的文本框的文字颜色 - QMUICMI.searchBarPlaceholderColor = UIColorPlaceholder; // SearchBarPlaceholderColor : QMUISearchBar 里的文本框的 placeholder 颜色 - QMUICMI.searchBarSearchIconImage = nil; // SearchBarSearchIconImage : QMUISearchBar 里的放大镜 icon - QMUICMI.searchBarClearIconImage = nil; // SearchBarClearIconImage : QMUISearchBar 里的文本框输入文字时右边的清空按钮的图片 - QMUICMI.searchBarTextFieldCornerRadius = 2.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小 - -#pragma mark - TableView / TableViewCell - - 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 : 列表默认的 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.tableViewSectionHeaderHeight = 20; // TableViewSectionHeaderHeight : Plain 类型的 QMUITableView sectionHeader 的默认高度 - QMUICMI.tableViewSectionFooterHeight = 0; // TableViewSectionFooterHeight : Plain 类型的 QMUITableView sectionFooter 的默认高度 - 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.tableViewGroupedSectionHeaderHeight = 15; // TableViewGroupedSectionHeaderHeight : Grouped 类型的 QMUITableView sectionHeader 的默认高度 - QMUICMI.tableViewGroupedSectionFooterHeight = 1; // TableViewGroupedSectionFooterHeight : Grouped 类型的 QMUITableView sectionFooter 的默认高度 - QMUICMI.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, PreferredVarForDevices(20, 15, 15, 15), 8, PreferredVarForDevices(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 - QMUICMI.windowLevelQMUIImagePreviewView = UIWindowLevelStatusBar + 1.0; // UIWindowLevelQMUIImagePreviewView : QMUIImagePreviewViewController 里使用的 UIWindow 的 windowLevel - -#pragma mark - Others +#pragma mark - + +// QMUI 2.3.0 版本里,配置表新增这个方法,返回 YES 表示在 App 启动时要自动应用这份配置表。仅当你的 App 里存在多份配置表时,才需要把除默认配置表之外的其他配置表的返回值改为 NO。 +- (BOOL)shouldApplyTemplateAutomatically { + [QMUIThemeManagerCenter.defaultThemeManager addThemeIdentifier:self.themeName theme:self]; - 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.navigationBarHiddenInitially = NO; // NavigationBarHiddenInitially : QMUINavigationControllerDelegate preferredNavigationBarHidden 的初始值,默认为NO + NSString *selectedThemeIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeIdentifier]; + BOOL result = [selectedThemeIdentifier isEqualToString:self.themeName]; + if (result) { + QMUIThemeManagerCenter.defaultThemeManager.currentTheme = self; + } + return result; } #pragma mark - @@ -186,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 5fc0b861..e3ff403c 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.h +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.h @@ -1,21 +1,12 @@ // // QMUIConfigurationTemplate.h // -// Created by QQMail on 15/3/29. +// Created by QMUI Team on 15/3/29. // Copyright (c) 2015年 QMUI Team. All rights reserved. // -#import -#import "QDThemeProtocol.h" +#import "QMUIConfigurationTemplate.h" -/** - * QMUIConfigurationTemplate 是一份配置表,用于配合 QMUIKit 来管理整个 App 的全局样式,使用方式如下: - * 1. 在 QMUI 项目代码的文件夹里找到 QMUIConfigurationTemplate 目录,把里面所有文件复制到自己项目里。 - * 2. 在自己项目的 AppDelegate 里 #import "QMUIConfigurationTemplate.h",然后在 application:didFinishLaunchingWithOptions: 里调用 [QMUIConfigurationTemplate setupConfigurationTemplate],即可让配置表生效。 - * 3. 默认情况下配置表里的所有赋值都被注释,表示使用 QMUI 的默认值,你可以把你想修改的表达式取消注释,并改为想要的值即可。 - * 4. 注意如果修改了属性 A,则请搜索整个文件里所有用到 A 的地方,把那个地方的注释也打开,否则使用的是 A 在 QMUI 里的默认值,而不是你修改后的值。 - * 5. 更新 QMUIKit 的版本时,请留意 Release Log 里是否有提醒更新配置表,请尽量保持自己项目里的配置表与 QMUIKit 里的配置表一致,避免遗漏新的属性。 - */ -@interface QMUIConfigurationTemplateGrass : NSObject +@interface QMUIConfigurationTemplateGrass : QMUIConfigurationTemplate @end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.m b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.m index 99dc2ef4..24b02241 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.m +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplateGrass.m @@ -2,182 +2,26 @@ // QMUIConfigurationTemplate.m // qmui // -// Created by QQMail on 15/3/29. +// Created by QMUI Team on 15/3/29. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import "QMUIConfigurationTemplateGrass.h" -#import @implementation QMUIConfigurationTemplateGrass -- (void)setupConfigurationTemplate { - - // === 修改配置值 === // - -#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 : QMUINavigationButton 的字体 - QMUICMI.navBarButtonFontBold = UIFontBoldMake(17); // NavBarButtonFontBold : QMUINavigationButtonTypeBold 的字体 - QMUICMI.navBarBackgroundImage = [QDUIHelper navigationBarBackgroundImageWithThemeColor:self.themeTintColor]; // NavBarBackgroundImage : UINavigationBar 的背景图 - QMUICMI.navBarShadowImage = [UIImage new]; // 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.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; // NavBarBarBackButtonTitlePositionAdjustment : 导航栏返回按钮的文字偏移 - QMUICMI.navBarBackIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavBack size:CGSizeMake(12, 20) tintColor:NavBarTintColor]; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片 - 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:UIColorWhite]; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 - -#pragma mark - TabBar - - QMUICMI.tabBarBackgroundImage = [UIImage qmui_imageWithColor:UIColorMake(249, 249, 249)]; // TabBarBackgroundImage : UITabBar 的背景图 - 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 = UIColorWhite; // SearchBarTextFieldBackground : QMUISearchBar 里的文本框的背景颜色 - QMUICMI.searchBarTextFieldBorderColor = UIColorMake(205, 208, 210); // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色 - QMUICMI.searchBarBottomBorderColor = UIColorMake(205, 208, 210); // SearchBarBottomBorderColor : QMUISearchBar 底部分隔线颜色 - QMUICMI.searchBarBarTintColor = UIColorMake(247, 247, 247); // SearchBarBarTintColor : QMUISearchBar 的 barTintColor,也即背景色 - QMUICMI.searchBarTintColor = self.themeTintColor; // SearchBarTintColor : QMUISearchBar 的 tintColor,也即上面的操作控件的主题色 - QMUICMI.searchBarTextColor = UIColorBlack; // SearchBarTextColor : QMUISearchBar 里的文本框的文字颜色 - QMUICMI.searchBarPlaceholderColor = UIColorPlaceholder; // SearchBarPlaceholderColor : QMUISearchBar 里的文本框的 placeholder 颜色 - QMUICMI.searchBarSearchIconImage = nil; // SearchBarSearchIconImage : QMUISearchBar 里的放大镜 icon - QMUICMI.searchBarClearIconImage = nil; // SearchBarClearIconImage : QMUISearchBar 里的文本框输入文字时右边的清空按钮的图片 - QMUICMI.searchBarTextFieldCornerRadius = 2.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小 - -#pragma mark - TableView / TableViewCell - - 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 : 列表默认的 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.tableViewSectionHeaderHeight = 20; // TableViewSectionHeaderHeight : Plain 类型的 QMUITableView sectionHeader 的默认高度 - QMUICMI.tableViewSectionFooterHeight = 0; // TableViewSectionFooterHeight : Plain 类型的 QMUITableView sectionFooter 的默认高度 - 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.tableViewGroupedSectionHeaderHeight = 15; // TableViewGroupedSectionHeaderHeight : Grouped 类型的 QMUITableView sectionHeader 的默认高度 - QMUICMI.tableViewGroupedSectionFooterHeight = 1; // TableViewGroupedSectionFooterHeight : Grouped 类型的 QMUITableView sectionFooter 的默认高度 - QMUICMI.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, PreferredVarForDevices(20, 15, 15, 15), 8, PreferredVarForDevices(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 - QMUICMI.windowLevelQMUIImagePreviewView = UIWindowLevelStatusBar + 1.0; // UIWindowLevelQMUIImagePreviewView : QMUIImagePreviewViewController 里使用的 UIWindow 的 windowLevel - -#pragma mark - Others +#pragma mark - + +// QMUI 2.3.0 版本里,配置表新增这个方法,返回 YES 表示在 App 启动时要自动应用这份配置表。仅当你的 App 里存在多份配置表时,才需要把除默认配置表之外的其他配置表的返回值改为 NO。 +- (BOOL)shouldApplyTemplateAutomatically { + [QMUIThemeManagerCenter.defaultThemeManager addThemeIdentifier:self.themeName theme:self]; - 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.navigationBarHiddenInitially = NO; // NavigationBarHiddenInitially : QMUINavigationControllerDelegate preferredNavigationBarHidden 的初始值,默认为NO + NSString *selectedThemeIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeIdentifier]; + BOOL result = [selectedThemeIdentifier isEqualToString:self.themeName]; + if (result) { + QMUIThemeManagerCenter.defaultThemeManager.currentTheme = self; + } + return result; } #pragma mark - @@ -186,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 780e9194..0134c5d6 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.h +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.h @@ -1,21 +1,12 @@ // // QMUIConfigurationTemplate.h // -// Created by QQMail on 15/3/29. +// Created by QMUI Team on 15/3/29. // Copyright (c) 2015年 QMUI Team. All rights reserved. // -#import -#import "QDThemeProtocol.h" +#import "QMUIConfigurationTemplate.h" -/** - * QMUIConfigurationTemplate 是一份配置表,用于配合 QMUIKit 来管理整个 App 的全局样式,使用方式如下: - * 1. 在 QMUI 项目代码的文件夹里找到 QMUIConfigurationTemplate 目录,把里面所有文件复制到自己项目里。 - * 2. 在自己项目的 AppDelegate 里 #import "QMUIConfigurationTemplate.h",然后在 application:didFinishLaunchingWithOptions: 里调用 [QMUIConfigurationTemplate setupConfigurationTemplate],即可让配置表生效。 - * 3. 默认情况下配置表里的所有赋值都被注释,表示使用 QMUI 的默认值,你可以把你想修改的表达式取消注释,并改为想要的值即可。 - * 4. 注意如果修改了属性 A,则请搜索整个文件里所有用到 A 的地方,把那个地方的注释也打开,否则使用的是 A 在 QMUI 里的默认值,而不是你修改后的值。 - * 5. 更新 QMUIKit 的版本时,请留意 Release Log 里是否有提醒更新配置表,请尽量保持自己项目里的配置表与 QMUIKit 里的配置表一致,避免遗漏新的属性。 - */ -@interface QMUIConfigurationTemplatePinkRose : NSObject +@interface QMUIConfigurationTemplatePinkRose : QMUIConfigurationTemplate @end diff --git a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.m b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.m index cfac894a..04193dd8 100644 --- a/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.m +++ b/qmuidemo/Modules/Common/Configuration/QMUIConfigurationTemplatePinkRose.m @@ -2,182 +2,26 @@ // QMUIConfigurationTemplate.m // qmui // -// Created by QQMail on 15/3/29. +// Created by QMUI Team on 15/3/29. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import "QMUIConfigurationTemplatePinkRose.h" -#import @implementation QMUIConfigurationTemplatePinkRose -- (void)setupConfigurationTemplate { - - // === 修改配置值 === // - -#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 : QMUINavigationButton 的字体 - QMUICMI.navBarButtonFontBold = UIFontBoldMake(17); // NavBarButtonFontBold : QMUINavigationButtonTypeBold 的字体 - QMUICMI.navBarBackgroundImage = [QDUIHelper navigationBarBackgroundImageWithThemeColor:self.themeTintColor]; // NavBarBackgroundImage : UINavigationBar 的背景图 - QMUICMI.navBarShadowImage = [UIImage new]; // 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.navBarBackButtonTitlePositionAdjustment = UIOffsetZero; // NavBarBarBackButtonTitlePositionAdjustment : 导航栏返回按钮的文字偏移 - QMUICMI.navBarBackIndicatorImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavBack size:CGSizeMake(12, 20) tintColor:NavBarTintColor]; // NavBarBackIndicatorImage : 导航栏的返回按钮的图片 - 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:UIColorWhite]; // NavBarAccessoryViewTypeDisclosureIndicatorImage : QMUINavigationTitleView 右边箭头的图片 - -#pragma mark - TabBar - - QMUICMI.tabBarBackgroundImage = [UIImage qmui_imageWithColor:UIColorMake(249, 249, 249)]; // TabBarBackgroundImage : UITabBar 的背景图 - 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 = UIColorWhite; // SearchBarTextFieldBackground : QMUISearchBar 里的文本框的背景颜色 - QMUICMI.searchBarTextFieldBorderColor = UIColorMake(205, 208, 210); // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色 - QMUICMI.searchBarBottomBorderColor = UIColorMake(205, 208, 210); // SearchBarBottomBorderColor : QMUISearchBar 底部分隔线颜色 - QMUICMI.searchBarBarTintColor = UIColorMake(247, 247, 247); // SearchBarBarTintColor : QMUISearchBar 的 barTintColor,也即背景色 - QMUICMI.searchBarTintColor = self.themeTintColor; // SearchBarTintColor : QMUISearchBar 的 tintColor,也即上面的操作控件的主题色 - QMUICMI.searchBarTextColor = UIColorBlack; // SearchBarTextColor : QMUISearchBar 里的文本框的文字颜色 - QMUICMI.searchBarPlaceholderColor = UIColorPlaceholder; // SearchBarPlaceholderColor : QMUISearchBar 里的文本框的 placeholder 颜色 - QMUICMI.searchBarSearchIconImage = nil; // SearchBarSearchIconImage : QMUISearchBar 里的放大镜 icon - QMUICMI.searchBarClearIconImage = nil; // SearchBarClearIconImage : QMUISearchBar 里的文本框输入文字时右边的清空按钮的图片 - QMUICMI.searchBarTextFieldCornerRadius = 2.0; // SearchBarTextFieldCornerRadius : QMUISearchBar 里的文本框的圆角大小 - -#pragma mark - TableView / TableViewCell - - 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 : 列表默认的 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.tableViewSectionHeaderHeight = 20; // TableViewSectionHeaderHeight : Plain 类型的 QMUITableView sectionHeader 的默认高度 - QMUICMI.tableViewSectionFooterHeight = 0; // TableViewSectionFooterHeight : Plain 类型的 QMUITableView sectionFooter 的默认高度 - 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.tableViewGroupedSectionHeaderHeight = 15; // TableViewGroupedSectionHeaderHeight : Grouped 类型的 QMUITableView sectionHeader 的默认高度 - QMUICMI.tableViewGroupedSectionFooterHeight = 1; // TableViewGroupedSectionFooterHeight : Grouped 类型的 QMUITableView sectionFooter 的默认高度 - QMUICMI.tableViewGroupedSectionHeaderContentInset = UIEdgeInsetsMake(16, PreferredVarForDevices(20, 15, 15, 15), 8, PreferredVarForDevices(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 - QMUICMI.windowLevelQMUIImagePreviewView = UIWindowLevelStatusBar + 1.0; // UIWindowLevelQMUIImagePreviewView : QMUIImagePreviewViewController 里使用的 UIWindow 的 windowLevel - -#pragma mark - Others +#pragma mark - + +// QMUI 2.3.0 版本里,配置表新增这个方法,返回 YES 表示在 App 启动时要自动应用这份配置表。仅当你的 App 里存在多份配置表时,才需要把除默认配置表之外的其他配置表的返回值改为 NO。 +- (BOOL)shouldApplyTemplateAutomatically { + [QMUIThemeManagerCenter.defaultThemeManager addThemeIdentifier:self.themeName theme:self]; - 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.navigationBarHiddenInitially = NO; // NavigationBarHiddenInitially : QMUINavigationControllerDelegate preferredNavigationBarHidden 的初始值,默认为NO + NSString *selectedThemeIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:QDSelectedThemeIdentifier]; + BOOL result = [selectedThemeIdentifier isEqualToString:self.themeName]; + if (result) { + QMUIThemeManagerCenter.defaultThemeManager.currentTheme = self; + } + return result; } #pragma mark - @@ -186,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.h b/qmuidemo/Modules/Common/Controllers/QDCommonGridViewController.h index 4c143359..57aa00b7 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonGridViewController.h +++ b/qmuidemo/Modules/Common/Controllers/QDCommonGridViewController.h @@ -2,7 +2,7 @@ // QDCommonGridViewController.h // qmuidemo // -// Created by MoLice on 2016/10/10. +// Created by QMUI Team on 2016/10/10. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -10,7 +10,8 @@ @interface QDCommonGridViewController : QDCommonViewController -@property(nonatomic, strong) QMUIOrderedDictionary *dataSource; +@property(nonatomic, strong) QMUIOrderedDictionary *dataSource; +@property(nonatomic, strong, readonly) UIScrollView *scrollView; @property(nonatomic, strong, readonly) QMUIGridView *gridView; @end diff --git a/qmuidemo/Modules/Common/Controllers/QDCommonGridViewController.m b/qmuidemo/Modules/Common/Controllers/QDCommonGridViewController.m index 623cdc91..5d4d8302 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonGridViewController.m +++ b/qmuidemo/Modules/Common/Controllers/QDCommonGridViewController.m @@ -2,34 +2,27 @@ // QDCommonGridViewController.m // qmuidemo // -// Created by MoLice on 2016/10/10. +// Created by QMUI Team on 2016/10/10. // Copyright © 2016年 QMUI Team. All rights reserved. // #import "QDCommonGridViewController.h" -@interface QDCommonGridViewController () - -@property(nonatomic, strong) UIScrollView *scrollView; -@end - @interface QDCommonGridButton : QMUIButton @end @implementation QDCommonGridViewController -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self initDataSource]; - } - return self; +- (void)didInitialize { + [super didInitialize]; + [self initDataSource]; } - (void)initSubviews { [super initSubviews]; - self.scrollView = [[UIScrollView alloc] init]; + _scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; [self.view addSubview:self.scrollView]; _gridView = [[QMUIGridView alloc] init]; @@ -43,74 +36,66 @@ - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; self.scrollView.frame = self.view.bounds; - if (CGRectGetWidth(self.view.bounds) <= [QMUIHelper screenSizeFor55Inch].width) { + CGFloat gridViewWidth = CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.safeAreaInsets); + + if (CGRectGetWidth(self.view.bounds) <= [QMUIHelper screenSizeFor69Inch].width) { self.gridView.columnCount = 3; - CGFloat itemWidth = flat(CGRectGetWidth(self.scrollView.bounds) / self.gridView.columnCount); + CGFloat itemWidth = flat(gridViewWidth / self.gridView.columnCount); self.gridView.rowHeight = itemWidth; } else { - CGFloat minimumItemWidth = flat([QMUIHelper screenSizeFor55Inch].width / 3.0); - CGFloat maximumItemWidth = flat(CGRectGetWidth(self.view.bounds) / 5.0); - CGFloat freeSpacingWhenDisplayingMinimumCount = CGRectGetWidth(self.scrollView.bounds) / maximumItemWidth - floor(CGRectGetWidth(self.scrollView.bounds) / maximumItemWidth); - CGFloat freeSpacingWhenDisplayingMaximumCount = CGRectGetWidth(self.scrollView.bounds) / minimumItemWidth - floor(CGRectGetWidth(self.scrollView.bounds) / minimumItemWidth); + 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); if (freeSpacingWhenDisplayingMinimumCount < freeSpacingWhenDisplayingMaximumCount) { // 按每行最少item的情况来布局的话,空间利用率会更高,所以按最少item来 - self.gridView.columnCount = floor(CGRectGetWidth(self.scrollView.bounds) / maximumItemWidth); - CGFloat itemWidth = floor(CGRectGetWidth(self.scrollView.bounds) / self.gridView.columnCount); + self.gridView.columnCount = floor(gridViewWidth / maximumItemWidth); + CGFloat itemWidth = floor(gridViewWidth / self.gridView.columnCount); self.gridView.rowHeight = itemWidth; } else { - self.gridView.columnCount = floor(CGRectGetWidth(self.scrollView.bounds) / minimumItemWidth); - CGFloat itemWidth = floor(CGRectGetWidth(self.scrollView.bounds) / self.gridView.columnCount); + self.gridView.columnCount = floor(gridViewWidth / minimumItemWidth); + CGFloat itemWidth = floor(gridViewWidth / self.gridView.columnCount); self.gridView.rowHeight = itemWidth; } } - CGFloat gridViewHeight = [self.gridView sizeThatFits:CGSizeMake(CGRectGetWidth(self.scrollView.bounds), CGFLOAT_MAX)].height; - self.gridView.frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollView.bounds), gridViewHeight); - self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.scrollView.bounds), CGRectGetMaxY(self.gridView.frame)); + for (NSInteger i = 0; i < self.gridView.subviews.count; i++) { + UIView *item = self.gridView.subviews[i]; + item.qmui_borderPosition = QMUIViewBorderPositionLeft | QMUIViewBorderPositionTop; + if ((i % self.gridView.columnCount == self.gridView.columnCount - 1) || (i == self.gridView.subviews.count - 1)) { + // 每行最后一个,或者所有的最后一个(因为它可能不是所在行的最后一个) + item.qmui_borderPosition |= QMUIViewBorderPositionRight; + } + if (i + self.gridView.columnCount >= self.gridView.subviews.count) { + // 那些下方没有其他 item 的 item,底部都加个边框 + item.qmui_borderPosition |= QMUIViewBorderPositionBottom; + } + } + + 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) @@ -135,7 +120,6 @@ - (instancetype)initWithFrame:(CGRect)frame { self.titleLabel.numberOfLines = 2; self.highlightedBackgroundColor = TableViewCellSelectedBackgroundColor; self.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; - self.qmui_borderPosition = QMUIBorderViewPositionRight | QMUIImageBorderPositionBottom; } return self; } @@ -153,8 +137,7 @@ - (void)layoutSubviews { self.imageView.center = CGPointMake(center.x, center.y - 12); - CGSize titleLabelSize = [self.titleLabel sizeThatFits:contentSize]; - self.titleLabel.frame = CGRectFlatMake(self.contentEdgeInsets.left, center.y + PreferredVarForDevices(27, 27, 21, 21), contentSize.width, titleLabelSize.height); + self.titleLabel.frame = CGRectFlatMake(self.contentEdgeInsets.left, center.y + 27, contentSize.width, QMUIViewSelfSizingHeight); } @end diff --git a/qmuidemo/Modules/Common/Controllers/QDCommonGroupListViewController.h b/qmuidemo/Modules/Common/Controllers/QDCommonGroupListViewController.h index e21a1879..a1e1ee2a 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonGroupListViewController.h +++ b/qmuidemo/Modules/Common/Controllers/QDCommonGroupListViewController.h @@ -2,7 +2,7 @@ // QDCommonGroupListViewController.h // qmuidemo // -// Created by 李浩成 on 2016/10/10. +// Created by QMUI Team on 2016/10/10. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Common/Controllers/QDCommonGroupListViewController.m b/qmuidemo/Modules/Common/Controllers/QDCommonGroupListViewController.m index 0ac32e8f..7ab71023 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonGroupListViewController.m +++ b/qmuidemo/Modules/Common/Controllers/QDCommonGroupListViewController.m @@ -2,7 +2,7 @@ // QDCommonGroupListViewController.m // qmuidemo // -// Created by 李浩成 on 2016/10/10. +// Created by QMUI Team on 2016/10/10. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -14,18 +14,9 @@ - (instancetype)init { return [self initWithStyle:UITableViewStyleGrouped]; } -- (instancetype)initWithStyle:(UITableViewStyle)style { - if (self = [super initWithStyle:style]) { - [self initDataSource]; - } - return self; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - CGFloat contentInsetTop = IOS_VERSION >= 11.0 ? 0 : CGRectGetMaxY(self.navigationController.navigationBar.frame); - self.tableView.contentInset = UIEdgeInsetsMake(contentInsetTop - 35, 0, 0, 0); - self.tableView.scrollIndicatorInsets = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0); +- (void)didInitializeWithStyle:(UITableViewStyle)style { + [super didInitializeWithStyle:style]; + [self initDataSource]; } #pragma mark - @@ -46,7 +37,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N static NSString *identifierNormal = @"cellNormal"; QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifierNormal]; if (!cell) { - cell = [[QMUITableViewCell alloc] initForTableView:self.tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifierNormal]; + cell = [[QMUITableViewCell alloc] initForTableView:tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifierNormal]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; } NSString *keyName = [self keyNameAtIndexPath:indexPath]; diff --git a/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.h b/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.h index f89962d6..fb7022fb 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.h +++ b/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.h @@ -2,16 +2,20 @@ // QDCommonListViewController.h // qmuidemo // -// Created by ZhoonChen on 15/9/15. +// Created by QMUI Team on 15/9/15. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import "QDCommonTableViewController.h" +/** + 对于那种简单地展示一个列表,点击后跳去别的界面的场景,不需要继承,直接初始化后赋值 @c dataSource 、@c dataSourceWithDetailText ,并在 didSelectTitleBlock 里处理每一行的点击事件即可。 + */ @interface QDCommonListViewController : QDCommonTableViewController @property(nonatomic, strong) NSArray *dataSource; -@property(nonatomic, strong) QMUIOrderedDictionary *dataSourceWithDetailText; +@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 6ef60396..7e576f43 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.m +++ b/qmuidemo/Modules/Common/Controllers/QDCommonListViewController.m @@ -2,7 +2,7 @@ // QDCommonListViewController.m // qmuidemo // -// Created by ZhoonChen on 15/9/15. +// Created by QMUI Team on 15/9/15. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -24,17 +24,24 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - static NSString *identifierNormal = @"cellNormal"; - QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifierNormal]; + NSString *identifier = nil; + QMUITableViewCell *cell = nil; + if (self.dataSourceWithDetailText) { + identifier = @"subtitle"; + } else { + identifier = @"normal"; + } + cell = [tableView dequeueReusableCellWithIdentifier:identifier]; if (!cell) { - if (self.dataSourceWithDetailText) { - cell = [[QMUITableViewCell alloc] initForTableView:self.tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifierNormal]; + if ([identifier isEqualToString:@"subtitle"]) { + cell = [[QMUITableViewCell alloc] initForTableView:tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]; + cell.detailTextLabel.numberOfLines = 0; } else { - cell = [[QMUITableViewCell alloc] initForTableView:self.tableView withStyle:UITableViewCellStyleValue1 reuseIdentifier:identifierNormal]; + cell = [[QMUITableViewCell alloc] initForTableView:tableView withStyle:UITableViewCellStyleValue1 reuseIdentifier:identifier]; } cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; } - if (self.dataSourceWithDetailText) { + if ([identifier isEqualToString:@"subtitle"]) { NSString *keyName = self.dataSourceWithDetailText.allKeys[indexPath.row]; cell.textLabel.text = keyName; cell.detailTextLabel.text = (NSString *)[self.dataSourceWithDetailText objectForKey:keyName]; @@ -49,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; } @@ -61,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 2599bf99..95ebd944 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.h +++ b/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.h @@ -2,12 +2,10 @@ // QDCommonTableViewController.h // qmuidemo // -// Created by ZhoonChen on 15/4/13. +// Created by QMUI Team on 15/4/13. // 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 1dd87b04..c1463838 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.m +++ b/qmuidemo/Modules/Common/Controllers/QDCommonTableViewController.m @@ -2,7 +2,7 @@ // QDCommonTableViewController.m // qmuidemo // -// Created by ZhoonChen on 15/4/13. +// Created by QMUI Team on 15/4/13. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -10,24 +10,37 @@ @implementation QDCommonTableViewController -- (void)didInitialized { - [super didInitialized]; - [[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]; + } } -#pragma mark - +- (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; +} -- (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 a3764efe..b05b8dcb 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonViewController.h +++ b/qmuidemo/Modules/Common/Controllers/QDCommonViewController.h @@ -2,12 +2,10 @@ // QDCommonViewController.h // qmuidemo // -// Created by ZhoonChen on 15/4/13. +// Created by QMUI Team on 15/4/13. // 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 263dfcd6..ab01ecea 100644 --- a/qmuidemo/Modules/Common/Controllers/QDCommonViewController.m +++ b/qmuidemo/Modules/Common/Controllers/QDCommonViewController.m @@ -2,7 +2,7 @@ // QDCommonViewController.m // qmuidemo // -// Created by ZhoonChen on 15/4/13. +// Created by QMUI Team on 15/4/13. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -10,21 +10,33 @@ @implementation QDCommonViewController -- (void)didInitialized { - [super didInitialized]; - [[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]; + } } -#pragma mark - +- (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]; +} -- (void)themeBeforeChanged:(NSObject *)themeBeforeChanged afterChanged:(NSObject *)themeAfterChanged { - +- (BOOL)shouldCustomizeNavigationBarTransitionIfHideable { + return YES; } @end diff --git a/qmuidemo/Modules/Common/Controllers/QDNavigationController.h b/qmuidemo/Modules/Common/Controllers/QDNavigationController.h index 439d95fa..78f8b93e 100644 --- a/qmuidemo/Modules/Common/Controllers/QDNavigationController.h +++ b/qmuidemo/Modules/Common/Controllers/QDNavigationController.h @@ -2,7 +2,7 @@ // QDNavigationController.h // qmuidemo // -// Created by ZhoonChen on 15/4/13. +// Created by QMUI Team on 15/4/13. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Common/Controllers/QDNavigationController.m b/qmuidemo/Modules/Common/Controllers/QDNavigationController.m index 3087f695..9b490ed7 100644 --- a/qmuidemo/Modules/Common/Controllers/QDNavigationController.m +++ b/qmuidemo/Modules/Common/Controllers/QDNavigationController.m @@ -2,7 +2,7 @@ // QDNavigationController.m // qmuidemo // -// Created by ZhoonChen on 15/4/13. +// Created by QMUI Team on 15/4/13. // Copyright (c) 2015年 QMUI Team. All rights reserved. // 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/Controllers/QDTabBarViewController.h b/qmuidemo/Modules/Common/Controllers/QDTabBarViewController.h index 1fab78e1..8851976d 100644 --- a/qmuidemo/Modules/Common/Controllers/QDTabBarViewController.h +++ b/qmuidemo/Modules/Common/Controllers/QDTabBarViewController.h @@ -2,7 +2,7 @@ // QDTabBarViewController.h // qmuidemo // -// Created by ZhoonChen on 15/6/2. +// Created by QMUI Team on 15/6/2. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Common/Controllers/QDTabBarViewController.m b/qmuidemo/Modules/Common/Controllers/QDTabBarViewController.m index ba151317..dbbd53e9 100644 --- a/qmuidemo/Modules/Common/Controllers/QDTabBarViewController.m +++ b/qmuidemo/Modules/Common/Controllers/QDTabBarViewController.m @@ -2,7 +2,7 @@ // QDTabBarViewController.m // qmuidemo // -// Created by ZhoonChen on 15/6/2. +// Created by QMUI Team on 15/6/2. // Copyright (c) 2015年 QMUI Team. All rights reserved. // 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 83734900..37458b41 100644 --- a/qmuidemo/Modules/Common/Utils/QDCommonUI.h +++ b/qmuidemo/Modules/Common/Utils/QDCommonUI.h @@ -2,7 +2,7 @@ // QDCommonUI.h // qmuidemo // -// Created by MoLice on 16/8/8. +// Created by QMUI Team on 16/8/8. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -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 c592e7b8..98468e53 100644 --- a/qmuidemo/Modules/Common/Utils/QDCommonUI.m +++ b/qmuidemo/Modules/Common/Utils/QDCommonUI.m @@ -2,22 +2,70 @@ // QDCommonUI.m // qmuidemo // -// Created by MoLice on 16/8/8. +// Created by QMUI Team on 16/8/8. // Copyright © 2016年 QMUI Team. All rights reserved. // #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 @@ -35,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 a7b78fc0..ab88a7bc 100644 --- a/qmuidemo/Modules/Common/Utils/QDUIHelper.h +++ b/qmuidemo/Modules/Common/Utils/QDUIHelper.h @@ -2,7 +2,7 @@ // QDUIHelper.h // qmuidemo // -// Created by ZhoonChen on 15/6/2. +// Created by QMUI Team on 15/6/2. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -10,8 +10,6 @@ @interface QDUIHelper : NSObject -+ (void)forceInterfaceOrientationPortrait; - @end @@ -28,6 +26,30 @@ @end +@interface QDUIHelper (QMUIDialogViewControllerAppearance) + ++ (void)customDialogViewControllerAppearance; + +@end + + +@interface QDUIHelper (QMUIEmotionView) + ++ (void)customEmotionViewAppearance; +@end + + +@interface QDUIHelper (QMUIImagePicker) + ++ (void)customImagePickerAppearance; + +@end + +@interface QDUIHelper (QMUIPopupContainerView) + ++ (void)customPopupAppearance; +@end + @interface QDUIHelper (UITabBarItem) @@ -40,14 +62,16 @@ + (QMUIButton *)generateDarkFilledButton; + (QMUIButton *)generateLightBorderedButton; - ++ (QMUIButton *)generateGhostButtonWithColor:(UIColor *)color; @end -@interface NSString (Code) +@interface QDUIHelper (Emotion) -- (void)enumerateCodeStringUsingBlock:(void (^)(NSString *codeString, NSRange codeRange))block; ++ (NSArray *)qmuiEmotions; +/// 用于主题更新后,更新表情 icon 的颜色 ++ (void)updateEmotionImages; @end @@ -69,3 +93,19 @@ + (UIImage *)navigationBarBackgroundImageWithThemeColor:(UIColor *)color; @end + +@class QMUIInteractiveDebugPanelViewController; +@class QMUIInteractiveDebugPanelItem; + +@interface QDUIHelper (Debug) + ++ (QMUIInteractiveDebugPanelViewController *)generateDebugViewControllerWithTitle:(NSString *)title items:(NSArray *)items; +@end + + +@interface NSString (Code) + +- (void)enumerateCodeStringUsingBlock:(void (^)(NSString *codeString, NSRange codeRange))block; + +@end + diff --git a/qmuidemo/Modules/Common/Utils/QDUIHelper.m b/qmuidemo/Modules/Common/Utils/QDUIHelper.m index 256dd7bd..212f7cc9 100644 --- a/qmuidemo/Modules/Common/Utils/QDUIHelper.m +++ b/qmuidemo/Modules/Common/Utils/QDUIHelper.m @@ -2,16 +2,35 @@ // QDUIHelper.m // qmuidemo // -// Created by ZhoonChen on 15/6/2. +// Created by QMUI Team on 15/6/2. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #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,6 +40,7 @@ @implementation QDUIHelper (QMUIMoreOperationAppearance) + (void)customMoreOperationAppearance { // 如果需要统一修改全局的 QMUIMoreOperationController 样式,在这里修改 appearance 的值即可 + [QMUIMoreOperationController appearance].cancelButtonTitleColor = UIColor.qd_tintColor; } @end @@ -34,6 +54,80 @@ + (void)customAlertControllerAppearance { @end +@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] = UIColor.qd_tintColor; + appearance.buttonTitleAttributes = [buttonTitleAttributes copy]; + + appearance.buttonHighlightedBackgroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.25]; +} + +@end + + +@implementation QDUIHelper (QMUIEmotionView) + ++ (void)customEmotionViewAppearance { + [QMUIEmotionView appearance].emotionSize = CGSizeMake(24, 24); + [QMUIEmotionView appearance].minimumEmotionHorizontalSpacing = 14; + [QMUIEmotionView appearance].sendButtonBackgroundColor = UIColor.qd_tintColor; +} + +@end + +@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: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 @implementation QDUIHelper (UITabBarItem) @@ -52,42 +146,102 @@ + (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; } @end -@implementation NSString (Code) +@implementation QDUIHelper (Emotion) + +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; + ++ (NSArray *)qmuiEmotions { + if (QMUIEmotionArray) { + return QMUIEmotionArray; + } + + NSMutableArray *emotions = [[NSMutableArray alloc] init]; + NSArray *emotionStringArray = [QMUIEmotionString componentsSeparatedByString:@";"]; + for (NSString *emotionString in emotionStringArray) { + NSArray *emotionItem = [emotionString componentsSeparatedByString:@"-"]; + NSString *identifier = [NSString stringWithFormat:@"emotion_%@", emotionItem.firstObject]; + QMUIEmotion *emotion = [QMUIEmotion emotionWithIdentifier:identifier displayName:emotionItem.lastObject]; + [emotions addObject:emotion]; + } + + QMUIEmotionArray = [NSArray arrayWithArray:emotions]; + [self asyncLoadImages:emotions]; + return QMUIEmotionArray; +} -- (void)enumerateCodeStringUsingBlock:(void (^)(NSString *, NSRange))block { - NSString *pattern = @"\\[?[A-Za-z0-9_.]+\\s?[A-Za-z0-9_:.]+\\]?"; - NSError *error = nil; - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error]; - [regex enumerateMatchesInString:self options:NSMatchingReportCompletion range:NSMakeRange(0, self.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) { - if (result.range.length > 0) { - if (block) { - block([self substringWithRange:result.range], result.range); - } +// 在子线程预加载 ++ (void)asyncLoadImages:(NSArray *)emotions { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + for (QMUIEmotion *e in emotions) { + e.image = [UIImageMake(e.identifier) qmui_imageWithBlendColor:UIColor.qd_tintColor]; } - }]; + }); +} + ++ (void)updateEmotionImages { + [self asyncLoadImages:[self qmuiEmotions]]; } @end @@ -96,13 +250,15 @@ - (void)enumerateCodeStringUsingBlock:(void (^)(NSString *, NSRange))block { @implementation QDUIHelper (SavePhoto) + (void)showAlertWhenSavedPhotoFailureByPermissionDenied { - QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"无法保存" message:@"你未开启“允许 QMUI 访问照片”选项" preferredStyle:QMUIAlertControllerStyleAlert]; + NSString *tipString = nil; + NSDictionary *mainInfoDictionary = [[NSBundle mainBundle] infoDictionary]; + NSString *appName = [mainInfoDictionary objectForKey:@"CFBundleDisplayName"]; + if (!appName) { + appName = [mainInfoDictionary objectForKey:(NSString *)kCFBundleNameKey]; + } + tipString = [NSString stringWithFormat:@"请在设备的\"设置-隐私-照片\"选项中,允许%@访问你的手机相册", appName]; - QMUIAlertAction *settingAction = [QMUIAlertAction actionWithTitle:@"去设置" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertAction *action) { - NSURL *url = [[NSURL alloc] initWithString:@"prefs:root=Privacy&path=PHOTOS"]; - [[UIApplication sharedApplication] openURL:url]; - }]; - [alertController addAction:settingAction]; + QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"无法保存" message:tipString preferredStyle:QMUIAlertControllerStyleAlert]; QMUIAlertAction *okAction = [QMUIAlertAction actionWithTitle:@"我知道了" style:QMUIAlertActionStyleCancel handler:nil]; [alertController addAction:okAction]; @@ -133,18 +289,63 @@ + (NSString *)humanReadableFileSize:(long long)size { @implementation QDUIHelper (Theme) + (UIImage *)navigationBarBackgroundImageWithThemeColor:(UIColor *)color { - CGSize size = CGSizeMake(4, 64); - UIImage *resultImage = nil; + CGSize size = CGSizeMake(4, 88); color = color ? color : UIColorClear; - UIGraphicsBeginImageContextWithOptions(size, YES, 0); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGGradientRef gradient = CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), (CFArrayRef)@[(id)color.CGColor, (id)[color qmui_colorWithAlphaAddedToWhite:.86].CGColor], NULL); - CGContextDrawLinearGradient(context, gradient, CGPointZero, CGPointMake(0, size.height), kCGGradientDrawsBeforeStartLocation); - - resultImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return [resultImage resizableImageWithCapInsets:UIEdgeInsetsMake(0, 1, 0, 1)]; + UIImage *resultImage = [UIImage qmui_imageWithSize:size opaque:YES scale:0 actions:^(CGContextRef contextRef) { + CGColorSpaceRef spaceRef = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColors(spaceRef, (CFArrayRef)@[(id)color.CGColor, (id)[color qmui_colorWithAlphaAddedToWhite:.86].CGColor], NULL); + CGContextDrawLinearGradient(contextRef, gradient, CGPointZero, CGPointMake(0, size.height), kCGGradientDrawsBeforeStartLocation); + CGColorSpaceRelease(spaceRef); + CGGradientRelease(gradient); + }]; + return [resultImage resizableImageWithCapInsets:UIEdgeInsetsMake(0, 1, 0, 1) resizingMode:UIImageResizingModeStretch]; +} + +@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) + +- (void)enumerateCodeStringUsingBlock:(void (^)(NSString *, NSRange))block { + NSString *pattern = @"\\[?[A-Za-z0-9_.\\(]+\\s?[A-Za-z0-9_:.\\)]+\\]?"; + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error]; + [regex enumerateMatchesInString:self options:NSMatchingReportCompletion range:NSMakeRange(0, self.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) { + if (result.range.length > 0) { + if (block) { + block([self substringWithRange:result.range], result.range); + } + } + }]; } @end 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/QDAboutViewController.h b/qmuidemo/Modules/Demos/About/QDAboutViewController.h index 5f3135d4..2577d687 100644 --- a/qmuidemo/Modules/Demos/About/QDAboutViewController.h +++ b/qmuidemo/Modules/Demos/About/QDAboutViewController.h @@ -2,7 +2,7 @@ // QDAboutViewController.h // qmuidemo // -// Created by MoLice on 2016/11/5. +// Created by QMUI Team on 2016/11/5. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/About/QDAboutViewController.m b/qmuidemo/Modules/Demos/About/QDAboutViewController.m index fb0e1c4e..edc8b3c8 100644 --- a/qmuidemo/Modules/Demos/About/QDAboutViewController.m +++ b/qmuidemo/Modules/Demos/About/QDAboutViewController.m @@ -2,7 +2,7 @@ // QDAboutViewController.m // qmuidemo // -// Created by MoLice on 2016/11/5. +// Created by QMUI Team on 2016/11/5. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -10,97 +10,57 @@ @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; @end @implementation QDAboutViewController -- (void)didInitialized { - [super didInitialized]; - - NSString *imagePath = [[NSUserDefaults standardUserDefaults] objectForKey:[self userDefaultsKeyForAboutLogoImage]]; - if (imagePath) { - UIImage *aboutLogoImage = [UIImage imageWithContentsOfFile:imagePath]; - if (aboutLogoImage) { - self.themeAboutLogoImage = aboutLogoImage; - return; - } - } - - dispatch_async(dispatch_get_main_queue(), ^{ - UIImage *aboutLogoImage = UIImageMake(@"about_logo_monochrome"); - UIImage *blendedAboutLogoImage = [aboutLogoImage qmui_imageWithBlendColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor]; - [self saveImageAsFile:blendedAboutLogoImage]; - self.themeAboutLogoImage = blendedAboutLogoImage; - }); -} - - (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]; 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 = QMUIBorderViewPositionTop; - [self.websiteButton addTarget:self action:@selector(handleWebsiteButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.scrollView addSubview:self.websiteButton]; - - self.documentButton = [self generateCellButtonWithTitle:@"功能列表"]; - self.documentButton.qmui_borderPosition = QMUIBorderViewPositionTop; - [self.documentButton addTarget:self action:@selector(handleDocumentButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.scrollView addSubview:self.documentButton]; - self.gitHubButton = [self generateCellButtonWithTitle:@"GitHub"]; - self.gitHubButton.qmui_borderPosition = QMUIBorderViewPositionTop | QMUIBorderViewPositionBottom; + self.gitHubButton.qmui_borderPosition = QMUIViewBorderPositionTop | QMUIViewBorderPositionBottom; [self.gitHubButton addTarget:self action:@selector(handleGitHubButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; [self.scrollView addSubview:self.gitHubButton]; self.copyrightLabel = [[UILabel alloc] init]; self.copyrightLabel.numberOfLines = 0; - self.copyrightLabel.attributedText = [[NSAttributedString alloc] initWithString:@"© 2017 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]; } -- (void)viewDidAppear:(BOOL)animated { - [super viewDidAppear:animated]; - if (self.themeAboutLogoImage && 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)setupNavigationItems { + [super setupNavigationItems]; + self.title = @"关于"; } -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; - self.title = @"关于"; +- (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 { @@ -118,80 +78,61 @@ - (QMUIButton *)generateCellButtonWithTitle:(NSString *)title { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - CGFloat tabBarHeight = 0; - UIEdgeInsets padding = UIEdgeInsetsMake(24, 24, 24, 24); + CGFloat navigationBarHeight = self.qmui_navigationBarMaxYInViewCoordinator; CGFloat versionLabelMarginTop = 10; CGFloat buttonHeight = TableViewCellNormalHeight; - self.scrollView.frame = CGRectSetHeight(self.view.bounds, CGRectGetHeight(self.view.bounds) - tabBarHeight); + UIEdgeInsets padding = UIEdgeInsetsMake(24, 24, 24, 24); + if (self.view.safeAreaInsets.bottom > 0) { + padding.bottom = padding.bottom - 20; + } + + self.scrollView.frame = self.view.bounds; if (IS_IPHONE && IS_LANDSCAPE) { CGFloat leftWidth = flat(CGRectGetWidth(self.scrollView.bounds) / 2); CGFloat rightWidth = CGRectGetWidth(self.scrollView.bounds) - leftWidth; CGFloat leftHeight = CGRectGetHeight(self.logoImageView.frame) + versionLabelMarginTop + CGRectGetHeight(self.versionButton.frame); - CGFloat leftMinY = CGFloatGetCenter(CGRectGetHeight(self.scrollView.bounds) - CGRectGetMaxY(self.navigationController.navigationBar.frame), leftHeight); + CGFloat leftMinY = CGFloatGetCenter(CGRectGetHeight(self.scrollView.bounds) - navigationBarHeight, leftHeight); self.logoImageView.frame = CGRectSetXY(self.logoImageView.frame, CGFloatGetCenter(leftWidth, CGRectGetHeight(self.logoImageView.frame)), leftMinY); 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) - CGRectGetMaxY(self.navigationController.navigationBar.frame) - 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) - CGRectGetMaxY(self.navigationController.navigationBar.frame)); + self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds) - UIEdgeInsetsGetVerticalValue(self.scrollView.adjustedContentInset)); } else { - CGFloat containerHeight = CGRectGetHeight(self.scrollView.bounds) - 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; + if (DEVICE_HEIGHT <= [QMUIHelper screenSizeFor40Inch].height) { + buttonMarginTop -= 8; + buttonHeight -= 8; + } + self.logoImageView.frame = CGRectSetXY(self.logoImageView.frame, CGRectGetMinXHorizontallyCenterInParentRect(self.scrollView.bounds, self.logoImageView.frame), mainContentMinY); 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) - CGRectGetMaxY(self.navigationController.navigationBar.frame) - 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/QMUI/QMUI_iOS/releases"]; -} - -- (void)handleWebsiteButtonEvent:(QMUIButton *)button { - [self openUrlString:@"http://www.qmuiteam.com/ios"]; -} - -- (void)handleDocumentButtonEvent:(QMUIButton *)button { - [self openUrlString:@"http://qmuiteam.com/ios/page/document.html"]; + [self openUrlString:@"https://github.com/Tencent/QMUI_iOS/releases"]; } - (void)handleGitHubButtonEvent:(QMUIButton *)button { @@ -199,17 +140,9 @@ - (void)handleGitHubButtonEvent:(QMUIButton *)button { } - (void)openUrlString:(NSString *)urlString { - UIApplication *application = [UIApplication sharedApplication]; + UIApplication *application = UIApplication.sharedApplication; NSURL *url = [NSURL URLWithString:urlString]; -#ifdef IOS10_SDK_ALLOWED - if ([application respondsToSelector:@selector(openURL:options:completionHandler:)]) { - [application openURL:url options:@{} completionHandler:nil]; - } else { - [application openURL:url]; - } -#else - [application openURL:url]; -#endif + [application openURL:url options:@{} completionHandler:nil]; } @end diff --git a/qmuidemo/Modules/Demos/Components/QDAssetsManagerViewController.h b/qmuidemo/Modules/Demos/Components/QDAssetsManagerViewController.h index 54bd0ba0..ae8b319a 100644 --- a/qmuidemo/Modules/Demos/Components/QDAssetsManagerViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDAssetsManagerViewController.h @@ -2,7 +2,7 @@ // QDAssetsManagerViewController.h // qmuidemo // -// Created by Kayo Lee on 15/6/9. +// Created by QMUI Team on 15/6/9. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDAssetsManagerViewController.m b/qmuidemo/Modules/Demos/Components/QDAssetsManagerViewController.m index baac7867..9494fa2b 100644 --- a/qmuidemo/Modules/Demos/Components/QDAssetsManagerViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDAssetsManagerViewController.m @@ -2,7 +2,7 @@ // QDAssetsManagerViewController.m // qmuidemo // -// Created by Kayo Lee on 15/6/9. +// Created by QMUI Team on 15/6/9. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -16,8 +16,8 @@ @interface QDAssetsManagerViewController () @implementation QDAssetsManagerViewController -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; +- (void)setupNavigationItems { + [super setupNavigationItems]; } - (void)initDataSource { @@ -41,14 +41,4 @@ - (void)didSelectCellWithTitle:(NSString *)title { } } -- (void)viewDidLoad { - [super viewDidLoad]; - // Do any additional setup after loading the view. -} - -- (void)didReceiveMemoryWarning { - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - @end diff --git a/qmuidemo/Modules/Demos/Components/QDBadgeViewController.h b/qmuidemo/Modules/Demos/Components/QDBadgeViewController.h new file mode 100644 index 00000000..7ecf4944 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDBadgeViewController.h @@ -0,0 +1,13 @@ +// +// QDBadgeViewController.h +// qmuidemo +// +// Created by QMUI Team on 2018/6/2. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import "QDCommonGroupListViewController.h" + +@interface QDBadgeViewController : QDCommonGroupListViewController + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDBadgeViewController.m b/qmuidemo/Modules/Demos/Components/QDBadgeViewController.m new file mode 100644 index 00000000..f073045d --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDBadgeViewController.m @@ -0,0 +1,140 @@ +// +// QDBadgeViewController.m +// qmuidemo +// +// Created by QMUI Team on 2018/6/2. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import "QDBadgeViewController.h" + +@interface QDBadgeViewController () + +@property(nonatomic, strong) UIToolbar *toolbar; +@property(nonatomic, strong) UITabBar *tabBar; +@end + +@implementation QDBadgeViewController + +- (void)setupNavigationItems { + [super setupNavigationItems]; + 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]; + + UITabBarItem *item1 = [QDUIHelper tabBarItemWithTitle:@"QMUIKit" image:[UIImageMake(@"icon_tabbar_uikit") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_uikit_selected") tag:0]; + + UITabBarItem *item2 = [QDUIHelper tabBarItemWithTitle:@"Components" image:[UIImageMake(@"icon_tabbar_component") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_component_selected") tag:1]; + item2.qmui_badgeString = @"99+";// 支持字符串 + + UITabBarItem *item3 = [QDUIHelper tabBarItemWithTitle:@"Lab" image:[UIImageMake(@"icon_tabbar_lab") imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] selectedImage:UIImageMake(@"icon_tabbar_lab_selected") tag:2]; + + self.tabBar.items = @[item1, item2, item3]; + self.tabBar.selectedItem = item1; + [self.tabBar sizeToFit]; + [self.view addSubview:self.tabBar]; +} + +- (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 = [[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:@"在 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.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; + item.qmui_shouldShowUpdatesIndicator = YES; + item.qmui_badgeInteger = 0; + + } else if ([title isEqualToString:@"在 UITabBarItem 上显示未读数"]) { + + UITabBarItem *item = self.tabBar.items.firstObject; + item.qmui_shouldShowUpdatesIndicator = NO; + item.qmui_badgeInteger = 12; + + } else if ([title isEqualToString:@"修改红点/未读数的样式(多点几次试试)"]) { + + UITabBarItem *item = self.tabBar.items[1]; + item.qmui_badgeString = @"99+";// 支持字符串 + item.qmui_badgeBackgroundColor = [QDCommonUI randomThemeColor]; + + } else if ([title isEqualToString:@"隐藏所有红点、未读数"]) { + + 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; + }]; + + } + [self.tableView qmui_clearsSelection]; +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDCellHeightCacheViewController.h b/qmuidemo/Modules/Demos/Components/QDCellHeightCacheViewController.h new file mode 100644 index 00000000..24a073cc --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCellHeightCacheViewController.h @@ -0,0 +1,18 @@ +// +// QDCellHeightCacheViewController.h +// qmuidemo +// +// Created by MoLice on 2019/J/9. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDCommonTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +/// 对应 QMUICellHeightCache.h 的示例 Demo,只要求 cell 在 sizeThatFits: 里返回当前的大小,不要求 tableView 开启 estimatedRowHeight 效果 +@interface QDCellHeightCacheViewController : QDCommonTableViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Components/QDCellHeightCacheViewController.m b/qmuidemo/Modules/Demos/Components/QDCellHeightCacheViewController.m new file mode 100644 index 00000000..bb3b1d9f --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCellHeightCacheViewController.m @@ -0,0 +1,116 @@ +// +// QDCellHeightCacheViewController.m +// qmuidemo +// +// Created by MoLice on 2019/J/9. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDCellHeightCacheViewController.h" +#import "QDDynamicHeightTableViewCell.h" + +static NSString * const kCellIdentifier = @"cell"; + +@interface QDCellHeightCacheViewController () + +@property(nonatomic, strong) QMUIOrderedDictionary *dataSource; +@end + +@implementation QDCellHeightCacheViewController + +- (void)didInitializeWithStyle:(UITableViewStyle)style { + [super didInitializeWithStyle:style]; + self.dataSource = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"张三 的想法", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。", + @"李四 的想法", @"UIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。", + @"王五 的想法", @"高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + @"QMUI Team 的想法", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。\nUIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。\n高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + + @"张三 的想法1", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。", + @"李四 的想法1", @"UIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。", + @"王五 的想法1", @"高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + @"QMUI Team 的想法1", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。\nUIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。\n高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + + @"张三 的想法2", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。", + @"李四 的想法2", @"UIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。", + @"王五 的想法2", @"高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + @"QMUI Team 的想法2", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。\nUIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。\n高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + + @"张三 的想法3", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。", + @"李四 的想法3", @"UIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。", + @"王五 的想法3", @"高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + @"QMUI Team 的想法3", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。\nUIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。\n高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + nil]; +} + +- (void)initTableView { + [super initTableView]; + // 为了展示效果,这里主动把 estimatedRowHeight 关闭,以与 QMUICellHeightKeyCache 区分开,如果你的 tableView 使用了 estimatedRowHeight,请使用 QMUICellHeightKeyCache,用法参考 QDCellHeightKeyCacheViewController + self.tableView.estimatedRowHeight = 0; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Reload" style:UIBarButtonItemStyleDone target:self action:@selector(handleRightBarButtonItem)]; +} + +- (void)handleRightBarButtonItem { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:1 inSection:0]; + id cachedKey = [self cachedKeyAtIndexPath:indexPath]; + + // 1. 模拟业务场景某个 indexPath 的数据发生变化 + [self.dataSource setObject:@"变化后的内容" forKey:self.dataSource.allKeys[indexPath.row]]; + + // 2. 在更新 UI 之前先令对应的缓存失效 + [self.tableView qmui_invalidateHeightForKey:cachedKey]; + + // 3. 更新 UI + [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; +} + +- (id)cachedKeyAtIndexPath:(NSIndexPath *)indexPath { + NSString *keyName = self.dataSource.allKeys[indexPath.row]; + NSString *contentText = [self.dataSource objectForKey:keyName]; + return @(contentText.length);// 这里简单处理,认为只要长度不同,高度就不同(但实际情况下长度就算相同,高度也有可能不同,要注意) +} + +#pragma mark - + +- (UITableViewCell *)qmui_tableView:(UITableView *)tableView cellWithIdentifier:(NSString *)identifier { + if ([identifier isEqualToString:kCellIdentifier]) { + QDDynamicHeightTableViewCell *cell = (QDDynamicHeightTableViewCell *)[tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[QDDynamicHeightTableViewCell alloc] initForTableView:tableView withReuseIdentifier:kCellIdentifier]; + } + cell.separatorInset = UIEdgeInsetsZero; + return cell; + } + return nil; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.dataSource.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + QDDynamicHeightTableViewCell *cell = (QDDynamicHeightTableViewCell *)[self qmui_tableView:tableView cellWithIdentifier:kCellIdentifier]; + NSString *keyName = self.dataSource.allKeys[indexPath.row]; + [cell updateCellAppearanceWithIndexPath:indexPath]; + [cell renderWithNameText:[NSString stringWithFormat:@"%@ - %@", @(indexPath.row), keyName] contentText:[self.dataSource objectForKey:keyName]]; + return cell; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + id cachedKey = [self cachedKeyAtIndexPath:indexPath]; + NSString *keyName = self.dataSource.allKeys[indexPath.row]; + return [tableView qmui_heightForCellWithIdentifier:kCellIdentifier cacheByKey:cachedKey configuration:^(QDDynamicHeightTableViewCell *cell) { + [cell updateCellAppearanceWithIndexPath:indexPath]; + [cell renderWithNameText:[NSString stringWithFormat:@"%@ - %@", @(indexPath.row), keyName] contentText:[self.dataSource objectForKey:keyName]]; + }]; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [self.tableView qmui_clearsSelection]; +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDCellHeightKeyCacheViewController.h b/qmuidemo/Modules/Demos/Components/QDCellHeightKeyCacheViewController.h new file mode 100644 index 00000000..0cef812e --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCellHeightKeyCacheViewController.h @@ -0,0 +1,15 @@ +// +// QDTableViewCellDynamicHeightViewController.h +// qmuidemo +// +// Created by QMUI Team on 2018/03/16. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import +#import "QDCommonTableViewController.h" + +/// 对应 UITableView (QMUICellHeightKeyCache) 的示例 Demo,要求 tableView 开启 estimatedRowHeight 并且 cell 在 sizeThatFits: 里返回当前的大小 +@interface QDCellHeightKeyCacheViewController : QDCommonTableViewController + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDCellHeightKeyCacheViewController.m b/qmuidemo/Modules/Demos/Components/QDCellHeightKeyCacheViewController.m new file mode 100644 index 00000000..ea411e1f --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCellHeightKeyCacheViewController.m @@ -0,0 +1,115 @@ +// +// QDTableViewCellDynamicHeightViewController.m +// qmuidemo +// +// Created by QMUI Team on 2018/03/16. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import "QDCellHeightKeyCacheViewController.h" +#import "QDDynamicHeightTableViewCell.h" + +static NSString * const kCellIdentifier = @"cell"; + +@interface QDCellHeightKeyCacheViewController () + +@property(nonatomic, strong) QMUIOrderedDictionary *dataSource; +@end + +@implementation QDCellHeightKeyCacheViewController + +- (void)didInitializeWithStyle:(UITableViewStyle)style { + [super didInitializeWithStyle:style]; + self.dataSource = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"固定高度", @"这一行的高度在 tableView:heightForRowAtIndexPath: 里控制,不经过 sizeThatFits:", + @"李四 的想法", @"UIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。", + @"王五 的想法", @"高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + @"QMUI Team 的想法", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。\nUIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。\n高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + + @"张三 的想法1", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。", + @"李四 的想法1", @"UIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。", + @"王五 的想法1", @"高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + @"QMUI Team 的想法1", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。\nUIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。\n高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + + @"张三 的想法2", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。", + @"李四 的想法2", @"UIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。", + @"王五 的想法2", @"高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + @"QMUI Team 的想法2", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。\nUIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。\n高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + + @"张三 的想法3", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。", + @"李四 的想法3", @"UIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。", + @"王五 的想法3", @"高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + @"QMUI Team 的想法3", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。\nUIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。\n高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", + nil]; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Reload" style:UIBarButtonItemStyleDone target:self action:@selector(handleRightBarButtonItem)]; +} + +- (void)initTableView { + [super initTableView]; + // 如果需要自动缓存 cell 高度的计算结果,则打开这个属性,然后实现 - [QMUITableViewDelegate qmui_tableView:cacheKeyForRowAtIndexPath:] 方法即可 + // 请自行确保 tableView 的 estimated height 生效。 + self.tableView.qmui_cacheCellHeightByKeyAutomatically = YES; +} + +- (void)handleRightBarButtonItem { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:3 inSection:0]; + id cachedKey = [self.tableView.delegate qmui_tableView:self.tableView cacheKeyForRowAtIndexPath:indexPath]; + + // 1. 模拟业务场景某个 indexPath 的数据发生变化 + [self.dataSource setObject:@"变化后的内容" forKey:self.dataSource.allKeys[indexPath.row]]; + + // 2. 在更新 UI 之前先令对应的缓存失效 + [self.tableView qmui_invalidateCellHeightCachedForKey:cachedKey]; + + // 3. 更新 UI + [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; +} + +#pragma mark - + +- (id)qmui_tableView:(UITableView *)tableView cacheKeyForRowAtIndexPath:(NSIndexPath *)indexPath { + // 返回一个用于标记当前 cell 高度的 key,只要 key 不变,高度就不会重新计算,所以建议将有可能影响 cell 高度的数据字段作为 key 的一部分(例如 username、content.md5 等),这样当数据发生变化时,只要触发 cell 的渲染,高度就会自动更新 + NSString *keyName = self.dataSource.allKeys[indexPath.row]; + NSString *contentText = [self.dataSource objectForKey:keyName]; + return @(contentText.length);// 这里简单处理,认为只要长度不同,高度就不同(但实际情况下长度就算相同,高度也有可能不同,要注意) +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.dataSource.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + QDDynamicHeightTableViewCell *cell = (QDDynamicHeightTableViewCell *)[tableView dequeueReusableCellWithIdentifier:kCellIdentifier]; + if (!cell) { + cell = [[QDDynamicHeightTableViewCell alloc] initForTableView:tableView withReuseIdentifier:kCellIdentifier]; + } + cell.separatorInset = UIEdgeInsetsZero; + NSString *keyName = self.dataSource.allKeys[indexPath.row]; + [cell updateCellAppearanceWithIndexPath:indexPath]; + [cell renderWithNameText:[NSString stringWithFormat:@"%@ - %@", @(indexPath.row), keyName] contentText:[self.dataSource objectForKey:keyName]]; + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [self.tableView qmui_clearsSelection]; +} + +- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { + // estimatedHeightForRowAtIndexPath 的返回值会用于“该 indexPath 对应的 key 尚未有被缓存的高度”时,换句话说,如果该 indexPath 对应的 key 已经缓存了高度,则不会再调用该 indexPath 的 estimatedHeightForRowAtIndexPath 方法。 + // 可通过观察 Xcode 控制台的 log 来判断当前方法是否被调用 + NSLog(@"%@ - estimatedHeightForRowAtIndexPath called", @(indexPath.row)); + return 300; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.row == 0) { + return 200;// 返回一个大于等于0的值,则使用业务自己的返回值 + } + return -1;// 返回一个小于0的值,表示交给 QMUICellHeightKeyCache +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDCellSizeKeyCacheViewController.h b/qmuidemo/Modules/Demos/Components/QDCellSizeKeyCacheViewController.h new file mode 100644 index 00000000..f3777edf --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCellSizeKeyCacheViewController.h @@ -0,0 +1,13 @@ +// +// QDCellSizeKeyCacheViewController.h +// qmuidemo +// +// Created by QMUI Team on 2018/3/18. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +@interface QDCellSizeKeyCacheViewController : QDCommonViewController + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDCellSizeKeyCacheViewController.m b/qmuidemo/Modules/Demos/Components/QDCellSizeKeyCacheViewController.m new file mode 100644 index 00000000..25b41d0a --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCellSizeKeyCacheViewController.m @@ -0,0 +1,131 @@ +// +// QDCellSizeKeyCacheViewController.m +// qmuidemo +// +// Created by QMUI Team on 2018/3/18. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import "QDCellSizeKeyCacheViewController.h" + +@interface QDCellSizeKeyCacheViewController () + +@property(nonatomic, copy) NSArray *dataSource; +@property(nonatomic, strong) UICollectionView *collectionView; +@property(nonatomic, strong) UICollectionViewFlowLayout *collectionLayout; +@end + +// 这个 cell 只是为了表现动态 size,所以不用看它的代码 +@interface QDDynamicSizeCollectionViewCell : UICollectionViewCell + +@property(nonatomic, strong) UILabel *textLabel; +@property(nonatomic, assign) UIEdgeInsets paddings; +@property(nonatomic, strong) NSIndexPath *indexPath; +@end + +@implementation QDCellSizeKeyCacheViewController + +- (void)didInitialize { + [super didInitialize]; + self.dataSource = @[@"UIViewController is a generic controller base class that manages a view. It has methods that are called when a view appears or disappears.", + @"Subclasses can override -loadView to create their custom view hierarchy, or specify a nib name to be loaded automatically. This class is also a good place for delegate & datasource methods, and other controller stuff.", + @"Views are the fundamental building blocks of your app's user interface, and the UIView class defines the behaviors that are common to all views. A view object renders content within its bounds rectangle and handles any interactions with that content.", + @"The UIView class is a concrete class that you can instantiate and use to display a fixed background color. You can also subclass it to draw more sophisticated content.", + @"To display labels, images, buttons, and other interface elements commonly found in apps, use the view subclasses provided by the UIKit framework rather than trying to define your own.", + @"The base class for controls, which are visual elements that convey a specific action or intention in response to user interactions.", + @"Controls implement elements such as buttons and sliders, which your app might use to facilitate navigation, gather user input, or manipulate content. Controls use the Target-Action mechanism to report user interactions to your app.", + @"You do not create instances of this class directly. The UIControl class is a subclassing point that you extend to implement custom controls. You can also subclass existing control classes to extend or modify their behaviors. For example, you might override the methods of this class to track touch events yourself or to determine when the state of the control changes.", + @"A control’s state determines its appearance and its ability to support user interactions. Controls can be in one of several states, which are defined by the UIControlState type. You can change the state of a control programmatically based on your app’s needs. For example, you might disable a control to prevent the user from interacting with it. User interactions can also change the state of a control.", + @"The appearance of labels is configurable, and they can display attributed strings, allowing you to customize the appearance of substrings within a label. You can add labels to your interface programmatically or by using Interface Builder.", + @"Supply either a string or an attributed string that represents the content.", + @"If using a nonattributed string, configure the appearance of the label.", + @"Set up Auto Layout rules to govern the size and position of the label in your interface.", + @"Provide accessibility information and localized strings.", + @"Image views let you efficiently draw any image that can be specified using a UIImage object. For example, you can use the UIImageView class to display the contents of many standard image files, such as JPEG and PNG files. You can configure image views programmatically or in your storyboard file and change the images they display at runtime. For animated images, you can also use the methods of this class to start and stop the animation and specify other animation parameters.", + ]; +} + +- (void)initSubviews { + [super initSubviews]; + self.collectionLayout = [[UICollectionViewFlowLayout alloc] init]; + self.collectionLayout.scrollDirection = UICollectionViewScrollDirectionVertical; + self.collectionLayout.sectionInset = UIEdgeInsetsMake(24, 24, 24, 24); + self.collectionLayout.minimumLineSpacing = self.collectionLayout.sectionInset.top; + + self.collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.collectionLayout]; + self.collectionView.dataSource = self; + self.collectionView.delegate = self; + self.collectionView.backgroundColor = UIColorWhite; + [self.collectionView registerClass:[QDDynamicSizeCollectionViewCell class] forCellWithReuseIdentifier:@"cell"]; + self.collectionView.qmui_cacheCellSizeByKeyAutomatically = YES; + [self.view addSubview:self.collectionView]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + self.collectionView.frame = self.view.bounds; + 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); +} + +#pragma mark - + +- (id)qmui_collectionView:(UICollectionView *)collectionView cacheKeyForItemAtIndexPath:(NSIndexPath *)indexPath { + return self.dataSource[indexPath.item].qmui_md5; +} + +#pragma mark - + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + return 1; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return self.dataSource.count; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + QDDynamicSizeCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; + NSString *text = self.dataSource[indexPath.item]; + cell.textLabel.attributedText = [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName: UIFontMake(14), NSForegroundColorAttributeName: UIColorBlack, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; + cell.indexPath = indexPath; + [cell setNeedsLayout]; + return cell; +} + +@end + +@implementation QDDynamicSizeCollectionViewCell + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + + self.backgroundColor = UIColorWhite; + 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]; + self.textLabel.numberOfLines = 0; + [self.contentView addSubview:self.textLabel]; + + self.paddings = UIEdgeInsetsMake(12, 16, 16, 16); + } + return self; +} + +- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes { + UICollectionViewLayoutAttributes *result = [super preferredLayoutAttributesFittingAttributes:layoutAttributes]; + CGFloat resultHeight = [self.textLabel sizeThatFits:CGSizeMake(result.size.width - UIEdgeInsetsGetHorizontalValue(self.paddings), CGFLOAT_MAX)].height + UIEdgeInsetsGetVerticalValue(self.paddings); + CGSize resultSize = CGSizeFlatted(CGSizeMake(result.size.width, resultHeight)); + NSLog(@"第 %@ 个 cell 的 preferredLayoutAttributesFittingAttributes: 被调用(说明这个 cell 的 size 重新计算了一遍),结果为 %@", @(self.indexPath.item), NSStringFromCGSize(resultSize)); + result.size = resultSize; + return result; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:self.layer.cornerRadius].CGPath; + self.textLabel.frame = CGRectMake(self.paddings.left, self.paddings.top, CGRectGetWidth(self.contentView.bounds) - UIEdgeInsetsGetHorizontalValue(self.paddings), CGRectGetHeight(self.contentView.bounds) - UIEdgeInsetsGetVerticalValue(self.paddings)); +} + +@end 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/UIKit/QDCollectionDemoViewController.h b/qmuidemo/Modules/Demos/Components/QDCollectionDemoViewController.h similarity index 93% rename from qmuidemo/Modules/Demos/UIKit/QDCollectionDemoViewController.h rename to qmuidemo/Modules/Demos/Components/QDCollectionDemoViewController.h index e8fc6649..b996da00 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDCollectionDemoViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDCollectionDemoViewController.h @@ -2,7 +2,7 @@ // QDCollectionDemoViewController.h // qmuidemo // -// Created by zhoonchen on 16/9/8. +// Created by QMUI Team on 16/9/8. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDCollectionDemoViewController.m b/qmuidemo/Modules/Demos/Components/QDCollectionDemoViewController.m new file mode 100644 index 00000000..5426eecf --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCollectionDemoViewController.m @@ -0,0 +1,125 @@ +// +// QDCollectionDemoViewController.m +// qmuidemo +// +// Created by QMUI Team on 16/9/8. +// Copyright © 2016年 QMUI Team. All rights reserved. +// + +#import "QDCollectionDemoViewController.h" +#import "QDCollectionViewDemoCell.h" + +@implementation QDCollectionDemoViewController + +- (instancetype)initWithLayoutStyle:(QMUICollectionViewPagingLayoutStyle)style { + if (self = [super initWithNibName:nil bundle:nil]) { + _collectionViewLayout = [[QMUICollectionViewPagingLayout alloc] initWithStyle:style]; + } + return self; +} + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + return [self initWithLayoutStyle:QMUICollectionViewPagingLayoutStyleDefault]; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + + self.titleView.userInteractionEnabled = YES; + [self.titleView addTarget:self action:@selector(handleTitleViewTouchEvent) forControlEvents:UIControlEventTouchUpInside]; + + self.navigationItem.rightBarButtonItems = @[[UIBarButtonItem qmui_itemWithTitle:self.collectionViewLayout.debug ? @"普通模式" : @"调试模式" target:self action:@selector(handleDebugItemEvent)], + [UIBarButtonItem qmui_itemWithTitle:self.collectionViewLayout.scrollDirection == UICollectionViewScrollDirectionVertical ? @"水平" : @"垂直" target:self action:@selector(handleDirectionItemEvent)]]; +} + +- (void)initSubviews { + [super initSubviews]; + + _collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.collectionViewLayout]; + self.collectionView.backgroundColor = UIColorClear; + self.collectionView.showsHorizontalScrollIndicator = NO; + self.collectionView.delegate = self; + self.collectionView.dataSource = self; + [self.collectionView registerClass:[QDCollectionViewDemoCell class] forCellWithReuseIdentifier:@"cell"]; + [self.view addSubview:self.collectionView]; + + self.collectionViewLayout.sectionInset = [self sectionInset]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + if (!CGSizeEqualToSize(self.collectionView.bounds.size, self.view.bounds.size)) { + self.collectionView.frame = self.view.bounds; + self.collectionViewLayout.sectionInset = [self sectionInset]; + [self.collectionViewLayout invalidateLayout]; + } +} + +- (void)handleTitleViewTouchEvent { + [self.collectionView qmui_scrollToTopAnimated:YES]; +} + +- (void)handleDirectionItemEvent { + self.collectionViewLayout.scrollDirection = self.collectionViewLayout.scrollDirection == UICollectionViewScrollDirectionVertical ? UICollectionViewScrollDirectionHorizontal : UICollectionViewScrollDirectionVertical; + [self.collectionViewLayout invalidateLayout]; + [self.collectionView qmui_scrollToTopAnimated:YES]; + [self.collectionView reloadData]; + + [self setupNavigationItems]; + [self.view setNeedsLayout]; +} + +- (void)handleDebugItemEvent { + self.collectionViewLayout.debug = !self.collectionViewLayout.debug; + self.collectionViewLayout.sectionInset = [self sectionInset]; + [self.collectionViewLayout invalidateLayout]; + [self.collectionView qmui_scrollToTopAnimated:YES]; + [self.collectionView reloadData]; + + [self setupNavigationItems]; +} + +- (UIEdgeInsets)sectionInset { + if (self.collectionViewLayout.debug) { + CGSize itemSize = CGSizeMake(100, 100); + 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); + } +} + +#pragma mark - + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + return 1; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return 20; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + QDCollectionViewDemoCell *cell = (QDCollectionViewDemoCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; + cell.debug = self.collectionViewLayout.debug; + cell.pagingThreshold = self.collectionViewLayout.pagingThreshold; + cell.scrollDirection = self.collectionViewLayout.scrollDirection; + cell.contentLabel.text = [NSString qmui_stringWithNSInteger:indexPath.item]; + cell.backgroundColor = [QDCommonUI randomThemeColor]; + [cell setNeedsLayout]; + return cell; +} + +#pragma mark - + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + if (self.collectionViewLayout.debug) { + return CGSizeMake(100, 100); + } + + 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; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDCollectionStackDemoViewController.h b/qmuidemo/Modules/Demos/Components/QDCollectionStackDemoViewController.h similarity index 87% rename from qmuidemo/Modules/Demos/UIKit/QDCollectionStackDemoViewController.h rename to qmuidemo/Modules/Demos/Components/QDCollectionStackDemoViewController.h index 1b2f799d..70e3bd14 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDCollectionStackDemoViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDCollectionStackDemoViewController.h @@ -2,7 +2,7 @@ // QDCollectionStackDemoViewController.h // qmuidemo // -// Created by ZhoonChen on 15/10/6. +// Created by QMUI Team on 15/10/6. // Copyright © 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDCollectionStackDemoViewController.m b/qmuidemo/Modules/Demos/Components/QDCollectionStackDemoViewController.m new file mode 100644 index 00000000..df7c1e59 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCollectionStackDemoViewController.m @@ -0,0 +1,124 @@ +// +// QDCollectionStackDemoViewController.m +// qmuidemo +// +// Created by QMUI Team on 15/10/6. +// Copyright © 2015年 QMUI Team. All rights reserved. +// + +#import "QDCollectionStackDemoViewController.h" +#import "QDFoldCollectionViewLayout.h" +#import "QDCollectionViewDemoCell.h" + +@interface QDCollectionStackDemoViewController () + +@property(nonatomic, strong) UIPanGestureRecognizer *panGesture; +@property(nonatomic, strong) UICollectionView *collectionView; +@property(nonatomic, strong) QDFoldCollectionViewLayout *collectionViewLayout; +@property(nonatomic, strong) NSMutableArray *datas; +@end + +@implementation QDCollectionStackDemoViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.datas = [[NSMutableArray alloc] initWithObjects:@"0", @"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"10", nil]; +} + +- (void)initSubviews { + [super initSubviews]; + self.collectionViewLayout = [[QDFoldCollectionViewLayout alloc] init]; + self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:self.collectionViewLayout]; + self.collectionView.backgroundColor = UIColorClear; // 不设置貌似会变黑色 + self.collectionView.showsHorizontalScrollIndicator = NO; // 隐藏滚动条 + self.collectionView.delegate = self; + self.collectionView.dataSource = self; + [self.collectionView registerClass:[QDCollectionViewDemoCell class] forCellWithReuseIdentifier:@"cell"]; + [self.view addSubview:self.collectionView]; + // 初始化收拾 + [self initGesture]; +} + +- (void)initGesture { + if (!self.panGesture) { + self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)]; + self.panGesture.delegate = self; + [self.collectionView addGestureRecognizer:self.panGesture]; + } +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + self.collectionView.frame = self.view.bounds; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; +} + +// delegate + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + return 1; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return self.datas.count; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + QDCollectionViewDemoCell *cell = (QDCollectionViewDemoCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; + cell.contentLabel.text = [NSString stringWithFormat:@"%@", [self.datas objectAtIndex:indexPath.item]]; + [cell setNeedsLayout]; + return cell; +} + +// gesture + +- (void)handlePanGesture:(UIPanGestureRecognizer *)gesture { + if (gesture.state == UIGestureRecognizerStateBegan) { + NSLog(@"gesture begin"); + // CGPoint point = [gesture locationInView:self.collectionView]; + // NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point]; + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + if (indexPath) { + self.collectionViewLayout.curIndexPath = indexPath; + self.collectionViewLayout.isMoving = YES; + } + } + else if (gesture.state == UIGestureRecognizerStateChanged) { + NSLog(@"gesture chagned"); + CGPoint point = [gesture translationInView:self.collectionView]; + self.collectionViewLayout.curPoint = point; + [self.collectionViewLayout invalidateLayout]; + } + else if (gesture.state == UIGestureRecognizerStateCancelled || gesture.state == UIGestureRecognizerStateEnded) { + NSLog(@"gesture canceled or ended"); + CGFloat maxDistance = fmax(fabs(self.collectionViewLayout.curPoint.x), fabs(self.collectionViewLayout.curPoint.y)); + self.collectionViewLayout.isMoving = NO; + self.collectionViewLayout.curIndexPath = nil; + if (maxDistance > 80) { + NSIndexPath *deleteIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + [self.datas removeObjectAtIndex:0]; + [UIView animateWithDuration:.5 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ + QDCollectionViewDemoCell *cell = (QDCollectionViewDemoCell *)[self.collectionView cellForItemAtIndexPath:deleteIndexPath]; + cell.layer.transform = CATransform3DMakeTranslation(self.collectionViewLayout.curPoint.x * 10, self.collectionViewLayout.curPoint.y * 10, 0); + cell.alpha = 0; + } completion:^(BOOL finished) { + [self.collectionView performBatchUpdates:^{ + [self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:deleteIndexPath]]; + } completion:^(BOOL finished) { + self.collectionViewLayout.curPoint = CGPointZero; + }]; + }]; + } else { + [self.collectionView performBatchUpdates:^{ + [self.collectionView reloadData]; + } completion:^(BOOL finished) { + self.collectionViewLayout.curPoint = CGPointZero; + }]; + } + } +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDCollectionViewDemoCell.h b/qmuidemo/Modules/Demos/Components/QDCollectionViewDemoCell.h new file mode 100644 index 00000000..f6ed2c07 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCollectionViewDemoCell.h @@ -0,0 +1,17 @@ +// +// QDCollectionViewDemoCell.h +// qmuidemo +// +// Created by QMUI Team on 15/9/24. +// Copyright © 2015年 QMUI Team. All rights reserved. +// + +#import + +@interface QDCollectionViewDemoCell : UICollectionViewCell + +@property(nonatomic, strong, readonly) UILabel *contentLabel; +@property(nonatomic, assign) BOOL debug; +@property(nonatomic, assign) CGFloat pagingThreshold; +@property(nonatomic, assign) UICollectionViewScrollDirection scrollDirection; +@end diff --git a/qmuidemo/Modules/Demos/Components/QDCollectionViewDemoCell.m b/qmuidemo/Modules/Demos/Components/QDCollectionViewDemoCell.m new file mode 100644 index 00000000..9a2b98b2 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDCollectionViewDemoCell.m @@ -0,0 +1,61 @@ +// +// QDCollectionViewDemoCell.m +// qmuidemo +// +// Created by QMUI Team on 15/9/24. +// Copyright © 2015年 QMUI Team. All rights reserved. +// + +#import "QDCollectionViewDemoCell.h" + +@interface QDCollectionViewDemoCell () + +@property(nonatomic, strong) CALayer *prevLayer; +@property(nonatomic, strong) CALayer *nextLayer; +@end + +@implementation QDCollectionViewDemoCell + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + self.layer.cornerRadius = 3; + + _contentLabel = [[UILabel alloc] qmui_initWithFont:UIFontLightMake(100) textColor:UIColorWhite]; + self.contentLabel.textAlignment = NSTextAlignmentCenter; + [self.contentView addSubview:self.contentLabel]; + + self.prevLayer = [CALayer layer]; + [self.prevLayer qmui_removeDefaultAnimations]; + self.prevLayer.backgroundColor = UIColorMakeWithRGBA(0, 0, 0, .3).CGColor; + [self.contentView.layer addSublayer:self.prevLayer]; + + self.nextLayer = [CALayer layer]; + [self.nextLayer qmui_removeDefaultAnimations]; + self.nextLayer.backgroundColor = self.prevLayer.backgroundColor; + [self.contentView.layer addSublayer:self.nextLayer]; + } + return self; +} + +- (void)setDebug:(BOOL)debug { + _debug = debug; + self.prevLayer.hidden = !debug; + self.nextLayer.hidden = !debug; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + [self.contentLabel sizeToFit]; + self.contentLabel.center = CGPointMake(CGRectGetWidth(self.contentView.bounds) / 2, CGRectGetHeight(self.contentView.bounds) / 2); + + if (self.scrollDirection == UICollectionViewScrollDirectionVertical) { + self.prevLayer.frame = CGRectMake(0, CGRectGetHeight(self.contentView.bounds) * (1 - self.pagingThreshold), CGRectGetWidth(self.contentView.bounds), PixelOne); + self.nextLayer.frame = CGRectSetY(self.prevLayer.frame, CGRectGetHeight(self.contentView.bounds) * self.pagingThreshold); + } else { + self.prevLayer.frame = CGRectMake(CGRectGetWidth(self.contentView.bounds) * (1 - self.pagingThreshold), 0, PixelOne, CGRectGetHeight(self.contentView.bounds)); + self.nextLayer.frame = CGRectSetX(self.prevLayer.frame, CGRectGetWidth(self.contentView.bounds) * self.pagingThreshold); + } +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDComponentsViewController.h b/qmuidemo/Modules/Demos/Components/QDComponentsViewController.h index 0e469b00..8bb21b5c 100644 --- a/qmuidemo/Modules/Demos/Components/QDComponentsViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDComponentsViewController.h @@ -2,7 +2,7 @@ // QDComponentsViewController.h // qmuidemo // -// Created by ZhoonChen on 15/6/2. +// Created by QMUI Team on 15/6/2. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDComponentsViewController.m b/qmuidemo/Modules/Demos/Components/QDComponentsViewController.m index 746e9913..dc6e6ab6 100644 --- a/qmuidemo/Modules/Demos/Components/QDComponentsViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDComponentsViewController.m @@ -2,11 +2,12 @@ // QDComponentsViewController.m // qmuidemo // -// Created by ZhoonChen on 15/6/2. +// Created by QMUI Team on 15/6/2. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #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,13 +26,36 @@ #import "QDToastListViewController.h" #import "QDKeyboardViewController.h" #import "QDMarqueeLabelViewController.h" +#import "QDMultipleDelegatesViewController.h" +#import "QDBadgeViewController.h" +#import "QDConsoleViewController.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)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; +- (void)didInitialize { + [super didInitialize]; self.title = @"Components"; - self.navigationItem.rightBarButtonItem = [QMUINavigationButton barButtonItemWithImage:UIImageMake(@"icon_nav_about") position:QMUINavigationButtonPositionRight target:self action:@selector(handleAboutItemEvent)]; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + 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 { @@ -40,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"), @@ -47,6 +72,7 @@ - (void)initDataSource { @"QMUIGridView", UIImageMake(@"icon_grid_gridView"), @"QMUIFloatLayoutView", UIImageMake(@"icon_grid_floatView"), @"QMUIStaticTableView", UIImageMake(@"icon_grid_staticTableView"), + @"QMUICellKeyCache", UIImageMake(@"icon_grid_cellKeyCache"), @"QMUIPickingImage", UIImageMake(@"icon_grid_pickingImage"), @"QMUIAssetsManager", UIImageMake(@"icon_grid_assetsManager"), @"QMUIImagePreviewView", UIImageMake(@"icon_grid_previewImage"), @@ -54,10 +80,19 @@ - (void)initDataSource { @"QMUIPopupContainerView", UIImageMake(@"icon_grid_popupView"), @"QMUIKeyboardManager", UIImageMake(@"icon_grid_keyboard"), @"QMUIMarqueeLabel", UIImageMake(@"icon_grid_marquee"), + @"QMUIMultipleDelegates", UIImageMake(@"icon_grid_multipleDelegates"), + @"QMUIBadge", UIImageMake(@"icon_grid_badge"), + @"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]; @@ -71,8 +106,48 @@ - (void)didSelectCellWithTitle:(NSString *)title { else if ([title isEqualToString:@"QMUIStaticTableView"]) { viewController = [[QDStaticTableViewController alloc] initWithStyle:UITableViewStyleGrouped]; } + else if ([title isEqualToString:@"QMUICellKeyCache"]) { + 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]; @@ -96,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]; @@ -110,6 +202,79 @@ - (void)didSelectCellWithTitle:(NSString *)title { else if ([title isEqualToString:@"QMUIMarqueeLabel"]) { viewController = [[QDMarqueeLabelViewController alloc] init]; } + else if ([title isEqualToString:@"QMUIMultipleDelegates"]) { + viewController = [[QDMultipleDelegatesViewController alloc] init]; + } + else if ([title isEqualToString:@"QMUIBadge"]) { + viewController = [[QDBadgeViewController alloc] init]; + } + else if ([title isEqualToString:@"QMUIScrollAnimator"]) { + 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 = ({ + 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.h b/qmuidemo/Modules/Demos/Components/QDConsoleViewController.h new file mode 100644 index 00000000..0c473242 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDConsoleViewController.h @@ -0,0 +1,17 @@ +// +// QDConsoleViewController.h +// qmuidemo +// +// Created by MoLice on 2019/1/15. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDConsoleViewController : QDCommonViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Components/QDConsoleViewController.m b/qmuidemo/Modules/Demos/Components/QDConsoleViewController.m new file mode 100644 index 00000000..5697cb85 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDConsoleViewController.m @@ -0,0 +1,79 @@ +// +// QDConsoleViewController.m +// qmuidemo +// +// Created by MoLice on 2019/1/15. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDConsoleViewController.h" + +@interface QDConsoleViewController () + +@property(nonatomic, strong) QMUIButton *printLogButton; +@property(nonatomic, strong) QMUIButton *printMultipleLogButton; +@property(nonatomic, strong) UILabel *tipsLabel; +@end + +@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: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; + NSDictionary *codeAttributes = CodeAttributes(12); + [tips.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { + [tips addAttributes:codeAttributes range:codeRange]; + }]; + self.tipsLabel.attributedText = tips; + [self.view addSubview:self.tipsLabel]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + 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); + self.tipsLabel.frame = CGRectMake(paddings.left, CGRectGetMaxY(self.printMultipleLogButton.frame) + 16, CGRectGetWidth(self.printMultipleLogButton.frame), QMUIViewSelfSizingHeight); +} + +- (void)handlePrintLogButtonEvent:(QMUIButton *)button { + // 支持打印普通文本 + [QMUIConsole log:@"NSString log"]; + + // 支持 NSAttributedString 自定义 log 样式 +// [QMUIConsole log:[[NSAttributedString alloc] initWithString:@"NSAttributedString log" attributes:({ +// NSMutableDictionary *attributes = [QMUIConsole appearance].textAttributes.mutableCopy; +// attributes[NSForegroundColorAttributeName] = UIColor.qd_tintColor; +// attributes; +// })]]; + + // 支持直接打印一个对象 +// [QMUIConsole log:button]; +} + +- (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] = [QDCommonUI randomThemeColor]; + attributes; + })]]; + [QMUIConsole logWithLevel:@"Error" name:@"QMUIButton" logString:button]; +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDCustomToastAnimator.h b/qmuidemo/Modules/Demos/Components/QDCustomToastAnimator.h index 7bda6270..78dc1176 100644 --- a/qmuidemo/Modules/Demos/Components/QDCustomToastAnimator.h +++ b/qmuidemo/Modules/Demos/Components/QDCustomToastAnimator.h @@ -2,7 +2,7 @@ // QDCustomToastAnimator.h // qmuidemo // -// Created by zhoonchen on 2016/12/13. +// Created by QMUI Team on 2016/12/13. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDCustomToastAnimator.m b/qmuidemo/Modules/Demos/Components/QDCustomToastAnimator.m index 4cf7462b..dc0702eb 100644 --- a/qmuidemo/Modules/Demos/Components/QDCustomToastAnimator.m +++ b/qmuidemo/Modules/Demos/Components/QDCustomToastAnimator.m @@ -2,20 +2,23 @@ // QDCustomToastAnimator.m // qmuidemo // -// Created by zhoonchen on 2016/12/13. +// Created by QMUI Team on 2016/12/13. // Copyright © 2016年 QMUI Team. All rights reserved. // #import "QDCustomToastAnimator.h" -@implementation QDCustomToastAnimator { - BOOL _isShowing; - BOOL _isAnimating; -} +@interface QDCustomToastAnimator () + +@property(nonatomic, assign) BOOL isShowing; +@property(nonatomic, assign) BOOL isAnimating; +@end + +@implementation QDCustomToastAnimator - (void)showWithCompletion:(void (^)(BOOL finished))completion { - _isShowing = YES; - _isAnimating = YES; + self.isShowing = YES; + self.isAnimating = YES; self.toastView.backgroundView.layer.transform = CATransform3DMakeTranslation(0, -30, 0); self.toastView.contentView.layer.transform = CATransform3DMakeTranslation(0, -30, 0); [UIView animateWithDuration:0.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ @@ -24,7 +27,7 @@ - (void)showWithCompletion:(void (^)(BOOL finished))completion { self.toastView.backgroundView.layer.transform = CATransform3DIdentity; self.toastView.contentView.layer.transform = CATransform3DIdentity; } completion:^(BOOL finished) { - _isAnimating = NO; + self.isAnimating = NO; if (completion) { completion(finished); } @@ -32,15 +35,15 @@ - (void)showWithCompletion:(void (^)(BOOL finished))completion { } - (void)hideWithCompletion:(void (^)(BOOL finished))completion { - _isShowing = NO; - _isAnimating = YES; + self.isShowing = NO; + self.isAnimating = YES; [UIView animateWithDuration:0.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ self.toastView.backgroundView.alpha = 0.0; self.toastView.contentView.alpha = 0.0; self.toastView.backgroundView.layer.transform = CATransform3DMakeTranslation(0, -30, 0); self.toastView.contentView.layer.transform = CATransform3DMakeTranslation(0, -30, 0); } completion:^(BOOL finished) { - _isAnimating = NO; + self.isAnimating = NO; self.toastView.backgroundView.layer.transform = CATransform3DIdentity; self.toastView.contentView.layer.transform = CATransform3DIdentity; if (completion) { @@ -50,11 +53,11 @@ - (void)hideWithCompletion:(void (^)(BOOL finished))completion { } - (BOOL)isShowing { - return _isShowing; + return self.isShowing; } - (BOOL)isAnimating { - return _isAnimating; + return self.isAnimating; } @end diff --git a/qmuidemo/Modules/Demos/Components/QDCustomToastContentView.h b/qmuidemo/Modules/Demos/Components/QDCustomToastContentView.h index e9bb99da..8e99b329 100644 --- a/qmuidemo/Modules/Demos/Components/QDCustomToastContentView.h +++ b/qmuidemo/Modules/Demos/Components/QDCustomToastContentView.h @@ -2,7 +2,7 @@ // QDCustomToastContentView.h // qmuidemo // -// Created by zhoonchen on 2016/12/13. +// Created by QMUI Team on 2016/12/13. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDCustomToastContentView.m b/qmuidemo/Modules/Demos/Components/QDCustomToastContentView.m index 386dff53..4efb6349 100644 --- a/qmuidemo/Modules/Demos/Components/QDCustomToastContentView.m +++ b/qmuidemo/Modules/Demos/Components/QDCustomToastContentView.m @@ -2,7 +2,7 @@ // QDCustomToastContentView.m // qmuidemo // -// Created by zhoonchen on 2016/12/13. +// Created by QMUI Team on 2016/12/13. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -59,9 +59,9 @@ - (void)renderWithImage:(UIImage *)image text:(NSString *)text detailText:(NSStr } - (CGSize)sizeThatFits:(CGSize)size { - CGFloat width = fminf(size.width, [QMUIHelper screenSizeFor55Inch].width); + CGFloat width = fmin(size.width, [QMUIHelper screenSizeFor55Inch].width); CGFloat height = kImageViewHeight + UIEdgeInsetsGetVerticalValue(kInsets); - return CGSizeMake(fminf(size.width, width), fminf(size.height, height)); + return CGSizeMake(fmin(size.width, width), fmin(size.height, height)); } - (void)layoutSubviews { @@ -79,7 +79,7 @@ - (void)layoutSubviews { CGFloat detailLimitHeight = CGRectGetHeight(self.bounds) - CGRectGetMaxY(self.textLabel.frame) - kTextLabelMarginBottom - kInsets.bottom; CGSize detailSize = [self.detailTextLabel sizeThatFits:CGSizeMake(labelWidth, detailLimitHeight)]; - self.detailTextLabel.frame = CGRectFlatMake(CGRectGetMinX(self.textLabel.frame), CGRectGetMaxY(self.textLabel.frame) + kTextLabelMarginBottom, labelWidth, fminf(detailLimitHeight, detailSize.height)); + self.detailTextLabel.frame = CGRectFlatMake(CGRectGetMinX(self.textLabel.frame), CGRectGetMaxY(self.textLabel.frame) + kTextLabelMarginBottom, labelWidth, fmin(detailLimitHeight, detailSize.height)); } @end diff --git a/qmuidemo/Modules/Demos/Components/QDDialogViewController.h b/qmuidemo/Modules/Demos/Components/QDDialogViewController.h index e58dde28..7c3d74c1 100644 --- a/qmuidemo/Modules/Demos/Components/QDDialogViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDDialogViewController.h @@ -2,7 +2,7 @@ // QDDialogViewController.h // qmuidemo // -// Created by MoLice on 16/7/20. +// Created by QMUI Team on 16/7/20. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDDialogViewController.m b/qmuidemo/Modules/Demos/Components/QDDialogViewController.m index 63e763fd..bdcc2e05 100644 --- a/qmuidemo/Modules/Demos/Components/QDDialogViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDDialogViewController.m @@ -2,7 +2,7 @@ // QDDialogViewController.m // qmuidemo // -// Created by MoLice on 16/7/20. +// Created by QMUI Team on 16/7/20. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -12,7 +12,7 @@ static NSString * const kSectionTitleForSelection = @"QMUIDialogSelectionViewController"; static NSString * const kSectionTitleForTextField = @"QMUIDialogTextFieldViewController"; -@interface QDDialogViewController () +@interface QDDialogViewController () @property(nonatomic, weak) QMUIDialogTextFieldViewController *currentTextFieldDialogViewController; @end @@ -33,6 +33,7 @@ - (instancetype)initWithStyle:(UITableViewStyle)style { nil], kSectionTitleForTextField, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: @"输入框弹窗", @"", + @"支持通过键盘 Return 按键触发弹窗提交按钮事件", @"默认开启,当需要自己管理输入框 shouldReturn 事件时请将其关闭", @"支持自动控制提交按钮的 enable 状态", @"默认开启,只要文字不为空则允许点击", @"支持自定义提交按钮的 enable 状态", @"通过 block 来控制状态", nil], @@ -67,7 +68,7 @@ - (void)didSelectCellWithTitle:(NSString *)title { } if ([title isEqualToString:@"支持多选"]) { - [self showMutipleSelectionDialogViewController]; + [self showMultipleSelectionDialogViewController]; return; } @@ -76,6 +77,11 @@ - (void)didSelectCellWithTitle:(NSString *)title { return; } + if ([title isEqualToString:@"支持通过键盘 Return 按键触发弹窗提交按钮事件"]) { + [self showReturnKeyDialogViewController]; + return; + } + if ([title isEqualToString:@"支持自动控制提交按钮的 enable 状态"]) { [self showSubmitButtonEnablesDialogViewController]; return; @@ -92,7 +98,7 @@ - (void)showNormalDialogViewController { dialogViewController.title = @"标题"; UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 100)]; contentView.backgroundColor = UIColorWhite; - UILabel *label = [[UILabel alloc] initWithFont:UIFontMake(14) textColor:UIColorBlack]; + UILabel *label = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; label.text = @"自定义contentView"; [label sizeToFit]; label.center = CGPointMake(CGRectGetWidth(contentView.bounds) / 2.0, CGRectGetHeight(contentView.bounds) / 2.0); @@ -108,9 +114,10 @@ - (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; - UILabel *label = [[UILabel alloc] initWithFont:UIFontMake(14) textColor:UIColorWhite]; + contentView.backgroundColor = UIColor.qd_tintColor; + UILabel *label = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorWhite]; label.text = @"自定义contentView"; [label sizeToFit]; label.center = CGPointMake(CGRectGetWidth(contentView.bounds) / 2.0, CGRectGetHeight(contentView.bounds) / 2.0); @@ -122,16 +129,23 @@ - (void)showAppearanceDialogViewController { [aDialogViewController hide]; }]; - // 自定义样式 - dialogViewController.headerViewBackgroundColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; - dialogViewController.headerFooterSeparatorColor = UIColorClear; + // === 自定义样式 === + 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; dialogViewController.buttonTitleAttributes = buttonTitleAttributes; - [dialogViewController.submitButton setImage:[[UIImageMake(@"icon_emotion") qmui_imageWithScaleToSize:CGSizeMake(18, 18) contentMode:UIViewContentModeScaleToFill] qmui_imageWithTintColor:buttonTitleAttributes[NSForegroundColorAttributeName]] forState:UIControlStateNormal]; + [dialogViewController.submitButton setImage:[[UIImageMake(@"icon_emotion") qmui_imageResizedInLimitedSize:CGSizeMake(18, 18) resizingMode:QMUIImageResizingModeScaleToFill] qmui_imageWithTintColor:buttonTitleAttributes[NSForegroundColorAttributeName]] forState:UIControlStateNormal]; dialogViewController.submitButton.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 8); [dialogViewController show]; @@ -167,7 +181,7 @@ - (void)showRadioSelectionDialogViewController { [dialogViewController addSubmitButtonWithText:@"确定" block:^(QMUIDialogViewController *aDialogViewController) { QMUIDialogSelectionViewController *d = (QMUIDialogSelectionViewController *)aDialogViewController; if (d.selectedItemIndex == QMUIDialogSelectionViewControllerSelectedItemIndexNone) { - [QMUITips showError:@"请至少选一个" inView:d.modalPresentedViewController.view hideAfterDelay:1.2]; + [QMUITips showError:@"请至少选一个" inView:d.qmui_modalPresentationViewController.view hideAfterDelay:1.2]; return; } NSString *city = d.items[d.selectedItemIndex]; @@ -182,7 +196,7 @@ - (void)showRadioSelectionDialogViewController { [dialogViewController show]; } -- (void)showMutipleSelectionDialogViewController { +- (void)showMultipleSelectionDialogViewController { QMUIDialogSelectionViewController *dialogViewController = [[QMUIDialogSelectionViewController alloc] init]; dialogViewController.titleView.style = QMUINavigationTitleViewStyleSubTitleVertical; dialogViewController.title = @"你常用的编程语言"; @@ -203,23 +217,23 @@ - (void)showMutipleSelectionDialogViewController { [d hide]; if ([d.selectedItemIndexes containsObject:@(5)]) { - [QMUITips showInfo:@"PHP 是世界上最好的编程语言" inView:weakSelf.view hideAfterDelay:2.0]; + [QMUITips showInfo:@"PHP 是世界上最好的编程语言" inView:weakSelf.view hideAfterDelay:1.8]; return; } if ([d.selectedItemIndexes containsObject:@(4)]) { - [QMUITips showInfo:@"你代码缩进用 Tab 还是 Space?" inView:weakSelf.view hideAfterDelay:2.0]; + [QMUITips showInfo:@"你代码缩进用 Tab 还是 Space?" inView:weakSelf.view hideAfterDelay:1.8]; return; } if ([d.selectedItemIndexes containsObject:@(3)]) { - [QMUITips showInfo:@"JavaScript 即将一统江湖" inView:weakSelf.view hideAfterDelay:2.0]; + [QMUITips showInfo:@"JavaScript 即将一统江湖" inView:weakSelf.view hideAfterDelay:1.8]; return; } if ([d.selectedItemIndexes containsObject:@(2)]) { - [QMUITips showInfo:@"Android 7 都出了,我还在兼容 Android 4" inView:weakSelf.view hideAfterDelay:2.0]; + [QMUITips showInfo:@"Android 7 都出了,我还在兼容 Android 4" inView:weakSelf.view hideAfterDelay:1.8]; return; } if ([d.selectedItemIndexes containsObject:@(0)] || [d.selectedItemIndexes containsObject:@(1)]) { - [QMUITips showInfo:@"iOS 开发你好" inView:weakSelf.view hideAfterDelay:2.0]; + [QMUITips showInfo:@"iOS 开发你好" inView:weakSelf.view hideAfterDelay:1.8]; return; } }]; @@ -228,12 +242,45 @@ - (void)showMutipleSelectionDialogViewController { - (void)showNormalTextFieldDialogViewController { QMUIDialogTextFieldViewController *dialogViewController = [[QMUIDialogTextFieldViewController alloc] init]; - dialogViewController.title = @"请输入昵称"; - dialogViewController.textField.delegate = self; - dialogViewController.textField.placeholder = @"昵称"; + dialogViewController.title = @"注册用户"; + [dialogViewController addTextFieldWithTitle:@"昵称" configurationHandler:^(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer) { + textField.placeholder = @"不超过10个字符"; + textField.maximumTextLength = 10; + }]; + [dialogViewController addTextFieldWithTitle:@"密码" configurationHandler:^(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer) { + textField.placeholder = @"6位数字"; + textField.keyboardType = UIKeyboardTypeNumberPad; + textField.maximumTextLength = 6; + textField.secureTextEntry = YES; + }]; + dialogViewController.enablesSubmitButtonAutomatically = NO;// 为了演示效果与第二个 cell 的区分开,这里手动置为 NO,平时的默认值为 YES [dialogViewController addCancelButtonWithText:@"取消" block:nil]; - [dialogViewController addSubmitButtonWithText:@"确定" block:^(QMUIDialogViewController *aDialogViewController) { - [aDialogViewController hide]; + [dialogViewController addSubmitButtonWithText:@"确定" block:^(QMUIDialogTextFieldViewController *aDialogViewController) { + if (aDialogViewController.textFields.firstObject.text.length > 0) { + [aDialogViewController hide]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [QMUITips showSucceed:@"提交成功" inView:self.view hideAfterDelay:1.2]; + }); + } else { + [QMUITips showInfo:@"请填写内容" inView:self.view hideAfterDelay:1.2]; + } + }]; + [dialogViewController show]; + self.currentTextFieldDialogViewController = dialogViewController; +} + +- (void)showReturnKeyDialogViewController { + QMUIDialogTextFieldViewController *dialogViewController = [[QMUIDialogTextFieldViewController alloc] init]; + dialogViewController.title = @"请输入别名"; + [dialogViewController addTextFieldWithTitle:nil configurationHandler:^(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer) { + textField.placeholder = @"点击键盘 Return 键视为点击确定按钮"; + textField.maximumTextLength = 10; + }]; + dialogViewController.shouldManageTextFieldsReturnEventAutomatically = YES;// 让键盘的 Return 键也能触发确定按钮的事件。这个属性默认就是 YES,这里为写出来只是为了演示 + [dialogViewController addCancelButtonWithText:@"取消" block:nil]; + [dialogViewController addSubmitButtonWithText:@"确定" block:^(QMUIDialogViewController *dialogViewController) { + [QMUITips showSucceed:@"提交成功" inView:self.view hideAfterDelay:1.2]; + [dialogViewController hide]; }]; [dialogViewController show]; self.currentTextFieldDialogViewController = dialogViewController; @@ -242,12 +289,14 @@ - (void)showNormalTextFieldDialogViewController { - (void)showSubmitButtonEnablesDialogViewController { QMUIDialogTextFieldViewController *dialogViewController = [[QMUIDialogTextFieldViewController alloc] init]; dialogViewController.title = @"请输入签名"; - dialogViewController.textField.delegate = self; - dialogViewController.textField.placeholder = @"不超过10个字"; - dialogViewController.textField.maximumTextLength = 10; + [dialogViewController addTextFieldWithTitle:nil configurationHandler:^(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer) { + textField.placeholder = @"不超过10个字"; + textField.maximumTextLength = 10; + }]; + dialogViewController.enablesSubmitButtonAutomatically = YES;// 自动根据输入框的内容是否为空来控制 submitButton.enabled 状态。这个属性默认就是 YES,这里为写出来只是为了演示 [dialogViewController addCancelButtonWithText:@"取消" block:nil]; [dialogViewController addSubmitButtonWithText:@"确定" block:^(QMUIDialogViewController *dialogViewController) { - [QMUITips showSucceed:@"提交成功" inView:self.view hideAfterDelay:2.0]; + [QMUITips showSucceed:@"提交成功" inView:self.view hideAfterDelay:1.2]; [dialogViewController hide]; }]; [dialogViewController show]; @@ -257,30 +306,23 @@ - (void)showSubmitButtonEnablesDialogViewController { - (void)showCustomSubmitButtonEnablesDialogViewController { QMUIDialogTextFieldViewController *dialogViewController = [[QMUIDialogTextFieldViewController alloc] init]; dialogViewController.title = @"请输入手机号码"; - dialogViewController.textField.delegate = self; - dialogViewController.textField.placeholder = @"11位手机号码"; - dialogViewController.textField.maximumTextLength = 11; + [dialogViewController addTextFieldWithTitle:nil configurationHandler:^(QMUILabel *titleLabel, QMUITextField *textField, CALayer *separatorLayer) { + textField.placeholder = @"11位手机号码"; + textField.keyboardType = UIKeyboardTypePhonePad; + textField.maximumTextLength = 11; + }]; + dialogViewController.enablesSubmitButtonAutomatically = YES;// 自动根据输入框的内容是否为空来控制 submitButton.enabled 状态。这个属性默认就是 YES,这里为写出来只是为了演示 dialogViewController.shouldEnableSubmitButtonBlock = ^BOOL(QMUIDialogTextFieldViewController *aDialogViewController) { - return aDialogViewController.textField.text.length == aDialogViewController.textField.maximumTextLength; + // 条件改为一定要写满11位才允许提交 + return aDialogViewController.textFields.firstObject.text.length == aDialogViewController.textFields.firstObject.maximumTextLength; }; [dialogViewController addCancelButtonWithText:@"取消" block:nil]; [dialogViewController addSubmitButtonWithText:@"确定" block:^(QMUIDialogViewController *dialogViewController) { - [QMUITips showSucceed:@"提交成功" inView:self.view hideAfterDelay:2.0]; + [QMUITips showSucceed:@"提交成功" inView:self.view hideAfterDelay:1.2]; [dialogViewController hide]; }]; [dialogViewController show]; self.currentTextFieldDialogViewController = dialogViewController; } -#pragma mark - - -- (BOOL)textFieldShouldReturn:(QMUITextField *)textField { - if (self.currentTextFieldDialogViewController.submitButton.enabled) { - [self.currentTextFieldDialogViewController hide]; - } else { - [QMUITips showSucceed:@"请输入文字" inView:self.currentTextFieldDialogViewController.modalPresentedViewController.view hideAfterDelay:2.0]; - } - return NO; -} - @end diff --git a/qmuidemo/Modules/Demos/Components/QDDynamicHeightTableViewCell.h b/qmuidemo/Modules/Demos/Components/QDDynamicHeightTableViewCell.h new file mode 100644 index 00000000..458d1c5f --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDDynamicHeightTableViewCell.h @@ -0,0 +1,25 @@ +// +// QDDynamicHeightTableViewCell.h +// qmuidemo +// +// Created by MoLice on 2019/J/9. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +// 这个 cell 只是为了展示每个 cell 高度不一样,这样才有被 cache 的意义,至于这个 cell 里的代码可以不看 +@interface QDDynamicHeightTableViewCell : QMUITableViewCell + +@property(nonatomic, strong) UIImageView *avatarImageView; +@property(nonatomic, strong) UILabel *nameLabel; +@property(nonatomic, strong) UILabel *contentLabel; +@property(nonatomic, strong) UILabel *timeLabel; + +- (void)renderWithNameText:(NSString *)nameText contentText:(NSString *)contentText; + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Components/QDDynamicHeightTableViewCell.m b/qmuidemo/Modules/Demos/Components/QDDynamicHeightTableViewCell.m new file mode 100644 index 00000000..6c19586e --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDDynamicHeightTableViewCell.m @@ -0,0 +1,93 @@ +// +// QDDynamicHeightTableViewCell.m +// qmuidemo +// +// Created by MoLice on 2019/J/9. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDDynamicHeightTableViewCell.h" + +const UIEdgeInsets kInsets = {15, 16, 15, 16}; +const CGFloat kAvatarSize = 30; +const CGFloat kAvatarMarginRight = 12; +const CGFloat kAvatarMarginBottom = 6; +const CGFloat kContentMarginBotom = 10; + +@implementation QDDynamicHeightTableViewCell + +- (void)didInitializeWithStyle:(UITableViewCellStyle)style { + [super didInitializeWithStyle:style]; + + UIImage *avatarImage = [UIImage qmui_imageWithStrokeColor:[QDCommonUI randomThemeColor] size:CGSizeMake(kAvatarSize, kAvatarSize) lineWidth:3 cornerRadius:6]; + _avatarImageView = [[UIImageView alloc] initWithImage:avatarImage]; + [self.contentView addSubview:self.avatarImageView]; + + _nameLabel = [[UILabel alloc] qmui_initWithFont:UIFontBoldMake(16) textColor:UIColor.qd_mainTextColor]; + [self.contentView addSubview:self.nameLabel]; + + _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:UIColor.qd_descriptionTextColor]; + [self.contentView addSubview:self.timeLabel]; +} + +- (void)renderWithNameText:(NSString *)nameText contentText:(NSString *)contentText { + + self.nameLabel.text = nameText; + self.contentLabel.attributedText = [self attributeStringWithString:contentText lineHeight:26]; + self.timeLabel.text = @"昨天 18:24"; + + self.contentLabel.textAlignment = NSTextAlignmentJustified; +} + +- (NSAttributedString *)attributeStringWithString:(NSString *)textString lineHeight:(CGFloat)lineHeight { + if (textString.qmui_trim.length <= 0) return nil; + NSAttributedString *attriString = [[NSAttributedString alloc] initWithString:textString attributes:@{NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:lineHeight lineBreakMode:NSLineBreakByCharWrapping textAlignment:NSTextAlignmentLeft]}]; + return attriString; +} + +- (CGSize)sizeThatFits:(CGSize)size { + 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; + + if (self.contentLabel.text.length > 0) { + CGSize contentSize = [self.contentLabel sizeThatFits:CGSizeMake(contentLabelWidth, CGFLOAT_MAX)]; + resultHeight += (contentSize.height + kContentMarginBotom); + } + + if (self.timeLabel.text.length > 0) { + CGSize timeSize = [self.timeLabel sizeThatFits:CGSizeMake(contentLabelWidth, CGFLOAT_MAX)]; + resultHeight += timeSize.height; + } + + resultSize.height = resultHeight; + NSLog(@"%@ 的 cell 的 sizeThatFits: 被调用(说明这个 cell 的高度重新计算了一遍)", self.nameLabel.text); + return resultSize; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + CGFloat contentLabelWidth = CGRectGetWidth(self.contentView.bounds) - UIEdgeInsetsGetHorizontalValue(kInsets); + self.avatarImageView.frame = CGRectSetXY(self.avatarImageView.frame, kInsets.left, kInsets.top); + if (self.nameLabel.text.length > 0) { + CGFloat nameLabelWidth = contentLabelWidth - CGRectGetWidth(self.avatarImageView.bounds) - kAvatarMarginRight; + CGSize nameSize = [self.nameLabel sizeThatFits:CGSizeMake(nameLabelWidth, CGFLOAT_MAX)]; + self.nameLabel.frame = CGRectFlatMake(CGRectGetMaxX(self.avatarImageView.frame) + kAvatarMarginRight, CGRectGetMinY(self.avatarImageView.frame) + (CGRectGetHeight(self.avatarImageView.bounds) - nameSize.height) / 2, nameLabelWidth, nameSize.height); + } + if (self.contentLabel.text.length > 0) { + CGSize contentSize = [self.contentLabel sizeThatFits:CGSizeMake(contentLabelWidth, CGFLOAT_MAX)]; + self.contentLabel.frame = CGRectFlatMake(kInsets.left, CGRectGetMaxY(self.avatarImageView.frame) + kAvatarMarginBottom, contentLabelWidth, contentSize.height); + } + if (self.timeLabel.text.length > 0) { + CGSize timeSize = [self.timeLabel sizeThatFits:CGSizeMake(contentLabelWidth, CGFLOAT_MAX)]; + self.timeLabel.frame = CGRectFlatMake(CGRectGetMinX(self.contentLabel.frame), CGRectGetMaxY(self.contentLabel.frame) + kContentMarginBotom, contentLabelWidth, timeSize.height); + } +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.h b/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.h index 80c4afbb..1b0045a0 100644 --- a/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.h @@ -2,7 +2,7 @@ // QDEmotionsViewController.h // qmuidemo // -// Created by MoLice on 16/9/6. +// Created by QMUI Team on 16/9/6. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.m b/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.m index 17175ca4..14f499e5 100644 --- a/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDEmotionsViewController.m @@ -2,7 +2,7 @@ // QDEmotionsViewController.m // qmuidemo // -// Created by MoLice on 16/9/6. +// Created by QMUI Team on 16/9/6. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -13,7 +13,7 @@ @interface QDEmotionsViewController () @property(nonatomic, strong) UILabel *descriptionLabel; @property(nonatomic, strong) UIView *toolbar; @property(nonatomic, strong) QMUITextField *textField; -@property(nonatomic, strong) QMUIQQEmotionManager *qqEmotionManager; +@property(nonatomic, strong) QMUIEmotionInputManager *emotionInputManager; @property(nonatomic, assign) BOOL keyboardVisible; @property(nonatomic, assign) CGFloat keyboardHeight; @end @@ -25,7 +25,7 @@ - (void)initSubviews { self.descriptionLabel = [[UILabel alloc] init]; self.descriptionLabel.numberOfLines = 0; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"本界面以 QMUIQQEmotionManager 为例,展示 QMUIEmotionView 的功能,若需查看 QMUIEmotionView 的使用方式,请参考 QMUIQQEmotionManager。" 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]; @@ -34,11 +34,12 @@ - (void)initSubviews { [self.view addSubview:self.descriptionLabel]; self.toolbar = [[UIView alloc] init]; - self.toolbar.qmui_borderPosition = QMUIBorderViewPositionTop; - self.toolbar.backgroundColor = UIColorWhite; + self.toolbar.qmui_borderPosition = QMUIViewBorderPositionTop; + 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; @@ -69,45 +70,84 @@ - (void)initSubviews { }; [self.toolbar addSubview:self.textField]; - self.qqEmotionManager = [[QMUIQQEmotionManager alloc] init]; - self.qqEmotionManager.boundTextField = self.textField; - self.qqEmotionManager.emotionView.qmui_borderPosition = QMUIBorderViewPositionTop; - [self.view addSubview:self.qqEmotionManager.emotionView]; + self.emotionInputManager = [[QMUIEmotionInputManager alloc] init]; + self.emotionInputManager.emotionView.emotions = [QDUIHelper qmuiEmotions]; + self.emotionInputManager.emotionView.qmui_borderPosition = QMUIViewBorderPositionTop; + self.emotionInputManager.boundTextField = self.textField; + [self.view addSubview:self.emotionInputManager.emotionView]; + + self.toolbar.alpha = 0; + 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; +} + +// 布局时依赖 self.view.safeAreaInset.bottom,但由于前一个界面有 tabBar,导致 push 进来后第一次布局,self.view.safeAreaInset.bottom 依然是以存在 tabBar 的方式来计算的,所以会有跳动,简单处理,这里通过动画来掩饰这个跳动,哈哈 +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + self.toolbar.transform = CGAffineTransformMakeTranslation(0, self.view.qmui_height - self.toolbar.qmui_top); + self.emotionInputManager.emotionView.transform = CGAffineTransformMakeTranslation(0, self.view.qmui_height - self.emotionInputManager.emotionView.qmui_top); + [UIView animateWithDuration:.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ + self.toolbar.alpha = 1; + self.emotionInputManager.emotionView.alpha = 1; + self.toolbar.transform = CGAffineTransformIdentity; + self.emotionInputManager.emotionView.transform = CGAffineTransformIdentity; + } completion:NULL]; + + self.textField.qmui_keyboardManager.delegateEnabled = YES; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsMake(20, 20, 20, 20); + UIEdgeInsets padding = UIEdgeInsetsMake(20, 20 + self.view.safeAreaInsets.left, 20, 20 + self.view.safeAreaInsets.right); CGFloat contentWidth = CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding); - CGSize descriptionLabelSize = [self.descriptionLabel sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)]; - self.descriptionLabel.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.navigationController.navigationBar.frame) + padding.top, contentWidth, descriptionLabelSize.height); + 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.qqEmotionManager.emotionView.frame = CGRectMake(0, CGRectGetMaxY(self.toolbar.frame), CGRectGetWidth(self.view.bounds), emotionViewHeight); + self.emotionInputManager.emotionView.frame = CGRectMake(0, CGRectGetMaxY(self.toolbar.frame), CGRectGetWidth(self.view.bounds), emotionViewHeight); } else { - self.qqEmotionManager.emotionView.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - emotionViewHeight, CGRectGetWidth(self.view.bounds), emotionViewHeight); - self.toolbar.frame = CGRectMake(0, CGRectGetMinY(self.qqEmotionManager.emotionView.frame) - toolbarHeight, CGRectGetWidth(self.view.bounds), toolbarHeight); + 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 = UIEdgeInsetsMake(2, 8, 2, 8); + 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)); } -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { - [super touchesBegan:touches withEvent:event]; - [self.view endEditing:YES]; +- (BOOL)shouldHideKeyboardWhenTouchInView:(UIView *)view { + if ([view isDescendantOfView:self.toolbar]) { + // 输入框并非撑满 toolbar 的,所以有可能点击到 toolbar 里空白的地方,此时保持键盘状态不变 + return NO; + } + 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 { // 告诉 qqEmotionManager 输入框的光标位置发生变化,以保证表情插入在光标之后 - self.qqEmotionManager.selectedRangeForBoundTextInput = self.textField.qmui_selectedRange; + self.emotionInputManager.selectedRangeForBoundTextInput = self.textField.qmui_selectedRange; return YES; } diff --git a/qmuidemo/Modules/Demos/Components/QDEmptyViewController.h b/qmuidemo/Modules/Demos/Components/QDEmptyViewController.h index 61a8a86f..1a8361df 100644 --- a/qmuidemo/Modules/Demos/Components/QDEmptyViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDEmptyViewController.h @@ -2,7 +2,7 @@ // QDEmptyViewController.h // qmui // -// Created by MoLice on 14-7-3. +// Created by QMUI Team on 14-7-3. // Copyright (c) 2014年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDEmptyViewController.m b/qmuidemo/Modules/Demos/Components/QDEmptyViewController.m index e7f9eb17..97584f42 100644 --- a/qmuidemo/Modules/Demos/Components/QDEmptyViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDEmptyViewController.m @@ -2,7 +2,7 @@ // QDEmptyViewController.m // qmui // -// Created by MoLice on 14-7-3. +// Created by QMUI Team on 14-7-3. // Copyright (c) 2014年 QMUI Team. All rights reserved. // @@ -10,11 +10,12 @@ @interface QDEmptyViewController () +@property(nonatomic, strong) UITapGestureRecognizer *tapGesture; @end @implementation QDEmptyViewController -- (id)initWithStyle:(UITableViewStyle)style { +- (instancetype)initWithStyle:(UITableViewStyle)style { if (self = [super initWithStyle:style]) { self.shouldShowSearchBar = YES; } @@ -23,15 +24,21 @@ - (id)initWithStyle:(UITableViewStyle)style { #pragma mark - 工具方法 -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; +- (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; } -- (void)setToolbarItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setToolbarItemsIsInEditMode:isInEditMode animated:animated]; +- (void)handleTapGesture:(UITapGestureRecognizer *)tap { + [self reload]; } -- (void)reload:(id)sender { +- (void)reload { [self hideEmptyView]; [self.tableView reloadData]; } @@ -46,7 +53,7 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N static NSString *identifier = @"cell"; QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; if (!cell) { - cell = [[QMUITableViewCell alloc] initForTableView:self.tableView withReuseIdentifier:identifier]; + cell = [[QMUITableViewCell alloc] initForTableView:tableView withReuseIdentifier:identifier]; } [cell updateCellAppearanceWithIndexPath:indexPath]; NSInteger row = indexPath.row; @@ -70,21 +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(@"image1") text:nil detailText:@"图片间距可通过imageInsets来调整" buttonTitle:nil buttonAction:NULL]; + [self showEmptyViewWithImage:UIImageMake(@"icon_grid_emptyView") text:nil detailText:@"图片间距可通过imageInsets来调整" buttonTitle:nil buttonAction:NULL]; } [self.tableView reloadData]; } -#pragma mark - - -- (void)willPresentSearchController:(QMUISearchController *)searchController { - [QMUIHelper renderStatusBarStyleDark]; -} - -- (void)willDismissSearchController:(QMUISearchController *)searchController { - [QMUIHelper renderStatusBarStyleLight]; -} - @end diff --git a/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.h b/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.h index 8c4a4fc2..7d316000 100644 --- a/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.h @@ -2,7 +2,7 @@ // QDFloatLayoutViewController.h // qmuidemo // -// Created by MoLice on 2016/11/10. +// Created by QMUI Team on 2016/11/10. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.m b/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.m index de2d89ca..8c9b16d0 100644 --- a/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDFloatLayoutViewController.m @@ -2,7 +2,7 @@ // QDFloatLayoutViewController.m // qmuidemo // -// Created by MoLice on 2016/11/10. +// Created by QMUI Team on 2016/11/10. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -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,10 +37,8 @@ - (void)initSubviews { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - UIEdgeInsets padding = UIEdgeInsetsMake(CGRectGetMaxY(self.navigationController.navigationBar.frame) + 36, 24, 36, 24); - CGFloat contentWidth = CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding); - CGSize floatLayoutViewSize = [self.floatLayoutView sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)]; - self.floatLayoutView.frame = CGRectMake(padding.left, padding.top, contentWidth, floatLayoutViewSize.height); + 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); } @end diff --git a/qmuidemo/Modules/Demos/Components/QDGridViewController.h b/qmuidemo/Modules/Demos/Components/QDGridViewController.h index e8df174e..7ccd4a4f 100644 --- a/qmuidemo/Modules/Demos/Components/QDGridViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDGridViewController.h @@ -2,7 +2,7 @@ // QDGridViewController.h // qmui // -// Created by MoLice on 15/1/30. +// Created by QMUI Team on 15/1/30. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDGridViewController.m b/qmuidemo/Modules/Demos/Components/QDGridViewController.m index 9709feff..c3e6614d 100644 --- a/qmuidemo/Modules/Demos/Components/QDGridViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDGridViewController.m @@ -2,7 +2,7 @@ // QDGridViewController.m // qmui // -// Created by MoLice on 15/1/30. +// Created by QMUI Team on 15/1/30. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -36,20 +36,18 @@ - (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 + CGRectGetMaxY(self.navigationController.navigationBar.frame), 24, 24, 24); + 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); - CGFloat gridViewHeight = [self.gridView sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)].height; - self.gridView.frame = CGRectMake(padding.left, padding.top, contentWidth, gridViewHeight); + self.gridView.frame = CGRectMake(padding.left, padding.top, contentWidth, QMUIViewSelfSizingHeight); - CGFloat tipsLabelHeight = [self.tipsLabel sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)].height; - self.tipsLabel.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.gridView.frame) + 16, contentWidth, tipsLabelHeight); + self.tipsLabel.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.gridView.frame) + 16, contentWidth, QMUIViewSelfSizingHeight); } @end diff --git a/qmuidemo/Modules/Demos/Components/QDImagePickerExampleViewController.h b/qmuidemo/Modules/Demos/Components/QDImagePickerExampleViewController.h index 4bed0957..e938b2da 100644 --- a/qmuidemo/Modules/Demos/Components/QDImagePickerExampleViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDImagePickerExampleViewController.h @@ -2,7 +2,7 @@ // QDImagePickerExampleViewController.h // qmuidemo // -// Created by Kayo Lee on 15/5/16. +// Created by QMUI Team on 15/5/16. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDImagePickerExampleViewController.m b/qmuidemo/Modules/Demos/Components/QDImagePickerExampleViewController.m index c564dff2..2b02f4ff 100644 --- a/qmuidemo/Modules/Demos/Components/QDImagePickerExampleViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDImagePickerExampleViewController.m @@ -2,13 +2,12 @@ // QDImagePickerExampleViewController.m // qmuidemo // -// Created by Kayo Lee on 15/5/16. +// Created by QMUI Team on 15/5/16. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import "QDImagePickerExampleViewController.h" #import "QDNavigationController.h" -#import #define MaxSelectedImageCount 9 #define NormalImagePickingTag 1045 @@ -21,6 +20,7 @@ @interface QDImagePickerExampleViewController () @property(nonatomic, strong) UIImage *selectedAvatarImage; + @end @implementation QDImagePickerExampleViewController @@ -44,7 +44,6 @@ - (void)didSelectCellWithTitle:(NSString *)title { } - (void)authorizationPresentAlbumViewControllerWithTitle:(NSString *)title { - // 请求访问照片库的权限,在 iOS 8 或以上版本中可以利用这个方法弹出 Alert 询问用户是否授权 if ([QMUIAssetsManager authorizationStatus] == QMUIAssetAuthorizationStatusNotDetermined) { [QMUIAssetsManager requestAuthorization:^(QMUIAssetAuthorizationStatus status) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -77,14 +76,7 @@ - (void)presentAlbumViewControllerWithTitle:(NSString *)title { QDNavigationController *navigationController = [[QDNavigationController alloc] initWithRootViewController:albumViewController]; // 获取最近发送图片时使用过的相簿,如果有则直接进入该相簿 - QMUIAssetsGroup *assetsGroup = [QMUIImagePickerHelper assetsGroupOfLastestPickerAlbumWithUserIdentify:nil]; - if (assetsGroup) { - QMUIImagePickerViewController *imagePickerViewController = [self imagePickerViewControllerForAlbumViewController:albumViewController]; - - [imagePickerViewController refreshWithAssetsGroup:assetsGroup]; - imagePickerViewController.title = [assetsGroup name]; - [navigationController pushViewController:imagePickerViewController animated:NO]; - } + [albumViewController pickLastAlbumGroupDirectlyIfCan]; [self presentViewController:navigationController animated:YES completion:NULL]; } @@ -129,10 +121,8 @@ - (QMUIImagePickerViewController *)imagePickerViewControllerForAlbumViewControll - (void)imagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController didFinishPickingImageWithImagesAssetArray:(NSMutableArray *)imagesAssetArray { // 储存最近选择了图片的相册,方便下次直接进入该相册 [QMUIImagePickerHelper updateLastestAlbumWithAssetsGroup:imagePickerViewController.assetsGroup ablumContentType:kAlbumContentType userIdentify:nil]; - // 显示 loading - [self startLoading]; - // 使用 delay 模拟网络请求时长 - [self performSelector:@selector(showTipLabelWithText:) withObject:[NSString stringWithFormat:@"成功发送%@张图片", @([imagesAssetArray count])] afterDelay:1.5]; + + [self sendImageWithImagesAssetArray:imagesAssetArray]; } - (QMUIImagePickerPreviewViewController *)imagePickerPreviewViewControllerForImagePickerViewController:(QMUIImagePickerViewController *)imagePickerViewController { @@ -166,8 +156,16 @@ - (QMUIImagePickerPreviewViewController *)imagePickerPreviewViewControllerForIma #pragma mark - - (void)imagePickerPreviewViewController:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController didCheckImageAtIndex:(NSInteger)index { + [self updateImageCountLabelForPreviewView:imagePickerPreviewViewController]; +} + +- (void)imagePickerPreviewViewController:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController didUncheckImageAtIndex:(NSInteger)index { + [self updateImageCountLabelForPreviewView:imagePickerPreviewViewController]; +} + +// 更新选中的图片数量 +- (void)updateImageCountLabelForPreviewView:(QMUIImagePickerPreviewViewController *)imagePickerPreviewViewController { if (imagePickerPreviewViewController.view.tag == MultipleImagePickingTag) { - // 在预览界面选择图片时,控制显示当前所选的图片,并且展示动画 QDMultipleImagePickerPreviewViewController *customImagePickerPreviewViewController = (QDMultipleImagePickerPreviewViewController *)imagePickerPreviewViewController; NSUInteger selectedCount = [imagePickerPreviewViewController.selectedImageAssetArray count]; if (selectedCount > 0) { @@ -185,16 +183,8 @@ - (void)imagePickerPreviewViewController:(QMUIImagePickerPreviewViewController * - (void)imagePickerPreviewViewController:(QDMultipleImagePickerPreviewViewController *)imagePickerPreviewViewController sendImageWithImagesAssetArray:(NSMutableArray *)imagesAssetArray { // 储存最近选择了图片的相册,方便下次直接进入该相册 [QMUIImagePickerHelper updateLastestAlbumWithAssetsGroup:imagePickerPreviewViewController.assetsGroup ablumContentType:kAlbumContentType userIdentify:nil]; - // 显示 loading - [self startLoading]; - // 使用 delay 模拟网络请求时长 - NSString *succeedTip; - if (imagePickerPreviewViewController.shouldUseOriginImage) { - succeedTip = @"成功发送%@张原图"; - } else { - succeedTip = @"成功发送%@张图片"; - } - [self performSelector:@selector(showTipLabelWithText:) withObject:[NSString stringWithFormat:succeedTip, @([imagesAssetArray count])] afterDelay:1.5]; + + [self sendImageWithImagesAssetArray:imagesAssetArray]; } #pragma mark - @@ -204,7 +194,20 @@ - (void)imagePickerPreviewViewController:(QDSingleImagePickerPreviewViewControll [QMUIImagePickerHelper updateLastestAlbumWithAssetsGroup:imagePickerPreviewViewController.assetsGroup ablumContentType:kAlbumContentType userIdentify:nil]; // 显示 loading [self startLoading]; - [self performSelector:@selector(setAvatarWithAvatarImage:) withObject:[imageAsset previewImage] afterDelay:1.8]; + [imageAsset requestImageData:^(NSData *imageData, NSDictionary *info, BOOL isGif, BOOL isHEIC) { + UIImage *targetImage = nil; + if (isGif) { + targetImage = [UIImage qmui_animatedImageWithData:imageData]; + } else { + targetImage = [UIImage imageWithData:imageData]; + if (isHEIC) { + // iOS 11 中新增 HEIF/HEVC 格式的资源,直接发送新格式的照片到不支持新格式的设备,照片可能会无法识别,可以先转换为通用的 JPEG 格式再进行使用。 + // 详细请浏览:https://github.com/Tencent/QMUI_iOS/issues/224 + targetImage = [UIImage imageWithData:UIImageJPEGRepresentation(targetImage, 1)]; + } + } + [self performSelector:@selector(setAvatarWithAvatarImage:) withObject:targetImage afterDelay:1.8]; + }]; } #pragma mark - 业务方法 @@ -213,6 +216,10 @@ - (void)startLoading { [QMUITips showLoadingInView:self.view]; } +- (void)startLoadingWithText:(NSString *)text { + [QMUITips showLoading:text inView:self.view]; +} + - (void)stopLoading { [QMUITips hideAllToastInView:self.view animated:YES]; } @@ -226,6 +233,32 @@ - (void)hideTipLabel { [QMUITips hideAllToastInView:self.view animated:YES]; } +- (void)sendImageWithImagesAssetArrayIfDownloadStatusSucceed:(NSMutableArray *)imagesAssetArray { + if ([QMUIImagePickerHelper imageAssetsDownloaded:imagesAssetArray]) { + // 所有资源从 iCloud 下载成功,模拟发送图片到服务器 + // 显示发送中 + [self showTipLabelWithText:@"发送中"]; + // 使用 delay 模拟网络请求时长 + [self performSelector:@selector(showTipLabelWithText:) withObject:[NSString stringWithFormat:@"成功发送%@个资源", @([imagesAssetArray count])] afterDelay:1.5]; + } +} + +- (void)sendImageWithImagesAssetArray:(NSMutableArray *)imagesAssetArray { + __weak __typeof(self)weakSelf = self; + + for (QMUIAsset *asset in imagesAssetArray) { + [QMUIImagePickerHelper requestImageAssetIfNeeded:asset completion:^(QMUIAssetDownloadStatus downloadStatus, NSError *error) { + if (downloadStatus == QMUIAssetDownloadStatusDownloading) { + [weakSelf startLoadingWithText:@"从 iCloud 加载中"]; + } else if (downloadStatus == QMUIAssetDownloadStatusSucceed) { + [weakSelf sendImageWithImagesAssetArrayIfDownloadStatusSucceed:imagesAssetArray]; + } else { + [weakSelf showTipLabelWithText:@"iCloud 下载错误,请重新选图"]; + } + }]; + } +} + - (void)setAvatarWithAvatarImage:(UIImage *)avatarImage { [self stopLoading]; self.selectedAvatarImage = avatarImage; diff --git a/qmuidemo/Modules/Demos/Components/QDImagePreviewExampleViewController.h b/qmuidemo/Modules/Demos/Components/QDImagePreviewExampleViewController.h deleted file mode 100644 index 678db050..00000000 --- a/qmuidemo/Modules/Demos/Components/QDImagePreviewExampleViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDImagePreviewExampleViewController.h -// qmuidemo -// -// Created by MoLice 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 612c1daa..00000000 --- a/qmuidemo/Modules/Demos/Components/QDImagePreviewExampleViewController.m +++ /dev/null @@ -1,31 +0,0 @@ -// -// QDImagePreviewExampleViewController.m -// qmuidemo -// -// Created by MoLice 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.h b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController1.h index b770fb11..1c491a40 100644 --- a/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController1.h +++ b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController1.h @@ -2,7 +2,7 @@ // QDImagePreviewViewController1.h // qmuidemo // -// Created by MoLice on 2016/12/6. +// Created by QMUI Team on 2016/12/6. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController1.m b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController1.m index a4c5b6ec..dcc36d37 100644 --- a/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController1.m +++ b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController1.m @@ -2,7 +2,7 @@ // QDImagePreviewViewController1.m // qmuidemo // -// Created by MoLice on 2016/12/6. +// Created by QMUI Team on 2016/12/6. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -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"), @@ -44,17 +42,17 @@ - (void)initSubviews { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - CGFloat originY = CGRectGetMaxY(self.navigationController.navigationBar.frame); + CGFloat originY = self.qmui_navigationBarMaxYInViewCoordinator; CGSize imageSize = self.images.firstObject.size; CGSize imagePreviewViewSize = CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetWidth(self.view.bounds) * imageSize.height / imageSize.width); - imagePreviewViewSize.height = fminf(CGRectGetHeight(self.view.bounds) - originY, imagePreviewViewSize.height); + imagePreviewViewSize.height = fmin(CGRectGetHeight(self.view.bounds) - originY, imagePreviewViewSize.height); self.imagePreviewView.frame = CGRectFlatMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), imagePreviewViewSize.width), originY, imagePreviewViewSize.width, imagePreviewViewSize.height); } -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; +- (void)setupNavigationItems { + [super setupNavigationItems]; self.title = [self titleForIndex:self.imagePreviewView.currentImageIndex]; } diff --git a/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.h b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.h index d5a5f640..223f3f99 100644 --- a/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.h +++ b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.h @@ -2,7 +2,7 @@ // QDImagePreviewViewController2.h // qmuidemo // -// Created by MoLice on 2016/12/6. +// Created by QMUI Team on 2016/12/6. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.m b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.m index eb9edce0..8a0c7e81 100644 --- a/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.m +++ b/qmuidemo/Modules/Demos/Components/QDImagePreviewViewController2.m @@ -2,7 +2,7 @@ // QDImagePreviewViewController2.m // qmuidemo // -// Created by MoLice on 2016/12/6. +// Created by QMUI Team on 2016/12/6. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -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,35 +34,67 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB - (void)initSubviews { [super initSubviews]; - self.imageButton = [[UIButton alloc] init]; - [self.imageButton setImage:self.images[2] forState:UIControlStateNormal]; - [self.imageButton addTarget:self action:@selector(handleImageButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [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.width / 2, self.images.firstObject.size.height / 2); - self.imageButton.frame = CGRectFlatMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), imageButtonSize.width), CGRectGetMaxY(self.navigationController.navigationBar.frame) + 24, imageButtonSize.width, imageButtonSize.height); + 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); - CGFloat labelWidth = CGRectGetWidth(self.view.bounds) - 32 * 2; - CGFloat tipsLabelHeight = [self.tipsLabel sizeThatFits:CGSizeMake(labelWidth, CGFLOAT_MAX)].height; - self.tipsLabel.frame = CGRectFlatMake(32, CGRectGetMaxY(self.imageButton.frame) + 8, labelWidth, tipsLabelHeight); + self.tipsLabel.frame = CGRectFlatMake(margins.left, CGRectGetMaxY(self.floatLayoutView.frame) + 16, contentWidth, QMUIViewSelfSizingHeight); } - (void)handleImageButtonEvent:(UIButton *)button { if (!self.imagePreviewViewController) { self.imagePreviewViewController = [[QMUIImagePreviewViewController alloc] init]; - self.imagePreviewViewController.imagePreviewView.delegate = self; - self.imagePreviewViewController.imagePreviewView.currentImageIndex = 2;// 默认查看的图片的 index + self.imagePreviewViewController.presentingStyle = QMUIImagePreviewViewControllerTransitioningStyleZoom;// 将 present 动画改为 zoom,也即从某个位置放大到屏幕中央。默认样式为 fade。 + self.imagePreviewViewController.imagePreviewView.delegate = self;// 将内部的图片查看器 delegate 指向当前 viewController,以获取要查看的图片数据 + + // 当需要在退出大图预览时做一些事情的时候,可配合 UIViewController (QMUI) 的 qmui_visibleStateDidChangeBlock 来实现。 + __weak __typeof(self)weakSelf = self; + self.imagePreviewViewController.qmui_visibleStateDidChangeBlock = ^(QMUIImagePreviewViewController *viewController, QMUIViewControllerVisibleState visibleState) { + if (visibleState == QMUIViewControllerWillDisappear) { + NSInteger exitAtIndex = viewController.imagePreviewView.currentImageIndex; + weakSelf.tipsLabel.text = [NSString stringWithFormat:@"浏览到第%@张就退出了", @(exitAtIndex + 1)]; + } + }; } - [self.imagePreviewViewController startPreviewFromRectInScreen:[self.imageButton convertRect:self.imageButton.imageView.frame toView:nil]]; + + 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]; } #pragma mark - @@ -73,18 +104,39 @@ - (NSUInteger)numberOfImagesInImagePreviewView:(QMUIImagePreviewView *)imagePrev } - (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView renderZoomImageView:(QMUIZoomImageView *)zoomImageView atIndex:(NSUInteger)index { - zoomImageView.image = self.images[index]; + zoomImageView.reusedIdentifier = @(index); + + // 模拟异步加载的情况 + if (index == 2) { + [zoomImageView showLoading]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if ([zoomImageView.reusedIdentifier isEqual:@(index)]) { + [zoomImageView hideEmptyView]; + zoomImageView.image = self.images[index]; + } + }); + } else { + zoomImageView.image = self.images[index]; + } } - (QMUIImagePreviewMediaType)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView assetTypeAtIndex:(NSUInteger)index { 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 { - [self.imageButton setImage:zoomImageView.image forState:UIControlStateNormal]; - [self.imagePreviewViewController endPreviewToRectInScreen:[self.imageButton convertRect:self.imageButton.imageView.frame toView:nil]]; + // 退出图片预览 + [self dismissViewControllerAnimated:YES completion:nil]; } @end diff --git a/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.h b/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.h index e87d0f42..3dcdab17 100644 --- a/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.h @@ -2,7 +2,7 @@ // QDKeyboardViewController.h // qmuidemo // -// Created by zhoonchen on 2017/3/27. +// Created by QMUI Team on 2017/3/27. // Copyright © 2017年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.m b/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.m index e9ae98e9..ec0c0992 100644 --- a/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDKeyboardViewController.m @@ -2,14 +2,14 @@ // QDKeyboardViewController.m // qmuidemo // -// Created by zhoonchen on 2017/3/27. +// Created by QMUI Team on 2017/3/27. // Copyright © 2017年 QMUI Team. All rights reserved. // #import "QDKeyboardViewController.h" -static CGFloat const kToolbarHeight = 50; +static CGFloat const kToolbarHeight = 56; static CGFloat const kEmotionViewHeight = 232; @interface QDKeyboardCustomViewController : QDCommonViewController @@ -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,12 +56,18 @@ - (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 = QMUIBorderViewPositionTop; + self.toolbarView.qmui_borderPosition = QMUIViewBorderPositionTop; [self.containerView addSubview:self.toolbarView]; _cancelButton = [[QMUIButton alloc] init]; @@ -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 = CGRectApplyAffineTransform(containerRect, self.containerView.transform); + 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)); @@ -97,21 +103,32 @@ - (void)viewDidLayoutSubviews { self.textView.frame = CGRectFlatMake(0, 0, CGRectGetWidth(self.containerView.bounds), CGRectGetHeight(self.containerView.bounds) - kToolbarHeight); } +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + self.navigationController.interactivePopGestureRecognizer.enabled = NO; +} + - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self.view endEditing:YES]; } +- (void)removeFromParentViewController { + self.navigationController.interactivePopGestureRecognizer.enabled = YES; + [super removeFromParentViewController]; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + return UIInterfaceOrientationMaskPortrait; +} + - (void)showInParentViewController:(UIViewController *)controller { - if (IS_LANDSCAPE) { - [QDUIHelper forceInterfaceOrientationPortrait]; - } - // 这一句访问了self.view,触发viewDidLoad: self.view.frame = controller.view.bounds; // 需要先布局好 + [controller addChildViewController:self]; [controller.view addSubview:self.view]; [self.view layoutIfNeeded]; @@ -123,6 +140,7 @@ - (void)showInParentViewController:(UIViewController *)controller { [UIView animateWithDuration:.25 delay:0.0 options:QMUIViewAnimationOptionsCurveOut animations:^{ self.maskControl.alpha = 1.0; } completion:^(BOOL finished) { + [self didMoveToParentViewController:self]; // 这一句触发viewDidAppear: [self endAppearanceTransition]; }]; @@ -131,19 +149,33 @@ - (void)showInParentViewController:(UIViewController *)controller { } - (void)hide { + [self willMoveToParentViewController:nil]; // 这一句触发viewWillDisappear: [self beginAppearanceTransition:NO animated:YES]; [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 - @@ -152,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]; }]; } @@ -180,7 +212,7 @@ @interface QDKeyboardViewController () @property(nonatomic, strong) CALayer *separatorLayer; -@property(nonatomic, strong) QMUIQQEmotionManager *qqEmotionManager; +@property(nonatomic, strong) QMUIEmotionInputManager *emotionInputManager; @end @@ -196,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"]) { @@ -219,16 +251,16 @@ - (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 = QMUIBorderViewPositionTop; + self.toolbarView.qmui_borderPosition = QMUIViewBorderPositionTop; [self.view addSubview:self.toolbarView]; _toolbarTextField = [[QMUITextField alloc] init]; 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; @@ -247,16 +279,17 @@ - (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]; [self.toolbarView addSubview:self.faceButton]; - self.qqEmotionManager = [[QMUIQQEmotionManager alloc] init]; - self.qqEmotionManager.boundTextField = self.toolbarTextField; - self.qqEmotionManager.emotionView.qmui_borderPosition = QMUIBorderViewPositionTop; - [self.view addSubview:self.qqEmotionManager.emotionView]; + self.emotionInputManager = [[QMUIEmotionInputManager alloc] init]; + self.emotionInputManager.boundTextField = self.toolbarTextField; + self.emotionInputManager.emotionView.qmui_borderPosition = QMUIViewBorderPositionTop; + self.emotionInputManager.emotionView.emotions = [QDUIHelper qmuiEmotions]; + [self.view addSubview:self.emotionInputManager.emotionView]; } - (UIInterfaceOrientationMask)supportedInterfaceOrientations { @@ -267,10 +300,24 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations { } } +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + self.toolbarTextField.qmui_keyboardManager.delegateEnabled = YES; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + // 避免手势返回的时候输入框往下掉 + self.toolbarTextField.qmui_keyboardManager.delegateEnabled = NO; +} + +- (BOOL)shouldAutomaticallyForwardAppearanceMethods { + return NO;// 那个 childViewController 是加到 navController.view 上的,所以需要手动管理 +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - CGRect toolbarRect = CGRectFlatMake(0, CGRectGetHeight(self.view.bounds), CGRectGetWidth(self.view.bounds), kToolbarHeight); - self.toolbarView.frame = CGRectApplyAffineTransform(toolbarRect, self.toolbarView.transform); + self.toolbarView.qmui_frameApplyTransform = CGRectFlatMake(0, CGRectGetHeight(self.view.bounds), CGRectGetWidth(self.view.bounds), kToolbarHeight); CGFloat textFieldInset = 8; CGFloat textFieldHeight = kToolbarHeight - textFieldInset * 2; @@ -291,12 +338,13 @@ - (void)viewDidLayoutSubviews { CGSize contentSize = [self.contentLabel sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)]; CGFloat commentButtonHeight = CGRectGetHeight(self.commentButton.bounds); CGFloat writeReviewButtonHeight = CGRectGetHeight(self.writeReviewButton.bounds); + CGFloat navigationBarHeight = self.qmui_navigationBarMaxYInViewCoordinator; - if (CGRectGetHeight(self.view.bounds) - CGRectGetMaxY(self.navigationController.navigationBar.frame) < contentSize.height + contentLabelInsetVertical * 2 + contentOffsetY + commentButtonHeight + writeReviewButtonHeight + buttonSpacing + buttonSectionInset * 2) { - buttonSectionInset = (CGRectGetHeight(self.view.bounds) - CGRectGetMaxY(self.navigationController.navigationBar.frame) - contentSize.height - contentLabelInsetVertical * 2 - contentOffsetY - commentButtonHeight - writeReviewButtonHeight - buttonSpacing) / 2; + if (CGRectGetHeight(self.view.bounds) - navigationBarHeight < contentSize.height + contentLabelInsetVertical * 2 + contentOffsetY + commentButtonHeight + writeReviewButtonHeight + buttonSpacing + buttonSectionInset * 2) { + buttonSectionInset = (CGRectGetHeight(self.view.bounds) - navigationBarHeight - contentSize.height - contentLabelInsetVertical * 2 - contentOffsetY - commentButtonHeight - writeReviewButtonHeight - buttonSpacing) / 2; } - self.contentLabel.frame = CGRectFlatMake(contentLabelInsetHorizontal, CGRectGetMaxY(self.navigationController.navigationBar.frame) + contentLabelInsetVertical - 6, contentWidth, contentSize.height); + self.contentLabel.frame = CGRectFlatMake(contentLabelInsetHorizontal, navigationBarHeight + contentLabelInsetVertical - 6, contentWidth, contentSize.height); self.separatorLayer.frame = CGRectFlatMake(0, CGRectGetMaxY(self.contentLabel.frame) + contentLabelInsetVertical, CGRectGetWidth(self.view.bounds), PixelOne); @@ -304,9 +352,9 @@ - (void)viewDidLayoutSubviews { self.writeReviewButton.frame = CGRectSetXY(self.writeReviewButton.frame, CGFloatGetCenter(CGRectGetWidth(self.view.bounds), CGRectGetWidth(self.writeReviewButton.bounds)), CGRectGetMaxY(self.commentButton.frame) + buttonSpacing); - if (self.qqEmotionManager.emotionView) { + if (self.emotionInputManager.emotionView) { CGRect emotionViewRect = CGRectMake(0, CGRectGetHeight(self.view.bounds), CGRectGetWidth(self.view.bounds), kEmotionViewHeight); - self.qqEmotionManager.emotionView.frame = CGRectApplyAffineTransform(emotionViewRect, self.qqEmotionManager.emotionView.transform); + self.emotionInputManager.emotionView.frame = CGRectApplyAffineTransformWithAnchorPoint(emotionViewRect, self.emotionInputManager.emotionView.transform, self.emotionInputManager.emotionView.layer.anchorPoint); } } @@ -328,6 +376,7 @@ - (void)handleWriteReviewItemEvent:(id)sender { } else { [self.customViewController.textView resignFirstResponder]; } + self.navigationController.interactivePopGestureRecognizer.enabled = NO; } - (void)handleCommentButtonEvent:(id)sender { @@ -347,17 +396,22 @@ - (void)handleFaceButtonEvent:(id)sender { } } -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { - [self.view endEditing:YES]; +- (BOOL)shouldHideKeyboardWhenTouchInView:(UIView *)view { + if (view == self.toolbarView) { + // 输入框并非撑满 toolbarView 的,所以有可能点击到 toolbarView 里空白的地方,此时保持键盘状态不变 + return NO; + } + if (self.faceButton.isSelected) { self.faceButton.selected = NO; [self hideToolbarViewWithKeyboardUserInfo:nil]; } + return YES; } - (void)showEmotionView { [UIView animateWithDuration:0.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - self.qqEmotionManager.emotionView.layer.transform = CATransform3DMakeTranslation(0, - CGRectGetHeight(self.qqEmotionManager.emotionView.bounds), 0); + self.emotionInputManager.emotionView.layer.transform = CATransform3DMakeTranslation(0, - CGRectGetHeight(self.emotionInputManager.emotionView.bounds), 0); } completion:NULL]; [self.toolbarTextField resignFirstResponder]; } @@ -370,7 +424,7 @@ - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { } - (BOOL)textFieldShouldEndEditing:(UITextField *)textField { - self.qqEmotionManager.selectedRangeForBoundTextInput = self.toolbarTextField.qmui_selectedRange; + self.emotionInputManager.selectedRangeForBoundTextInput = self.toolbarTextField.qmui_selectedRange; return YES; } @@ -380,14 +434,14 @@ - (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.qqEmotionManager.emotionView.layer.transform = CATransform3DMakeTranslation(0, - distanceFromBottom, 0); + self.emotionInputManager.emotionView.layer.transform = CATransform3DMakeTranslation(0, - distanceFromBottom, 0); } completion:NULL]; } else { // 相对于表情面板 [UIView animateWithDuration:0.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ - self.toolbarView.layer.transform = CATransform3DMakeTranslation(0, - CGRectGetHeight(self.qqEmotionManager.emotionView.bounds) - kToolbarHeight, 0); + self.toolbarView.layer.transform = CATransform3DMakeTranslation(0, - CGRectGetHeight(self.emotionInputManager.emotionView.bounds) - kToolbarHeight, 0); } completion:NULL]; } } @@ -396,12 +450,12 @@ - (void)hideToolbarViewWithKeyboardUserInfo:(QMUIKeyboardUserInfo *)keyboardUser if (keyboardUserInfo) { [QMUIKeyboardManager animateWithAnimated:YES keyboardUserInfo:keyboardUserInfo animations:^{ self.toolbarView.layer.transform = CATransform3DIdentity; - self.qqEmotionManager.emotionView.layer.transform = CATransform3DIdentity; + self.emotionInputManager.emotionView.layer.transform = CATransform3DIdentity; } completion:NULL]; } else { [UIView animateWithDuration:0.25 delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{ self.toolbarView.layer.transform = CATransform3DIdentity; - self.qqEmotionManager.emotionView.layer.transform = CATransform3DIdentity; + self.emotionInputManager.emotionView.layer.transform = CATransform3DIdentity; } 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.h b/qmuidemo/Modules/Demos/Components/QDMarqueeLabelViewController.h index 1e4b72bd..174832b7 100644 --- a/qmuidemo/Modules/Demos/Components/QDMarqueeLabelViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDMarqueeLabelViewController.h @@ -2,7 +2,7 @@ // QDMarqueeLabelViewController.h // qmuidemo // -// Created by MoLice on 2017/6/3. +// Created by QMUI Team on 2017/6/3. // Copyright © 2017年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDMarqueeLabelViewController.m b/qmuidemo/Modules/Demos/Components/QDMarqueeLabelViewController.m index 352c54dc..0da9eef4 100644 --- a/qmuidemo/Modules/Demos/Components/QDMarqueeLabelViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDMarqueeLabelViewController.m @@ -2,7 +2,7 @@ // QDMarqueeLabelViewController.m // qmuidemo // -// Created by MoLice on 2017/6/3. +// Created by QMUI Team on 2017/6/3. // Copyright © 2017年 QMUI Team. All rights reserved. // @@ -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 @@ -125,18 +137,21 @@ - (instancetype)initWithFrame:(CGRect)frame { self.backgroundColor = [QDCommonUI randomThemeColor]; self.layer.cornerRadius = 3; - self.label = [[QMUIMarqueeLabel alloc] initWithFont:UIFontMake(16) textColor:UIColorWhite]; + self.label = [[QMUIMarqueeLabel alloc] qmui_initWithFont:UIFontMake(16) textColor:UIColorWhite]; [self.label qmui_calculateHeightAfterSetAppearance]; - self.label.fadeStartColor = self.backgroundColor; - self.label.fadeEndColor = [self.backgroundColor colorWithAlphaComponent:0]; [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.h b/qmuidemo/Modules/Demos/Components/QDModalPresentationViewController.h index 3567c02d..325099e1 100644 --- a/qmuidemo/Modules/Demos/Components/QDModalPresentationViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDModalPresentationViewController.h @@ -2,7 +2,7 @@ // QDModalPresentationViewController.h // qmuidemo // -// Created by MoLice on 16/7/20. +// Created by QMUI Team on 16/7/20. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDModalPresentationViewController.m b/qmuidemo/Modules/Demos/Components/QDModalPresentationViewController.m index b16681b4..40cd7f50 100644 --- a/qmuidemo/Modules/Demos/Components/QDModalPresentationViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDModalPresentationViewController.m @@ -2,7 +2,7 @@ // QDModalPresentationViewController.m // qmuidemo // -// Created by MoLice on 16/7/20. +// Created by QMUI Team on 16/7/20. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -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]; @@ -88,9 +88,7 @@ - (void)handleShowContentView { [contentView addSubview:label]; UIEdgeInsets contentViewPadding = UIEdgeInsetsMake(20, 20, 20, 20); - CGFloat contentLimitWidth = CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding); - CGSize labelSize = [label sizeThatFits:CGSizeMake(contentLimitWidth, CGFLOAT_MAX)]; - label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, contentLimitWidth, labelSize.height); + label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding), QMUIViewSelfSizingHeight); QMUIModalPresentationViewController *modalViewController = [[QMUIModalPresentationViewController alloc] init]; modalViewController.contentView = contentView; @@ -102,20 +100,19 @@ - (void)handleShowContentViewController { QMUIModalPresentationViewController *modalViewController = [[QMUIModalPresentationViewController alloc] init]; modalViewController.contentViewController = contentViewController; - modalViewController.maximumContentViewWidth = CGFLOAT_MAX; [modalViewController showWithAnimated:YES completion:nil]; } - (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) { @@ -126,9 +123,7 @@ - (void)handleAnimationStyle { [contentView addSubview:label]; UIEdgeInsets contentViewPadding = UIEdgeInsetsMake(20, 20, 20, 20); - CGFloat contentLimitWidth = CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding); - CGSize labelSize = [label sizeThatFits:CGSizeMake(contentLimitWidth, CGFLOAT_MAX)]; - label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, contentLimitWidth, labelSize.height); + label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding), QMUIViewSelfSizingHeight); QMUIModalPresentationViewController *modalViewController = [[QMUIModalPresentationViewController alloc] init]; modalViewController.animationStyle = self.currentAnimationStyle % 3; @@ -140,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; @@ -151,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) { @@ -162,12 +159,14 @@ - (void)handleCustomDimmingView { [contentView addSubview:label]; UIEdgeInsets contentViewPadding = UIEdgeInsetsMake(20, 20, 20, 20); - CGFloat contentLimitWidth = CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding); - CGSize labelSize = [label sizeThatFits:CGSizeMake(contentLimitWidth, CGFLOAT_MAX)]; - label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, contentLimitWidth, labelSize.height); + 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]; @@ -178,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; @@ -186,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) { @@ -197,47 +196,24 @@ - (void)handleLayoutBlockAndAnimation { [contentView addSubview:label]; UIEdgeInsets contentViewPadding = UIEdgeInsetsMake(20, 20, 20, 20); - CGFloat contentLimitWidth = CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding); - CGSize labelSize = [label sizeThatFits:CGSizeMake(contentLimitWidth, CGFLOAT_MAX)]; - label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, contentLimitWidth, labelSize.height); + label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding), QMUIViewSelfSizingHeight); 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:.3 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:.3 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); @@ -254,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]; }]; @@ -264,27 +240,36 @@ - (void)handleKeyboard { label.attributedText = attributedString; [contentView addSubview:label]; - CGSize labelSize = [label sizeThatFits:CGSizeMake(contentLimitWidth, CGFLOAT_MAX)]; - label.frame = CGRectMake(contentViewPadding.left, CGRectGetMaxY(textField.frame) + 8, contentLimitWidth, labelSize.height); - - contentView.frame = CGRectSetHeight(contentView.frame, CGRectGetMaxY(label.frame) + contentViewPadding.bottom); + label.frame = CGRectMake(contentViewPadding.left, CGRectGetMaxY(textField.frame) + 8, contentLimitWidth, QMUIViewSelfSizingHeight); + 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,9 +278,7 @@ - (void)handleWindowShowing { [contentView addSubview:label]; UIEdgeInsets contentViewPadding = UIEdgeInsetsMake(20, 20, 20, 20); - CGFloat contentLimitWidth = CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding); - CGSize labelSize = [label sizeThatFits:CGSizeMake(contentLimitWidth, CGFLOAT_MAX)]; - label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, contentLimitWidth, labelSize.height); + label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding), QMUIViewSelfSizingHeight); QMUIModalPresentationViewController *modalViewController = [[QMUIModalPresentationViewController alloc] init]; modalViewController.contentView = contentView; @@ -305,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]; @@ -321,49 +304,58 @@ - (void)handlePresentShowing { [contentView addSubview:label]; UIEdgeInsets contentViewPadding = UIEdgeInsetsMake(20, 20, 20, 20); - CGFloat contentLimitWidth = CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding); - CGSize labelSize = [label sizeThatFits:CGSizeMake(contentLimitWidth, CGFLOAT_MAX)]; - label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, contentLimitWidth, labelSize.height); + label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding), QMUIViewSelfSizingHeight); QMUIModalPresentationViewController *modalViewController = [[QMUIModalPresentationViewController alloc] init]; modalViewController.contentView = contentView; - // 以 presentViewController 的形式展示 - [self presentViewController:modalViewController animated:NO completion:nil]; + // 以 presentViewController 的形式展示时,animated 要传 NO,否则系统的动画会覆盖 QMUIModalPresentationAnimationStyle 的动画 + [self presentViewController:modalViewController animated:NO completion:NULL]; } - (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, CGRectGetMaxY(self.navigationController.navigationBar.frame) + 40, CGRectGetWidth(self.view.bounds) - 40 * 2, CGRectGetHeight(self.view.bounds) - CGRectGetMaxY(self.navigationController.navigationBar.frame) - 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); - CGFloat contentLimitWidth = CGRectGetWidth(contentView.bounds) - UIEdgeInsetsGetHorizontalValue(contentViewPadding); - CGSize labelSize = [label sizeThatFits:CGSizeMake(contentLimitWidth, CGFLOAT_MAX)]; - label.frame = CGRectMake(contentViewPadding.left, contentViewPadding.top, contentLimitWidth, labelSize.height); + [self.modalViewControllerForAddSubview showInView:self.view animated:NO completion:nil]; } @end @@ -372,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]; @@ -384,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]; @@ -392,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"]) { @@ -416,17 +408,14 @@ - (void)viewDidLayoutSubviews { [self.imageView qmui_sizeToFitKeepingImageAspectRatioInSize:CGSizeMake(imageViewLimitWidth, CGFLOAT_MAX)]; CGFloat textLabelMarginLeft = 20; - CGFloat textLabelLimitWidth = contentSize.width - CGRectGetWidth(self.imageView.frame) - textLabelMarginLeft; - CGSize textLabelSize = [self.textLabel sizeThatFits:CGSizeMake(textLabelLimitWidth, CGFLOAT_MAX)]; - self.textLabel.frame = CGRectMake(CGRectGetMaxX(self.imageView.frame) + textLabelMarginLeft, padding.top - 6, textLabelLimitWidth, textLabelSize.height); + self.textLabel.frame = CGRectMake(CGRectGetMaxX(self.imageView.frame) + textLabelMarginLeft, padding.top - 6, contentSize.width - CGRectGetWidth(self.imageView.frame) - textLabelMarginLeft, QMUIViewSelfSizingHeight); } else { // 竖屏下图文垂直布局 CGFloat imageViewLimitHeight = 120; self.imageView.frame = CGRectMake(padding.left, padding.top, contentSize.width, imageViewLimitHeight); CGFloat textLabelMarginTop = 20; - CGSize textLabelSize = [self.textLabel sizeThatFits:CGSizeMake(contentSize.width, CGFLOAT_MAX)]; - self.textLabel.frame = CGRectMake(padding.left, CGRectGetMaxY(self.imageView.frame) + textLabelMarginTop, contentSize.width, textLabelSize.height); + self.textLabel.frame = CGRectMake(padding.left, CGRectGetMaxY(self.imageView.frame) + textLabelMarginTop, contentSize.width, QMUIViewSelfSizingHeight); } self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetMaxY(self.textLabel.frame) + padding.bottom); @@ -434,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.contentViewMargins), CGFLOAT_MAX); + 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/QDMoreOperationViewController.h b/qmuidemo/Modules/Demos/Components/QDMoreOperationViewController.h index 4122f75f..af243266 100644 --- a/qmuidemo/Modules/Demos/Components/QDMoreOperationViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDMoreOperationViewController.h @@ -2,13 +2,13 @@ // QDMoreOperationViewController.h // qmuidemo // -// Created by Kayo Lee on 15/5/18. +// Created by QMUI Team on 15/5/18. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import "QDCommonListViewController.h" -typedef enum { +typedef NS_ENUM(NSUInteger, MoreOperationTag) { MoreOperationTagShareWechat, MoreOperationTagShareMoment, MoreOperationTagShareQzone, @@ -17,7 +17,7 @@ typedef enum { MoreOperationTagBookMark, MoreOperationTagSafari, MoreOperationTagReport -} MoreOperationTag; +}; @interface QDMoreOperationViewController : QDCommonListViewController diff --git a/qmuidemo/Modules/Demos/Components/QDMoreOperationViewController.m b/qmuidemo/Modules/Demos/Components/QDMoreOperationViewController.m index 4bcc7989..fb2379b0 100644 --- a/qmuidemo/Modules/Demos/Components/QDMoreOperationViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDMoreOperationViewController.m @@ -2,150 +2,133 @@ // QDMoreOperationViewController.m // qmuidemo // -// Created by Kayo Lee on 15/5/18. +// Created by QMUI Team on 15/5/18. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import "QDMoreOperationViewController.h" -@interface QDMoreOperationViewController () +@interface QDMoreOperationViewController () @end -@implementation QDMoreOperationViewController { - QMUIMoreOperationController *_moreOperationController; - NSInteger _index; - BOOL _isSelected; -} +@implementation QDMoreOperationViewController - (void)initDataSource { [super initDataSource]; self.dataSourceWithDetailText = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: - @"普通样式", @"点击“收藏”来修改此item的选中态", - @"支持在某个位置插入一个item", @"在第一行的第一个位置插入“邮件”", - @"根据不同的状态显示或隐藏item", @"把第二行的“举报”隐藏掉", - @"修改控件主题色(夜间模式)", @"通过appearance在app启动时设置全局样式", + @"支持 item 多行显示", @"每个 item 可通过 selected 来切换不同状态", + @"支持动态修改 item 位置", @"点击第二行 item 来修改第一行 item", + @"支持修改皮肤样式(例如夜间模式)", @"通过 appearance 设置全局样式", nil]; } -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; -} - - (void)didSelectCellWithTitle:(NSString *)title { [self.tableView qmui_clearsSelection]; - if ([title isEqualToString:@"普通样式"]) { - _index = 0; - [self showMoreOperationController]; - } else if ([title isEqualToString:@"支持在某个位置插入一个item"]) { - _index = 1; - [self showMoreOperationController]; - } else if ([title isEqualToString:@"根据不同的状态显示或隐藏item"]) { - _index = 2; - [self showMoreOperationController]; - } else if ([title isEqualToString:@"修改控件主题色(夜间模式)"]) { - _index = 3; - [self showMoreOperationController]; - } -} - -- (void)showMoreOperationController { - - // 为了不用写那么多样式的reset,直接每次都重新init一个新的controller - - _moreOperationController = [[QMUIMoreOperationController alloc] init]; - _moreOperationController.delegate = self; - - [_moreOperationController addItemWithTitle:@"微信好友" image:UIImageMake(@"icon_moreOperation_shareFriend") type:QMUIMoreOperationItemTypeImportant tag:MoreOperationTagShareWechat]; - - [_moreOperationController addItemWithTitle:@"朋友圈" image:UIImageMake(@"icon_moreOperation_shareMoment") type:QMUIMoreOperationItemTypeImportant tag:MoreOperationTagShareMoment]; - - [_moreOperationController addItemWithTitle:@"新浪微博" image:UIImageMake(@"icon_moreOperation_shareWeibo") type:QMUIMoreOperationItemTypeImportant tag:MoreOperationTagShareWeibo]; - - [_moreOperationController addItemWithTitle:@"QQ空间" image:UIImageMake(@"icon_moreOperation_shareQzone") type:QMUIMoreOperationItemTypeImportant tag:MoreOperationTagShareQzone]; - - [_moreOperationController addItemWithTitle:@"浏览器打开" image:UIImageMake(@"icon_moreOperation_openInSafari") type:QMUIMoreOperationItemTypeNormal tag:MoreOperationTagSafari]; - - [_moreOperationController addItemWithTitle:@"举报" image:UIImageMake(@"icon_moreOperation_report") type:QMUIMoreOperationItemTypeNormal tag:MoreOperationTagReport]; - - [_moreOperationController addItemWithTitle:@"收藏" selectedTitle:@"取消收藏" image:UIImageMake(@"icon_moreOperation_collect") selectedImage:UIImageMake(@"icon_moreOperation_notCollect") type:QMUIMoreOperationItemTypeNormal tag:MoreOperationTagBookMark]; - QMUIMoreOperationItemView *collectItem = [_moreOperationController itemAtTag:MoreOperationTagBookMark]; - collectItem.selected = _isSelected; - - ////////////// - - if (_index % 4 == 0) { - - } else if (_index % 4 == 1) { + if ([title isEqualToString:@"支持 item 多行显示"]) { - QMUIMoreOperationItemView *mailItem = [_moreOperationController itemAtTag:MoreOperationTagShareMail]; - if (!mailItem) { - mailItem = [_moreOperationController createItemWithTitle:@"邮件" selectedTitle:nil image:UIImageMake(@"icon_moreOperation_shareChat") selectedImage:nil type:QMUIMoreOperationItemTypeImportant tag:MoreOperationTagShareMail]; - [_moreOperationController insertItem:mailItem toIndex:0]; - } + QMUIMoreOperationController *moreOperationController = [[QMUIMoreOperationController alloc] init]; + // 如果你的 item 是确定的,则可以直接通过 items 属性来显示,如果 item 需要经过一些判断才能确定下来,请看第二个示例 + moreOperationController.items = @[ + // 第一行 + @[ + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareFriend") title:@"分享给微信好友" handler:^(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView) { + [moreOperationController hideToBottom];// 如果嫌每次都在 handler 里写 hideToBottom 烦,也可以直接把这句写到 moreOperationController:didSelectItemView: 里,它可与 handler 共存 + }], + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareMoment") title:@"分享到朋友圈" handler:^(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView) { + [moreOperationController hideToBottom]; + }], + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareWeibo") title:@"分享到微博" handler:^(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView) { + [moreOperationController hideToBottom]; + }], + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareQzone") title:@"分享到QQ空间" handler:^(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView) { + [moreOperationController hideToBottom]; + }], + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareChat") title:@"分享到私信" handler:^(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView) { + [moreOperationController hideToBottom]; + }] + ], + + // 第二行 + @[ + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_collect") selectedImage:UIImageMake(@"icon_moreOperation_notCollect") title:@"收藏" selectedTitle:@"取消收藏" handler:^(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView) { + itemView.selected = !itemView.selected;// 通过 selected 切换 itemView 的状态 + }], + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_report") title:@"反馈" handler:^(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView) { + [moreOperationController hideToBottom]; + }], + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_openInSafari") title:@"在Safari中打开" handler:^(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView) { + [moreOperationController hideToBottom]; + }] + ], + ]; + [moreOperationController showFromBottom]; - _moreOperationController.cancelButtonMarginTop = [QMUIMoreOperationController appearance].contentEdgeMargin; + } else if ([title isEqualToString:@"支持动态修改 item 位置"]) { - } else if (_index % 4 == 2) { + QMUIMoreOperationController *moreOperationController = [[QMUIMoreOperationController alloc] init]; + moreOperationController.items = @[ + // 第一行 + @[ + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareFriend") title:@"分享给微信好友" handler:NULL], + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareMoment") title:@"分享到朋友圈" handler:NULL], + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareWeibo") title:@"分享到微博" handler:NULL] + ] + ]; + // 动态给第二行插入一个 item + [moreOperationController addItemView:[QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_add") title:@"添加" handler:^(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView) { + + // 动态添加 item + NSInteger sectionToAdd = 0; + if (itemView.indexPath.section == 0) { + sectionToAdd = 1; + } + [moreOperationController addItemView:[QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareQzone") title:@"分享到QQ空间" handler:NULL] inSection:sectionToAdd]; + + }] inSection:1]; - [_moreOperationController setItemHidden:YES tag:MoreOperationTagReport]; - - _moreOperationController.contentEdgeMargin = 0; - _moreOperationController.contentMaximumWidth = CGRectGetWidth(self.view.bounds); - _moreOperationController.contentCornerRadius = 0; - _moreOperationController.contentBackgroundColor = UIColorMake(246, 246, 246); - _moreOperationController.cancelButtonHeight = 46; - _moreOperationController.cancelButtonTitleColor = UIColorMake(34, 34, 34); - _moreOperationController.cancelButtonFont = UIFontMake(16); - _moreOperationController.cancelButtonSeparatorColor = UIColorClear; - - } else if (_index % 4 == 3) { - - _moreOperationController.contentBackgroundColor = UIColorMake(34, 34, 34); - _moreOperationController.cancelButtonSeparatorColor = UIColorMake(51, 51, 51); - _moreOperationController.cancelButtonBackgroundColor = UIColorMake(34, 34, 34); - _moreOperationController.cancelButtonTitleColor = UIColorMake(102, 102, 102); - _moreOperationController.itemTitleColor = UIColorMake(102, 102, 102); - - for (QMUIMoreOperationItemView *item in _moreOperationController.items) { - item.alpha = 0.4; - } + // 再给第二行插入一个 item + [moreOperationController addItemView:[QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_remove") title:@"删除" handler:^(QMUIMoreOperationController *moreOperationController, QMUIMoreOperationItemView *itemView) { + + // 动态减少 item + NSInteger sectionToRemove = 0; + if (itemView.indexPath.section == 0) { + sectionToRemove = 1; + } + if (moreOperationController.items.count > 1) { + [moreOperationController removeItemViewAtIndexPath:[NSIndexPath indexPathForItem:moreOperationController.items[sectionToRemove].count - 1 inSection:sectionToRemove]]; + } + + }] inSection:1]; + moreOperationController.cancelButton.hidden = YES;// 通过控制 cancelButton.hidden 的值来控制取消按钮的显示、隐藏 + [moreOperationController showFromBottom]; + } else if ([title isEqualToString:@"支持修改皮肤样式(例如夜间模式)"]) { + QMUIMoreOperationController *moreOperationController = [[QMUIMoreOperationController alloc] init]; + moreOperationController.delegate = self; + moreOperationController.contentBackgroundColor = UIColorMake(34, 34, 34); + moreOperationController.cancelButtonSeparatorColor = UIColorMake(51, 51, 51); + moreOperationController.cancelButtonBackgroundColor = UIColorMake(34, 34, 34); + moreOperationController.cancelButtonTitleColor = UIColorMake(102, 102, 102); + moreOperationController.itemTitleColor = UIColorMake(102, 102, 102); + moreOperationController.items = @[ + @[ + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareFriend") title:@"分享给微信好友" handler:NULL], + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareMoment") title:@"分享到朋友圈" handler:NULL], + [QMUIMoreOperationItemView itemViewWithImage:UIImageMake(@"icon_moreOperation_shareWeibo") title:@"分享到微博" handler:NULL] + ] + ]; + [moreOperationController.items qmui_enumerateNestedArrayWithBlock:^(QMUIMoreOperationItemView *itemView, BOOL *stop) { + [itemView setImage:[[itemView imageForState:UIControlStateNormal] qmui_imageWithAlpha:.4] forState:UIControlStateNormal]; + }]; + [moreOperationController showFromBottom]; } - - // 显示更多操作面板 - [_moreOperationController showFromBottom]; } -#pragma mark - +#pragma mark - -- (void)moreOperationController:(QMUIMoreOperationController *)moreOperationController didSelectItemAtTag:(NSInteger)tag { - QMUIMoreOperationItemView *itemView = [moreOperationController itemAtTag:tag]; - NSString *tipString = itemView.titleLabel.text; - switch (tag) { - case MoreOperationTagShareWechat: - break; - case MoreOperationTagShareMoment: - break; - case MoreOperationTagShareWeibo: - break; - case MoreOperationTagShareQzone: - break; - case MoreOperationTagShareMail: - break; - case MoreOperationTagSafari: - break; - case MoreOperationTagReport: - break; - case MoreOperationTagBookMark: - tipString = [itemView titleForState:_isSelected ? UIControlStateSelected : UIControlStateNormal]; - _isSelected = !_isSelected; - break; - default: - break; - } - [QMUITips showWithText:tipString inView:self.view hideAfterDelay:0.5]; +- (void)moreOperationController:(QMUIMoreOperationController *)moreOperationController didSelectItemView:(QMUIMoreOperationItemView *)itemView { [moreOperationController hideToBottom]; } diff --git a/qmuidemo/Modules/Demos/Components/QDMultipleDelegatesViewController.h b/qmuidemo/Modules/Demos/Components/QDMultipleDelegatesViewController.h new file mode 100644 index 00000000..235dd2e0 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDMultipleDelegatesViewController.h @@ -0,0 +1,13 @@ +// +// QDMultipleDelegatesViewController.h +// qmuidemo +// +// Created by QMUI Team on 2018/3/28. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +@interface QDMultipleDelegatesViewController : QDCommonViewController + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDMultipleDelegatesViewController.m b/qmuidemo/Modules/Demos/Components/QDMultipleDelegatesViewController.m new file mode 100644 index 00000000..eaba9882 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDMultipleDelegatesViewController.m @@ -0,0 +1,133 @@ +// +// QDMultipleDelegatesViewController.m +// qmuidemo +// +// Created by QMUI Team on 2018/3/28. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import "QDMultipleDelegatesViewController.h" + +@interface QDTextFieldDelegator : NSObject + +@property(nonatomic, weak) UIView *containerView; +@property(nonatomic, strong) UILabel *titleLabel; +@property(nonatomic, strong) UILabel *printLabel; +@end + +@interface QDMultipleDelegatesViewController () + +@property(nonatomic, strong) UILabel *descriptionLabel; +@property(nonatomic, strong) UITextField *textField; +@property(nonatomic, strong) QDTextFieldDelegator *delegator1; +@property(nonatomic, strong) QDTextFieldDelegator *delegator2; +@end + +@implementation QDMultipleDelegatesViewController + +- (void)initSubviews { + [super initSubviews]; + self.delegator1 = [[QDTextFieldDelegator alloc] init]; + self.delegator1.titleLabel.text = @"delegate1"; + self.delegator1.containerView = self.view; + + self.delegator2 = [[QDTextFieldDelegator alloc] init]; + self.delegator2.titleLabel.text = @"delegate2"; + self.delegator2.containerView = self.view; + + self.textField = [[UITextField alloc] init]; + + // 开启对多 delegate 的支持 + self.textField.qmui_multipleDelegatesEnabled = YES; + + // 然后像平常一样给 delegate 赋值即可,只不过多次赋值不会互相覆盖 + self.textField.delegate = self.delegator1; + self.textField.delegate = self.delegator2; + + // 示例:如何清除某些指定的 delegate +// [self.textField qmui_removeDelegate:self.delegator1]; + + // 示例:如何清除所有的 delegate +// self.textField.delegate = nil; + + // 后面的代码不用看了 + self.textField.leftViewMode = UITextFieldViewModeAlways; + self.textField.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 8, 12)]; + self.textField.layer.borderWidth = PixelOne; + self.textField.layer.borderColor = UIColorSeparator.CGColor; + self.textField.layer.cornerRadius = 6; + self.textField.placeholder = @"一个输入框,多个 delegate 响应"; + [self.view addSubview:self.textField]; + + 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: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16]}];; + [self.view addSubview:self.descriptionLabel]; +} + +- (BOOL)shouldHideKeyboardWhenTouchInView:(UIView *)view { + return YES; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + 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); + + self.textField.frame = CGRectMake(paddings.left, CGRectGetMaxY(self.descriptionLabel.frame) + 12, CGRectGetWidth(self.descriptionLabel.frame), 44); + + CGFloat delegatorMarginTop = 24; + CGFloat delegatorHorizontalSpacing = 24; + [self.delegator1.titleLabel sizeToFit]; + self.delegator1.titleLabel.frame = CGRectSetXY(self.delegator1.titleLabel.frame, CGRectGetMinX(self.textField.frame), CGRectGetMaxY(self.textField.frame) + delegatorMarginTop); + self.delegator1.printLabel.frame = CGRectMake(CGRectGetMinX(self.delegator1.titleLabel.frame), CGRectGetMaxY(self.delegator1.titleLabel.frame) + 12, (CGRectGetWidth(self.textField.frame) - delegatorHorizontalSpacing) / 2, QMUIViewSelfSizingHeight); + + self.delegator2.printLabel.frame = CGRectSetX(self.delegator1.printLabel.frame, CGRectGetMaxX(self.delegator1.printLabel.frame) + delegatorHorizontalSpacing); + CGFloat printLabelHeight = [self.delegator2.printLabel sizeThatFits:CGSizeMake(CGRectGetWidth(self.delegator2.printLabel.frame), CGFLOAT_MAX)].height; + self.delegator2.printLabel.frame = CGRectSetHeight(self.delegator2.printLabel.frame, printLabelHeight); + + [self.delegator2.titleLabel sizeToFit]; + self.delegator2.titleLabel.frame = CGRectSetXY(self.delegator2.titleLabel.frame, CGRectGetMinX(self.delegator2.printLabel.frame), CGRectGetMinY(self.delegator1.titleLabel.frame)); +} + +@end + +@implementation QDTextFieldDelegator + +- (instancetype)init { + if (self = [super init]) { + self.titleLabel = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColorBlack]; + + self.printLabel = [[UILabel alloc] init]; + self.printLabel.qmui_textAttributes = @{NSFontAttributeName: UIFontMake(12), NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}; + self.printLabel.numberOfLines = 0; + } + return self; +} + +- (void)setContainerView:(UIView *)containerView { + _containerView = containerView; + if (containerView) { + [containerView addSubview:self.titleLabel]; + [containerView addSubview:self.printLabel]; + } else { + [self.titleLabel removeFromSuperview]; + [self.printLabel removeFromSuperview]; + } + [containerView setNeedsLayout]; +} + +#pragma mark - + +- (void)textFieldDidBeginEditing:(UITextField *)textField { + self.printLabel.text = [NSString stringWithFormat:@"%@\ndelegator is %@", NSStringFromSelector(_cmd), self]; + [self.containerView setNeedsLayout]; +} + +- (void)textFieldDidEndEditing:(UITextField *)textField { + self.printLabel.text = [NSString stringWithFormat:@"%@\ndelegator is %@", NSStringFromSelector(_cmd), self]; + [self.containerView setNeedsLayout]; +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.h b/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.h index dba6b804..6ef0232f 100644 --- a/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.h @@ -2,7 +2,7 @@ // QDMultipleImagePickerPreviewViewController.h // qmuidemo // -// Created by Kayo Lee on 15/5/16. +// Created by QMUI Team on 15/5/16. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.m b/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.m index 24e7ff49..2a776e58 100644 --- a/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDMultipleImagePickerPreviewViewController.m @@ -2,100 +2,107 @@ // QDMultipleImagePickerPreviewViewController.m // qmuidemo // -// Created by Kayo Lee on 15/5/16. +// Created by QMUI Team on 15/5/16. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import "QDMultipleImagePickerPreviewViewController.h" -#define BottomToolBarViewHeight 45 #define ImageCountLabelSize CGSizeMake(18, 18) -@implementation QDMultipleImagePickerPreviewViewController { - QMUIButton *_sendButton; - QMUIButton *_originImageCheckboxButton; - UIView *_bottomToolBarView; -} +@interface QDMultipleImagePickerPreviewViewController () + +@property(nonatomic, strong) QMUIButton *sendButton; +@property(nonatomic, strong) QMUIButton *originImageCheckboxButton; +@property(nonatomic, strong) UIView *bottomToolBarView; +@end + +@implementation QDMultipleImagePickerPreviewViewController @dynamic delegate; - (void)initSubviews { [super initSubviews]; - _bottomToolBarView = [[UIView alloc] init]; - _bottomToolBarView.backgroundColor = self.toolBarBackgroundColor; - [self.view addSubview:_bottomToolBarView]; + self.bottomToolBarView = [[UIView alloc] init]; + self.bottomToolBarView.backgroundColor = self.toolBarBackgroundColor; + [self.view addSubview:self.bottomToolBarView]; - _sendButton = [[QMUIButton alloc] init]; - _sendButton.qmui_outsideEdge = UIEdgeInsetsMake(-6, -6, -6, -6); - [_sendButton setTitleColor:self.toolBarTintColor forState:UIControlStateNormal]; - [_sendButton setTitle:@"发送" forState:UIControlStateNormal]; - _sendButton.titleLabel.font = UIFontMake(16); - [_sendButton sizeToFit]; - [_sendButton addTarget:self action:@selector(handleSendButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - [_bottomToolBarView addSubview:_sendButton]; + self.sendButton = [[QMUIButton alloc] init]; + self.sendButton.adjustsTitleTintColorAutomatically = YES; + self.sendButton.adjustsImageTintColorAutomatically = YES; + self.sendButton.qmui_outsideEdge = UIEdgeInsetsMake(-6, -6, -6, -6); + [self.sendButton setTitle:@"发送" forState:UIControlStateNormal]; + self.sendButton.titleLabel.font = UIFontMake(16); + [self.sendButton sizeToFit]; + [self.sendButton addTarget:self action:@selector(handleSendButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + [self.bottomToolBarView addSubview:self.sendButton]; _imageCountLabel = [[QMUILabel alloc] init]; - _imageCountLabel.backgroundColor = ButtonTintColor; - _imageCountLabel.textColor = UIColorWhite; + _imageCountLabel.backgroundColor = self.toolBarTintColor; + _imageCountLabel.textColor = [self.toolBarTintColor qmui_colorIsDark] ? UIColorWhite : UIColorBlack; _imageCountLabel.font = UIFontMake(12); _imageCountLabel.textAlignment = NSTextAlignmentCenter; _imageCountLabel.lineBreakMode = NSLineBreakByCharWrapping; _imageCountLabel.layer.masksToBounds = YES; _imageCountLabel.layer.cornerRadius = ImageCountLabelSize.width / 2; _imageCountLabel.hidden = YES; - [_bottomToolBarView addSubview:_imageCountLabel]; + [self.bottomToolBarView addSubview:_imageCountLabel]; - _originImageCheckboxButton = [[QMUIButton alloc] init]; - _originImageCheckboxButton.titleLabel.font = UIFontMake(14); - [_originImageCheckboxButton setTitleColor:self.toolBarTintColor forState:UIControlStateNormal]; - [_originImageCheckboxButton setImage:UIImageMake(@"origin_image_checkbox") forState:UIControlStateNormal]; - [_originImageCheckboxButton setImage:UIImageMake(@"origin_image_checkbox_checked") forState:UIControlStateSelected]; - [_originImageCheckboxButton setImage:UIImageMake(@"origin_image_checkbox_checked") forState:UIControlStateSelected|UIControlStateHighlighted]; - [_originImageCheckboxButton setTitle:@"原图" forState:UIControlStateNormal]; - [_originImageCheckboxButton setImageEdgeInsets:UIEdgeInsetsMake(0, -5.0f, 0, 5.0f)]; - [_originImageCheckboxButton setContentEdgeInsets:UIEdgeInsetsMake(0, 5.0f, 0, 0)]; - [_originImageCheckboxButton sizeToFit]; - _originImageCheckboxButton.qmui_outsideEdge = UIEdgeInsetsMake(-6.0f, -6.0f, -6.0f, -6.0f); - [_originImageCheckboxButton addTarget:self action:@selector(handleOriginImageCheckboxButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - [_bottomToolBarView addSubview:_originImageCheckboxButton]; -} - -- (void)viewDidLoad { - [super viewDidLoad]; + self.originImageCheckboxButton = [[QMUIButton alloc] init]; + self.originImageCheckboxButton.adjustsTitleTintColorAutomatically = YES; + self.originImageCheckboxButton.adjustsImageTintColorAutomatically = YES; + self.originImageCheckboxButton.titleLabel.font = UIFontMake(14); + [self.originImageCheckboxButton setImage:UIImageMake(@"origin_image_checkbox") forState:UIControlStateNormal]; + [self.originImageCheckboxButton setImage:UIImageMake(@"origin_image_checkbox_checked") forState:UIControlStateSelected]; + [self.originImageCheckboxButton setImage:UIImageMake(@"origin_image_checkbox_checked") forState:UIControlStateSelected|UIControlStateHighlighted]; + [self.originImageCheckboxButton setTitle:@"原图" forState:UIControlStateNormal]; + [self.originImageCheckboxButton setImageEdgeInsets:UIEdgeInsetsMake(0, -5.0f, 0, 5.0f)]; + [self.originImageCheckboxButton setContentEdgeInsets:UIEdgeInsetsMake(0, 5.0f, 0, 0)]; + [self.originImageCheckboxButton sizeToFit]; + self.originImageCheckboxButton.qmui_outsideEdge = UIEdgeInsetsMake(-6.0f, -6.0f, -6.0f, -6.0f); + [self.originImageCheckboxButton addTarget:self action:@selector(handleOriginImageCheckboxButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + [self.bottomToolBarView addSubview:self.originImageCheckboxButton]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - [self updateOriginImageCheckboxButtonIfNeed]; + [self updateOriginImageCheckboxButtonWithIndex:self.imagePreviewView.currentImageIndex]; + if ([self.selectedImageAssetArray count] > 0) { + NSUInteger selectedCount = [self.selectedImageAssetArray count]; + _imageCountLabel.text = [[NSString alloc] initWithFormat:@"%@", @(selectedCount)]; + _imageCountLabel.hidden = NO; + } else { + _imageCountLabel.hidden = YES; + } } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; CGFloat bottomToolBarPaddingHorizontal = 12.0f; - _bottomToolBarView.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - BottomToolBarViewHeight, CGRectGetWidth(self.view.bounds), BottomToolBarViewHeight); - _sendButton.frame = CGRectSetXY(_sendButton.frame, CGRectGetWidth(_bottomToolBarView.frame) - bottomToolBarPaddingHorizontal - CGRectGetWidth(_sendButton.frame), CGFloatGetCenter(CGRectGetHeight(_bottomToolBarView.frame), CGRectGetHeight(_sendButton.frame))); - _imageCountLabel.frame = CGRectMake(CGRectGetMinX(_sendButton.frame) - 5 - ImageCountLabelSize.width, CGRectGetMinY(_sendButton.frame) + CGFloatGetCenter(CGRectGetHeight(_sendButton.frame), CGRectGetHeight(_imageCountLabel.frame)), ImageCountLabelSize.width, ImageCountLabelSize.height); - _originImageCheckboxButton.frame = CGRectSetXY(_originImageCheckboxButton.frame, bottomToolBarPaddingHorizontal, CGFloatGetCenter(CGRectGetHeight(_bottomToolBarView.frame), CGRectGetHeight(_originImageCheckboxButton.frame))); + CGFloat bottomToolBarContentHeight = 44; + 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); + self.originImageCheckboxButton.frame = CGRectSetXY(self.originImageCheckboxButton.frame, bottomToolBarPaddingHorizontal, CGFloatGetCenter(bottomToolBarContentHeight, CGRectGetHeight(self.originImageCheckboxButton.frame))); +} + +- (void)setToolBarTintColor:(UIColor *)toolBarTintColor { + [super setToolBarTintColor:toolBarTintColor]; + self.bottomToolBarView.tintColor = toolBarTintColor; + _imageCountLabel.backgroundColor = toolBarTintColor; + _imageCountLabel.textColor = [toolBarTintColor qmui_colorIsDark] ? UIColorWhite : UIColorBlack; } - (void)singleTouchInZoomingImageView:(QMUIZoomImageView *)zoomImageView location:(CGPoint)location { [super singleTouchInZoomingImageView:zoomImageView location:location]; - _bottomToolBarView.hidden = !_bottomToolBarView.hidden; + self.bottomToolBarView.hidden = !self.bottomToolBarView.hidden; } - (void)zoomImageView:(QMUIZoomImageView *)imageView didHideVideoToolbar:(BOOL)didHide { [super zoomImageView:imageView didHideVideoToolbar:didHide]; - _bottomToolBarView.hidden = didHide; -} - -- (void)setDownloadStatus:(QMUIAssetDownloadStatus)downloadStatus { - [super setDownloadStatus:downloadStatus]; - if (downloadStatus == QMUIAssetDownloadStatusSucceed) { - _originImageCheckboxButton.enabled = YES; - } else { - _originImageCheckboxButton.enabled = NO; - } + self.bottomToolBarView.hidden = didHide; } - (void)handleSendButtonClick:(id)sender { @@ -111,44 +118,49 @@ - (void)handleSendButtonClick:(id)sender { }]; } -- (void)handleOriginImageCheckboxButtonClick:(id)sender { - QMUINavigationButton *button = sender; +- (void)handleOriginImageCheckboxButtonClick:(UIButton *)button { if (button.selected) { button.selected = NO; [button setTitle:@"原图" forState:UIControlStateNormal]; [button sizeToFit]; - [_bottomToolBarView setNeedsLayout]; + [self.bottomToolBarView setNeedsLayout]; } else { button.selected = YES; - [self updateOriginImageCheckboxButtonIfNeed]; + [self updateOriginImageCheckboxButtonWithIndex:self.imagePreviewView.currentImageIndex]; + if (!self.checkboxButton.selected) { + [self.checkboxButton sendActionsForControlEvents:UIControlEventTouchUpInside]; + } } self.shouldUseOriginImage = button.selected; } -- (void)updateOriginImageCheckboxButtonIfNeed { - if (_originImageCheckboxButton.selected) { - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:self.imagePreviewView.currentImageIndex]; - [imageAsset assetSize:^(long long size) { - [_originImageCheckboxButton setTitle:[NSString stringWithFormat:@"原图(%@)", [QDUIHelper humanReadableFileSize:size]] forState:UIControlStateNormal]; - [_originImageCheckboxButton sizeToFit]; - [_bottomToolBarView setNeedsLayout]; - }]; +- (void)updateOriginImageCheckboxButtonWithIndex:(NSInteger)index { + QMUIAsset *asset = self.imagesAssetArray[index]; + if (asset.assetType == QMUIAssetTypeAudio || asset.assetType == QMUIAssetTypeVideo) { + self.originImageCheckboxButton.hidden = YES; + } else { + self.originImageCheckboxButton.hidden = NO; + if (self.originImageCheckboxButton.selected) { + [asset assetSize:^(long long size) { + [self.originImageCheckboxButton setTitle:[NSString stringWithFormat:@"原图(%@)", [QDUIHelper humanReadableFileSize:size]] forState:UIControlStateNormal]; + [self.originImageCheckboxButton sizeToFit]; + [self.bottomToolBarView setNeedsLayout]; + }]; + } } } #pragma mark - +- (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.safeAreaInsets.bottom);// videToolbarMargins 是利用 UIAppearance 赋值的,也即意味着要在 addSubview 之后才会被赋值,而在 renderZoomImageView 里,zoomImageView 可能尚未被添加到 view 层级里,所以无法通过 zoomImageView.videoToolbarMargins 获取到原来的值,因此只能通过 [QMUIZoomImageView appearance] 的方式获取 +} + - (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView willScrollHalfToIndex:(NSUInteger)index { [super imagePreviewView:imagePreviewView willScrollHalfToIndex:index]; - if (_originImageCheckboxButton.selected) { - QMUIAsset *imageAsset = [self.imagesAssetArray objectAtIndex:index]; - [imageAsset assetSize:^(long long size) { - [_originImageCheckboxButton setTitle:[NSString stringWithFormat:@"原图(%@)", [QDUIHelper humanReadableFileSize:size]] forState:UIControlStateNormal]; - [_originImageCheckboxButton sizeToFit]; - [_bottomToolBarView setNeedsLayout]; - }]; - } + [self updateOriginImageCheckboxButtonWithIndex:index]; } @end diff --git a/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingAnimatorViewController.h b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingAnimatorViewController.h new file mode 100644 index 00000000..13d7ad59 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingAnimatorViewController.h @@ -0,0 +1,17 @@ +// +// QDNavigationBarScrollingAnimatorViewController.h +// qmuidemo +// +// Created by QMUI Team on 2018/O/29. +// Copyright © 2018 QMUI Team. All rights reserved. +// + +#import "QDCommonTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDNavigationBarScrollingAnimatorViewController : QDCommonTableViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingAnimatorViewController.m b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingAnimatorViewController.m new file mode 100644 index 00000000..771ab26e --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingAnimatorViewController.m @@ -0,0 +1,99 @@ +// +// QDNavigationBarScrollingAnimatorViewController.m +// qmuidemo +// +// Created by QMUI Team on 2018/O/29. +// Copyright © 2018 QMUI Team. All rights reserved. +// + +#import "QDNavigationBarScrollingAnimatorViewController.h" + +@interface QDNavigationBarScrollingAnimatorViewController () + +@property(nonatomic, strong) QMUINavigationBarScrollingAnimator *navigationAnimator; +@end + +@implementation QDNavigationBarScrollingAnimatorViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.navigationAnimator = [[QMUINavigationBarScrollingAnimator alloc] init]; + self.navigationAnimator.scrollView = self.tableView;// 指定要关联的 scrollView + self.navigationAnimator.offsetYToStartAnimation = 30;// 设置滚动的起点,值即表示在默认停靠的位置往下滚动多少距离后即触发动画,默认是 0 + self.navigationAnimator.distanceToStopAnimation = 64;// 设置从起点开始滚动多长的距离达到终点 + + // 有两种方式更改 navigationBar 的样式,一种是利用 animator 为每个属性提供的单独 block,直接返回这个属性在特定 progress 下的样式即可,另一种是直接用 animationBlock,Demo 这里使用第一种。 + // 若使用第二种,则第一种会失效。 + // 若希望同时使用两种,则请在 animationBlock 里手动获取各个属性对应的 block 的返回值并设置到 navigationBar 上。 + self.navigationAnimator.backgroundImageBlock = ^UIImage * _Nonnull(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress) { + return [NavBarBackgroundImage qmui_imageWithAlpha:progress]; + }; + self.navigationAnimator.shadowImageBlock = ^UIImage * _Nonnull(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress) { + return [NavBarShadowImage qmui_imageWithAlpha:progress]; + }; + self.navigationAnimator.tintColorBlock = ^UIColor * _Nonnull(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress) { + return [UIColor qmui_colorFromColor:UIColorBlack toColor:NavBarTintColor progress:progress]; + }; + self.navigationAnimator.titleViewTintColorBlock = self.navigationAnimator.tintColorBlock; + self.navigationAnimator.statusbarStyleBlock = ^UIStatusBarStyle(QMUINavigationBarScrollingAnimator * _Nonnull animator, float progress) { + return progress < .25 ? UIStatusBarStyleDefault : UIStatusBarStyleLightContent; + }; +} + +- (UIStatusBarStyle)preferredStatusBarStyle { + // 需要手动调用 navigationAnimator.statusbarStyleBlock 来告诉系统状态栏的变化 + if (self.navigationAnimator) { + return self.navigationAnimator.statusbarStyleBlock(self.navigationAnimator, self.navigationAnimator.progress); + } + return [super preferredStatusBarStyle]; +} + +// 建议配合 QMUINavigationControllerAppearanceDelegate 控制不同界面切换时的 navigationBar 样式,否则需自己在 viewWillAppear:、viewWillDisappear: 里控制 + +#pragma mark - + +- (UIImage *)qmui_navigationBarBackgroundImage { + return self.navigationAnimator.backgroundImageBlock(self.navigationAnimator, self.navigationAnimator.progress); +} + +- (UIImage *)qmui_navigationBarShadowImage { + return self.navigationAnimator.shadowImageBlock(self.navigationAnimator, self.navigationAnimator.progress); +} + +- (UIColor *)qmui_navigationBarTintColor { + return self.navigationAnimator.tintColorBlock(self.navigationAnimator, self.navigationAnimator.progress); +} + +- (UIColor *)qmui_titleViewTintColor { + return [self qmui_navigationBarTintColor]; +} + +#pragma mark - + +// 为了展示接口的使用,QMUI Demo 没有打开配置表的 AutomaticCustomNavigationBarTransitionStyle,因此当 navigationBar 样式与默认样式不同时,需要手动在 customNavigationBarTransitionKey 里返回一个与其他界面不相同的值,这样才能使用自定义的 navigationBar 转场样式 +- (NSString *)customNavigationBarTransitionKey { + return self.navigationAnimator.progress >= 1 ? nil : @"progress"; +} + +#pragma mark - + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return 50; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *identifier = @"cell"; + QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[QMUITableViewCell alloc] initForTableView:tableView withReuseIdentifier:identifier]; + } + cell.textLabel.text = [NSString qmui_stringWithNSInteger:indexPath.row]; + [cell updateCellAppearanceWithIndexPath:indexPath]; + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:YES]; +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingSnapAnimatorViewController.h b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingSnapAnimatorViewController.h new file mode 100644 index 00000000..2e58aa20 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingSnapAnimatorViewController.h @@ -0,0 +1,17 @@ +// +// QDNavigationBarScrollingSnapAnimatorViewController.h +// qmuidemo +// +// Created by QMUI Team on 2018/O/29. +// Copyright © 2018 QMUI Team. All rights reserved. +// + +#import "QDCommonTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDNavigationBarScrollingSnapAnimatorViewController : QDCommonTableViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingSnapAnimatorViewController.m b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingSnapAnimatorViewController.m new file mode 100644 index 00000000..938a4062 --- /dev/null +++ b/qmuidemo/Modules/Demos/Components/QDNavigationBarScrollingSnapAnimatorViewController.m @@ -0,0 +1,74 @@ +// +// QDNavigationBarScrollingSnapAnimatorViewController.m +// qmuidemo +// +// Created by QMUI Team on 2018/O/29. +// Copyright © 2018 QMUI Team. All rights reserved. +// + +#import "QDNavigationBarScrollingSnapAnimatorViewController.h" + +@interface QDNavigationBarScrollingSnapAnimatorViewController () + +@property(nonatomic, strong) QMUINavigationBarScrollingSnapAnimator *navigationAnimator; +@end + +@implementation QDNavigationBarScrollingSnapAnimatorViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + self.navigationAnimator = [[QMUINavigationBarScrollingSnapAnimator alloc] init]; + self.navigationAnimator.scrollView = self.tableView; + self.navigationAnimator.offsetYToStartAnimation = 44;// 设置滚动的起点,默认是 0,也即 scrollView 在默认位置稍微往下滚则开始做动画,44 则表示在默认位置再往下滚动44之后才触发动画 + __weak __typeof(self)weakSelf = self; + self.navigationAnimator.animationBlock = ^(QMUINavigationBarScrollingSnapAnimator * _Nonnull animator, BOOL offsetYReached) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + NSLog(@"导航栏%@, inset.top = %.2f, offset.y = %.2f", offsetYReached ? @"被隐藏了" : @"显示出来了", strongSelf.tableView.contentInset.top, strongSelf.tableView.contentOffset.y); + [strongSelf.navigationController setNavigationBarHidden:offsetYReached animated:YES]; + }; + + // 为了避免更改 navigationBar 显隐影响 scrollView 的滚动,这里屏蔽掉自动适应 contentInset + self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + self.tableView.contentInset = UIEdgeInsetsMake(self.qmui_navigationBarMaxYInViewCoordinator, 0, self.view.safeAreaInsets.bottom, 0); + self.tableView.scrollIndicatorInsets = self.tableView.contentInset; + [self.tableView qmui_scrollToTopUponContentInsetTopChange]; +} + +// 建议配合 QMUINavigationControllerDelegate 控制不同界面切换时的 navigationBar 样式/显隐,否则需自己在 viewWillAppear:、viewWillDisappear: 里控制 + +#pragma mark - + +- (BOOL)preferredNavigationBarHidden { + return self.navigationAnimator.offsetYReached; +} + +- (BOOL)forceEnableInteractivePopGestureRecognizer { + return self.navigationAnimator.offsetYReached; +} + +#pragma mark - + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return 50; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *identifier = @"cell"; + QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[QMUITableViewCell alloc] initForTableView:tableView withReuseIdentifier:identifier]; + } + cell.textLabel.text = [NSString qmui_stringWithNSInteger:indexPath.row]; + [cell updateCellAppearanceWithIndexPath:indexPath]; + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:YES]; +} + +@end diff --git a/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.h b/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.h index f4b22735..39c9974e 100644 --- a/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.h @@ -2,7 +2,7 @@ // QDNavigationTitleViewController.h // qmui // -// Created by MoLice on 14-7-2. +// Created by QMUI Team on 14-7-2. // Copyright (c) 2014年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.m b/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.m index 71dba126..dda6fb28 100644 --- a/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDNavigationTitleViewController.m @@ -2,7 +2,7 @@ // QDNavigationTitleViewController.m // qmui // -// Created by MoLice on 14-7-2. +// Created by QMUI Team on 14-7-2. // Copyright (c) 2014年 QMUI Team. All rights reserved. // @@ -29,8 +29,8 @@ - (void)dealloc { self.titleView.delegate = nil; } -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; +- (void)setupNavigationItems { + [super setupNavigationItems]; self.title = @"主标题"; } @@ -43,6 +43,7 @@ - (void)initPopupContainerViewIfNeeded { 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) { weakSelf.titleView.active = NO; @@ -50,13 +51,6 @@ - (void)initPopupContainerViewIfNeeded { } } -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - if (self.popupMenuView.isShowing) { - [self.popupMenuView layoutWithTargetView:self.titleView]; - } -} - - (void)initDataSource { self.dataSource = @[@"显示左边的 loading", @"显示右边的 accessoryView", @@ -98,17 +92,17 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath // 水平对齐方式 { QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"水平对齐方式" message:nil preferredStyle:QMUIAlertControllerStyleActionSheet]; - [alertController addAction:[QMUIAlertAction actionWithTitle:@"左对齐" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertAction *action) { + [alertController addAction:[QMUIAlertAction actionWithTitle:@"左对齐" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { self.titleView.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft; self.horizontalAlignment = self.titleView.contentHorizontalAlignment; [self.tableView reloadData]; }]]; - [alertController addAction:[QMUIAlertAction actionWithTitle:@"居中对齐" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertAction *action) { + [alertController addAction:[QMUIAlertAction actionWithTitle:@"居中对齐" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { self.titleView.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter; self.horizontalAlignment = self.titleView.contentHorizontalAlignment; [self.tableView reloadData]; }]]; - [alertController addAction:[QMUIAlertAction actionWithTitle:@"右对齐" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertAction *action) { + [alertController addAction:[QMUIAlertAction actionWithTitle:@"右对齐" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { self.titleView.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight; self.horizontalAlignment = self.titleView.contentHorizontalAlignment; [self.tableView reloadData]; @@ -181,7 +175,6 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N - (void)didChangedActive:(BOOL)active forTitleView:(QMUINavigationTitleView *)titleView { if (active) { - [self.popupMenuView layoutWithTargetView:self.titleView]; [self.popupMenuView showWithAnimated:YES]; } } diff --git a/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.h b/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.h index 72f7c522..c4d7770e 100644 --- a/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.h @@ -2,7 +2,7 @@ // QDPieProgressViewController.h // qmuidemo // -// Created by MoLice on 15/9/8. +// Created by QMUI Team on 15/9/8. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.m b/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.m index 8b28491d..32e3c8fc 100644 --- a/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDPieProgressViewController.m @@ -2,7 +2,7 @@ // QDPieProgressViewController.m // qmuidemo // -// Created by MoLice on 15/9/8. +// Created by QMUI Team on 15/9/8. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -10,83 +10,105 @@ @interface QDPieProgressViewController () +@property(nonatomic, strong) UIScrollView *scrollView; + @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; @property(nonatomic, strong) UILabel *titleLabel2; @property(nonatomic, strong) QMUIPieProgressView *progressView2; -@property(nonatomic, strong) QMUIPieProgressView *progressView3; -@property(nonatomic, strong) QMUIPieProgressView *progressView4; + +@property(nonatomic, strong) UIView *section3; +@property(nonatomic, strong) UILabel *titleLabel3; +@property(nonatomic, strong) QMUIPieProgressView *progressView31; +@property(nonatomic, strong) QMUIPieProgressView *progressView32; +@property(nonatomic, strong) QMUIPieProgressView *progressView33; @end @implementation QDPieProgressViewController -- (void)viewDidLoad { - [super viewDidLoad]; - self.view.backgroundColor = UIColorWhite; -} - - (void)initSubviews { [super initSubviews]; + self.scrollView = [[UIScrollView alloc] init]; + [self.view addSubview:self.scrollView]; + self.section1 = [[UIView alloc] init]; self.section1.qmui_borderColor = UIColorSeparator; - self.section1.qmui_borderPosition = QMUIBorderViewPositionBottom; + self.section1.qmui_borderPosition = QMUIViewBorderPositionBottom; self.section1.qmui_borderWidth = PixelOne; - [self.view addSubview:self.section1]; + [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]; - self.titleLabel1 = [[UILabel alloc] initWithFont:UIFontMake(13) textColor:self.progressView1.tintColor]; + self.titleLabel1 = [[UILabel alloc] qmui_initWithFont:UIFontMake(13) textColor:self.progressView1.tintColor]; [self.titleLabel1 qmui_calculateHeightAfterSetAppearance]; 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:UIControlEventTouchUpInside]; + [self.slider addTarget:self action:@selector(handleSliderTouchUpInside:) forControlEvents:UIControlEventValueChanged]; [self.section1 addSubview:self.slider]; self.section2 = [[UIView alloc] init]; self.section2.qmui_borderColor = UIColorSeparator; - self.section2.qmui_borderPosition = QMUIBorderViewPositionBottom; + self.section2.qmui_borderPosition = QMUIViewBorderPositionBottom; self.section2.qmui_borderWidth = PixelOne; - [self.view addSubview:self.section2]; + [self.scrollView addSubview:self.section2]; - self.progressView2 = [[QMUIPieProgressView alloc] initWithFrame:CGRectMake(0, 0, 45, 45)]; - self.progressView2.tintColor = UIColorTheme3; - [self.progressView2 setProgress:.68]; + self.progressView2 = [[QMUIPieProgressView alloc] initWithFrame:CGRectMake(0, 0, 60, 60)]; + self.progressView2.tintColor = UIColor.qd_tintColor; + self.progressView2.borderWidth = 2; + self.progressView2.borderInset = 3; + [self.progressView2 setProgress:.3]; [self.section2 addSubview:self.progressView2]; - self.progressView3 = [[QMUIPieProgressView alloc] initWithFrame:CGRectMake(0, 0, 25, 25)]; - self.progressView3.tintColor = UIColorTheme5; - [self.progressView3 setProgress:.1]; - [self.section2 addSubview:self.progressView3]; - - self.progressView4 = [[QMUIPieProgressView alloc] initWithFrame:CGRectMake(0, 0, 38, 38)]; - self.progressView4.tintColor = UIColorTheme4; - self.progressView4.backgroundColor = [self.progressView4.tintColor qmui_colorWithAlphaAddedToWhite:.2]; - [self.progressView4 setProgress:.28]; - [self.section2 addSubview:self.progressView4]; - - self.titleLabel2 = [[UILabel alloc] initWithFont:UIFontMake(11) textColor:self.titleLabel1.textColor]; + self.titleLabel2 = [[UILabel alloc] qmui_initWithFont:UIFontMake(11) textColor:UIColor.qd_descriptionTextColor]; self.titleLabel2.numberOfLines = 0; - self.titleLabel2.text = @"通过 backgroundColor 或 tintColor 修改颜色"; + self.titleLabel2.text = @"通过 borderWidth、borderInset 修改样式"; [self.titleLabel2 sizeToFit]; [self.section2 addSubview:self.titleLabel2]; + + self.section3 = [[UIView alloc] init]; + self.section3.qmui_borderColor = UIColorSeparator; + self.section3.qmui_borderPosition = QMUIViewBorderPositionBottom; + self.section3.qmui_borderWidth = PixelOne; + [self.scrollView addSubview:self.section3]; + + self.progressView31 = [[QMUIPieProgressView alloc] initWithFrame:CGRectMake(0, 0, 45, 45)]; + self.progressView31.tintColor = UIColorTheme3; + [self.progressView31 setProgress:.68]; + [self.section3 addSubview:self.progressView31]; + + self.progressView32 = [[QMUIPieProgressView alloc] initWithFrame:CGRectMake(0, 0, 25, 25)]; + self.progressView32.tintColor = UIColorTheme5; + [self.progressView32 setProgress:.1]; + [self.section3 addSubview:self.progressView32]; + + self.progressView33 = [[QMUIPieProgressView alloc] initWithFrame:CGRectMake(0, 0, 38, 38)]; + self.progressView33.tintColor = UIColorTheme4; + self.progressView33.backgroundColor = [self.progressView33.tintColor qmui_colorWithAlphaAddedToWhite:.2]; + [self.progressView33 setProgress:.28]; + [self.section3 addSubview:self.progressView33]; + + self.titleLabel3 = [[UILabel alloc] qmui_initWithFont:UIFontMake(11) textColor:UIColor.qd_descriptionTextColor]; + self.titleLabel3.numberOfLines = 0; + self.titleLabel3.text = @"通过 backgroundColor 或 tintColor 修改颜色"; + [self.titleLabel3 sizeToFit]; + [self.section3 addSubview:self.titleLabel3]; } - (void)viewDidAppear:(BOOL)animated { @@ -98,22 +120,31 @@ - (void)viewDidAppear:(BOOL)animated { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - CGFloat horizontalInset = 25; + self.scrollView.frame = self.view.bounds; + + CGFloat horizontalInset = 25 + self.scrollView.safeAreaInsets.left; CGFloat sectionHeight = 145; CGFloat progressView1MarginRight = 30; - self.section1.frame = CGRectMake(0, CGRectGetMaxY(self.navigationController.navigationBar.frame), CGRectGetWidth(self.view.bounds), sectionHeight); - self.progressView1.frame = CGRectSetXY(self.progressView1.frame, horizontalInset, CGRectGetMinYVerticallyCenterInParentRect(self.section1.frame, self.progressView1.frame) - 6); // 因为下面有个label,因此这里向上偏一点以让视觉上更平衡 + self.section1.frame = CGRectMake(0, 0, CGRectGetWidth(self.scrollView.bounds), sectionHeight); + self.progressView1.frame = CGRectSetXY(self.progressView1.frame, horizontalInset, CGRectGetMinYVerticallyCenterInParentRect(self.section1.bounds, self.progressView1.frame) - 6); // 因为下面有个label,因此这里向上偏一点以让视觉上更平衡 self.titleLabel1.frame = CGRectMake(CGRectGetMinX(self.progressView1.frame), CGRectGetMaxY(self.progressView1.frame) + 9, CGRectGetWidth(self.progressView1.bounds), CGRectGetHeight(self.titleLabel1.bounds)); - self.slider.frame = CGRectMake(CGRectGetMaxX(self.progressView1.frame) + progressView1MarginRight, CGRectGetMidY(self.progressView1.frame) - CGRectGetMidY(self.slider.bounds), CGRectGetWidth(self.view.bounds) - CGRectGetMaxX(self.progressView1.frame) - progressView1MarginRight - horizontalInset, CGRectGetHeight(self.slider.bounds)); + self.slider.frame = CGRectMake(CGRectGetMaxX(self.progressView1.frame) + progressView1MarginRight, CGRectGetMidY(self.progressView1.frame) - CGRectGetMidY(self.slider.bounds), CGRectGetWidth(self.scrollView.bounds) - CGRectGetMaxX(self.progressView1.frame) - progressView1MarginRight - horizontalInset, CGRectGetHeight(self.slider.bounds)); - self.section2.frame = CGRectMake(0, CGRectGetMaxY(self.section1.frame), CGRectGetWidth(self.view.bounds), sectionHeight); - CGPoint referenceCenter = self.progressView1.center; - self.progressView2.center = CGPointMake(referenceCenter.x - 20, referenceCenter.y - 15); - self.progressView3.center = CGPointMake(referenceCenter.x + 20, referenceCenter.y - 15); - self.progressView4.center = CGPointMake(referenceCenter.x + 10, referenceCenter.y + 22); + self.section2.frame = CGRectMake(0, CGRectGetMaxY(self.section1.frame), CGRectGetWidth(self.scrollView.bounds), sectionHeight); + self.progressView2.frame = CGRectSetXY(self.progressView2.frame, horizontalInset + 5, CGRectGetMinYVerticallyCenterInParentRect(self.section2.bounds, self.progressView2.frame)); [self.titleLabel2 sizeToFit]; - self.titleLabel2.frame = CGRectSetXY(self.titleLabel2.frame, CGRectGetMinX(self.slider.frame), CGRectGetMinYVerticallyCenterInParentRect(self.section2.frame, self.titleLabel2.frame)); + self.titleLabel2.frame = CGRectSetXY(self.titleLabel2.frame, CGRectGetMinX(self.slider.frame), CGRectGetMinYVerticallyCenterInParentRect(self.section2.bounds, self.titleLabel2.frame)); + + self.section3.frame = CGRectMake(0, CGRectGetMaxY(self.section2.frame), CGRectGetWidth(self.scrollView.bounds), sectionHeight); + CGPoint referenceCenter = self.progressView1.center; + self.progressView31.center = CGPointMake(referenceCenter.x - 20, referenceCenter.y - 15); + self.progressView32.center = CGPointMake(referenceCenter.x + 20, referenceCenter.y - 15); + self.progressView33.center = CGPointMake(referenceCenter.x + 10, referenceCenter.y + 22); + [self.titleLabel3 sizeToFit]; + self.titleLabel3.frame = CGRectSetXY(self.titleLabel3.frame, CGRectGetMinX(self.slider.frame), CGRectGetMinYVerticallyCenterInParentRect(self.section3.bounds, self.titleLabel3.frame)); + + self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.scrollView.bounds), CGRectGetMaxY(self.section3.frame)); } - (void)handleProgressViewValueChanged:(QMUIPieProgressView *)progressView { diff --git a/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.h b/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.h index 3a1e0d35..7b604455 100644 --- a/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.h @@ -2,7 +2,7 @@ // QDPopupContainerViewController.h // qmuidemo // -// Created by MoLice on 15/12/17. +// Created by QMUI Team on 15/12/17. // Copyright © 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.m b/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.m index 4957d171..3c665a97 100644 --- a/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDPopupContainerViewController.m @@ -2,7 +2,7 @@ // QDPopupContainerViewController.m // qmuidemo // -// Created by MoLice on 15/12/17. +// Created by QMUI Team on 15/12/17. // Copyright © 2015年 QMUI Team. All rights reserved. // @@ -10,7 +10,7 @@ @interface QDPopupContainerView : QMUIPopupContainerView -@property(nonatomic, strong) QMUIQQEmotionManager *qqEmotionManager; +@property(nonatomic, strong) QMUIEmotionInputManager *emotionInputManager; @end @implementation QDPopupContainerView @@ -19,9 +19,20 @@ - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.contentEdgeInsets = UIEdgeInsetsZero; - self.qqEmotionManager = [[QMUIQQEmotionManager alloc] init]; - self.qqEmotionManager.emotionView.sendButton.hidden = YES; - [self.contentView addSubview:self.qqEmotionManager.emotionView]; + self.emotionInputManager = [[QMUIEmotionInputManager alloc] init]; + 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; } @@ -33,7 +44,7 @@ - (CGSize)sizeThatFitsInContentView:(CGSize)size { - (void)layoutSubviews { [super layoutSubviews]; // 所有布局都参照 contentView - self.qqEmotionManager.emotionView.frame = self.contentView.bounds; + self.emotionInputManager.emotionView.frame = self.contentView.bounds; } @end @@ -41,13 +52,17 @@ - (void)layoutSubviews { @interface QDPopupContainerViewController () @property(nonatomic, strong) QMUIButton *button1; -@property(nonatomic, strong) QMUIPopupContainerView *popupView1; +@property(nonatomic, strong) QMUIPopupContainerView *popupByAddSubview; @property(nonatomic, strong) QMUIButton *button2; -@property(nonatomic, strong) QMUIPopupMenuView *popupView2; @property(nonatomic, strong) QMUIButton *button3; -@property(nonatomic, strong) QDPopupContainerView *popupView3; +@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) QMUIPopupContainerView *popupAtBarButtonItem; @end @implementation QDPopupContainerViewController @@ -72,117 +87,188 @@ - (void)initSubviews { [self.button1 setTitle:@"显示默认浮层" forState:UIControlStateNormal]; [self.view addSubview:self.button1]; - // 使用方法 1,以 addSubview: 的形式显示到界面上 - self.popupView1 = [[QMUIPopupContainerView alloc] init]; - self.popupView1.safetyMarginsOfSuperview = UIEdgeInsetsSetTop(self.popupView1.safetyMarginsOfSuperview, NavigationContentTop + 10); - self.popupView1.imageView.image = [[UIImageMake(@"icon_emotion") qmui_imageWithScaleToSize:CGSizeMake(24, 24) contentMode:UIViewContentModeScaleToFill] qmui_imageWithTintColor:[QDThemeManager sharedInstance].currentTheme.themeTintColor]; - self.popupView1.textLabel.text = @"默认自带 imageView、textLabel,可展示简单的内容"; - self.popupView1.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 8); - self.popupView1.didHideBlock = ^(BOOL hidesByUserTap) { - [weakSelf.button1 setTitle:@"显示默认浮层" forState:UIControlStateNormal]; - }; - // 使用方法 1 时需要隐藏浮层,并自行添加到目标 UIView 上 - self.popupView1.hidden = YES; - [self.view addSubview:self.popupView1]; - - - self.button2 = [QDUIHelper generateLightBorderedButton]; [self.button2 addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.button2 setTitle:@"显示菜单浮层" forState:UIControlStateNormal]; + [self.button2 setImage:TableViewCellDisclosureIndicatorImage forState:UIControlStateNormal]; + [self.button2 setTitle:@"右边" forState:UIControlStateNormal]; + self.button2.imagePosition = QMUIButtonImagePositionRight; + self.button2.spacingBetweenImageAndTitle = 4; [self.view addSubview:self.button2]; - // 使用方法 2,以 UIWindow 的形式显示到界面上,这种无需默认隐藏,也无需 add 到某个 UIView 上 - self.popupView2 = [[QMUIPopupMenuView alloc] init]; - self.popupView2.automaticallyHidesWhenUserTap = YES;// 点击空白地方消失浮层 - self.popupView2.maskViewBackgroundColor = UIColorMaskWhite;// 使用方法 2 并且打开了 automaticallyHidesWhenUserTap 的情况下,可以修改背景遮罩的颜色 - self.popupView2.maximumWidth = 180; - self.popupView2.shouldShowItemSeparator = YES; - self.popupView2.separatorInset = UIEdgeInsetsMake(0, self.popupView2.padding.left, 0, self.popupView2.padding.right); - self.popupView2.items = @[[QMUIPopupMenuItem itemWithImage:[UIImageMake(@"icon_tabbar_uikit") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"QMUIKit" handler:^{ - [weakSelf.popupView2 hideWithAnimated:YES]; - }], - [QMUIPopupMenuItem itemWithImage:[UIImageMake(@"icon_tabbar_component") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"Components" handler:^{ - [weakSelf.popupView2 hideWithAnimated:YES]; - }], - [QMUIPopupMenuItem itemWithImage:[UIImageMake(@"icon_tabbar_lab") imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] title:@"Lab" handler:^{ - [weakSelf.popupView2 hideWithAnimated:YES]; - }]]; - self.popupView2.didHideBlock = ^(BOOL hidesByUserTap) { - [weakSelf.button2 setTitle:@"显示菜单浮层" forState:UIControlStateNormal]; + 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 = [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]; }; + self.popupByAddSubview.sourceView = self.button1;// 相对于 button1 布局 + // 使用方法 1 时,显示浮层前需要先手动隐藏浮层,并自行添加到目标 UIView 上 + 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.button3 = [QDUIHelper generateLightBorderedButton]; - [self.button3 addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; - [self.button3 setTitle:@"显示自定义浮层" forState:UIControlStateNormal]; - [self.view addSubview:self.button3]; + // 使用方法 2,以 UIWindow 的形式显示到界面上,这种无需默认隐藏,也无需 add 到某个 UIView 上 + self.popupByWindow = [[QMUIPopupContainerView alloc] init]; + self.popupByWindow.automaticallyHidesWhenUserTap = 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.button4 setTitle:@"显示菜单浮层" forState:UIControlStateNormal]; + }; + self.popupByWindow.sourceView = self.button4;// 相对于 button4 布局 + - self.popupView3 = [[QDPopupContainerView alloc] init]; - self.popupView3.preferLayoutDirection = QMUIPopupContainerViewLayoutDirectionBelow;// 默认在目标的下方,如果目标下方空间不够,会尝试放到目标上方。若上方空间也不够,则缩小自身的高度。 - self.popupView3.didHideBlock = ^(BOOL hidesByUserTap) { - [weakSelf.button3 setTitle:@"显示自定义浮层" forState:UIControlStateNormal]; + self.popupWithCustomView = [[QDPopupContainerView alloc] init]; + self.popupWithCustomView.preferLayoutDirection = QMUIPopupContainerViewLayoutDirectionBelow;// 默认在目标的下方,如果目标下方空间不够,会尝试放到目标上方。若上方空间也不够,则缩小自身的高度。 + self.popupWithCustomView.didHideBlock = ^(BOOL hidesByUserTap) { + [weakSelf.button5 setTitle:@"显示自定义浮层" forState:UIControlStateNormal]; }; + + // 在 UIBarButtonItem 上显示 + self.popupAtBarButtonItem = [[QMUIPopupContainerView alloc] init]; + self.popupAtBarButtonItem.automaticallyHidesWhenUserTap = YES;// 点击空白地方消失浮层 + self.popupAtBarButtonItem.textLabel.textColor = UIColor.qd_mainTextColor; + self.popupAtBarButtonItem.textLabel.font = UIFontMake(16); + self.popupAtBarButtonItem.textLabel.text = @"指向某个 UIBarButtonItem"; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_nav_about") target:self action:@selector(handleRightBarButtonItemEvent)]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // popupView3 使用方法 2 显示,并且没有打开 automaticallyHidesWhenUserTap,则需要手动隐藏 - if (self.popupView3.isShowing) { - [self.popupView3 hideWithAnimated:animated]; + if (self.popupWithCustomView.isShowing) { + [self.popupWithCustomView hideWithAnimated:animated]; } } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - CGFloat minY = CGRectGetMaxY(self.navigationController.navigationBar.frame); + CGFloat minY = self.qmui_navigationBarMaxYInViewCoordinator; CGFloat viewportHeight = CGRectGetHeight(self.view.bounds) - minY; CGFloat sectionHeight = viewportHeight / 3.0; - self.button1.frame = CGRectSetXY(self.button1.frame, CGFloatGetCenter(CGRectGetWidth(self.view.bounds), CGRectGetWidth(self.button1.frame)), minY + (sectionHeight - CGRectGetHeight(self.button1.frame)) / 2.0); - [self.popupView1 layoutWithTargetView:self.button1];// 相对于 button1 布局 + 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.popupView2 layoutWithTargetView:self.button2];// 相对于 button2 布局 + 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.popupView3 layoutWithTargetRectInScreenCoordinate:[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 { if (button == self.button1) { - if (self.popupView1.isShowing) { - [self.popupView1 hideWithAnimated:YES]; + if (self.popupByAddSubview.isShowing) { + [self.popupByAddSubview hideWithAnimated:YES]; [self.button1 setTitle:@"显示默认浮层" forState:UIControlStateNormal]; } else { - [self.popupView1 showWithAnimated:YES]; + [self.popupByAddSubview showWithAnimated:YES]; [self.button1 setTitle:@"隐藏默认浮层" forState:UIControlStateNormal]; } return; } if (button == self.button2) { - [self.popupView2 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.popupView3.isShowing) { - [self.popupView3 hideWithAnimated:YES]; + if (self.popupHorizontal.isShowing) { + [self.popupHorizontal hideWithAnimated:YES]; } else { - [self.popupView3 showWithAnimated:YES]; - [self.button3 setTitle:@"隐藏自定义浮层" forState:UIControlStateNormal]; + 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.button5 setTitle:@"隐藏自定义浮层" forState:UIControlStateNormal]; } return; } } +- (void)handleRightBarButtonItemEvent { + if (self.popupAtBarButtonItem.isShowing) { + [self.popupAtBarButtonItem hideWithAnimated:YES]; + } else { + // 相对于右上角的按钮布局 + self.popupAtBarButtonItem.sourceBarItem = self.navigationItem.rightBarButtonItem; + [self.popupAtBarButtonItem showWithAnimated:YES]; + } +} @end 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/QDSaveImageToSpecifiedAlbumViewController.h b/qmuidemo/Modules/Demos/Components/QDSaveImageToSpecifiedAlbumViewController.h index 766f3dd4..9d0005b5 100644 --- a/qmuidemo/Modules/Demos/Components/QDSaveImageToSpecifiedAlbumViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDSaveImageToSpecifiedAlbumViewController.h @@ -2,7 +2,7 @@ // QDSaveImageToSpecifiedAlbumViewController.h // qmuidemo // -// Created by Kayo Lee on 15/6/9. +// Created by QMUI Team on 15/6/9. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDSaveImageToSpecifiedAlbumViewController.m b/qmuidemo/Modules/Demos/Components/QDSaveImageToSpecifiedAlbumViewController.m index b5e697ef..510e39b4 100644 --- a/qmuidemo/Modules/Demos/Components/QDSaveImageToSpecifiedAlbumViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDSaveImageToSpecifiedAlbumViewController.m @@ -2,32 +2,32 @@ // QDSaveImageToSpecifiedAlbumViewController.m // qmuidemo // -// Created by Kayo Lee on 15/6/9. +// Created by QMUI Team on 15/6/9. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import "QDSaveImageToSpecifiedAlbumViewController.h" -#import #import "QDUIHelper.h" #define TestImageSize CGSizeMake(160, 160) -@implementation QDSaveImageToSpecifiedAlbumViewController { - QMUIButton *_changeImageButton; - QMUIButton *_saveButton; - QMUIAlertController *_alertController; - UIImageView *_testImageView; - - NSArray *_textArray; - ALAssetsLibrary *_assetsLibrary; - NSMutableArray *_albumsArray; -} +@interface QDSaveImageToSpecifiedAlbumViewController () + +@property(nonatomic, strong) QMUIButton *changeImageButton; +@property(nonatomic, strong) QMUIButton *saveButton; +@property(nonatomic, strong) QMUIAlertController *alertController; +@property(nonatomic, strong) UIImageView *testImageView; +@property(nonatomic, copy) NSArray *textArray; +@property(nonatomic, strong) NSMutableArray *albumsArray; + +@end + +@implementation QDSaveImageToSpecifiedAlbumViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - _textArray = @[@"A", @"B", @"C", @"D", @"E", @"F", @"G"]; - _assetsLibrary = [[ALAssetsLibrary alloc] init]; - _albumsArray = [[NSMutableArray alloc] init]; + self.textArray = @[@"A", @"B", @"C", @"D", @"E", @"F", @"G"]; + self.albumsArray = [[NSMutableArray alloc] init]; } return self; } @@ -35,21 +35,21 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB - (void)initSubviews { [super initSubviews]; - _testImageView = [[UIImageView alloc] init]; - [_testImageView setImage:[self randomImage]]; - [self.view addSubview:_testImageView]; + self.testImageView = [[UIImageView alloc] init]; + [self.testImageView setImage:[self randomImage]]; + [self.view addSubview:self.testImageView]; // 普通按钮 - _changeImageButton = [QDUIHelper generateLightBorderedButton]; - [_changeImageButton setTitle:@"更换随机图片" forState:UIControlStateNormal]; - [_changeImageButton addTarget:self action:@selector(handleGeneratedButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - [self.view addSubview:_changeImageButton]; + self.changeImageButton = [QDUIHelper generateLightBorderedButton]; + [self.changeImageButton setTitle:@"更换随机图片" forState:UIControlStateNormal]; + [self.changeImageButton addTarget:self action:@selector(handleGeneratedButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.changeImageButton]; // 边框按钮 - _saveButton = [QDUIHelper generateDarkFilledButton]; - [_saveButton setTitle:@"保存图片到指定相册" forState:UIControlStateNormal]; - [_saveButton addTarget:self action:@selector(handleSaveButtonClick:) forControlEvents:UIControlEventTouchUpInside]; - [self.view addSubview:_saveButton]; + self.saveButton = [QDUIHelper generateDarkFilledButton]; + [self.saveButton setTitle:@"保存图片到指定相册" forState:UIControlStateNormal]; + [self.saveButton addTarget:self action:@selector(handleSaveButtonClick:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.saveButton]; } - (void)viewWillAppear:(BOOL)animated { @@ -62,11 +62,11 @@ - (void)viewWillDisappear:(BOOL)animated { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - CGFloat contentMinY = CGRectGetMaxY(self.navigationController.navigationBar.frame); + CGFloat contentMinY = self.qmui_navigationBarMaxYInViewCoordinator; - _testImageView.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), TestImageSize.width), contentMinY + 60, TestImageSize.width, TestImageSize.height); - _changeImageButton.frame = CGRectSetXY(_changeImageButton.frame, CGFloatGetCenter(CGRectGetWidth(self.view.bounds), CGRectGetWidth(_changeImageButton.frame)), CGRectGetMaxY(_testImageView.frame) + 50); - _saveButton.frame = CGRectSetY(_changeImageButton.frame, CGRectGetMaxY(_changeImageButton.frame) + 30); + self.testImageView.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), TestImageSize.width), contentMinY + 60, TestImageSize.width, TestImageSize.height); + self.changeImageButton.frame = CGRectSetXY(self.changeImageButton.frame, CGFloatGetCenter(CGRectGetWidth(self.view.bounds), CGRectGetWidth(self.changeImageButton.frame)), CGRectGetMaxY(self.testImageView.frame) + 50); + self.saveButton.frame = CGRectSetY(self.changeImageButton.frame, CGRectGetMaxY(self.changeImageButton.frame) + 30); } - (UIImage *)imageFromText:(NSString *)text textColor:(UIColor *)textColor { @@ -74,19 +74,14 @@ - (UIImage *)imageFromText:(NSString *)text textColor:(UIColor *)textColor { NSDictionary *fontAttributes = @{NSFontAttributeName: font, NSForegroundColorAttributeName: textColor}; CGSize size = [text sizeWithAttributes:fontAttributes]; - UIGraphicsBeginImageContextWithOptions(size, NO, 0.0); - - [text drawAtPoint:CGPointMake(0.0, 0.0) withAttributes:fontAttributes]; - - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - return image; + return [UIImage qmui_imageWithSize:size opaque:NO scale:0 actions:^(CGContextRef contextRef) { + [text drawAtPoint:CGPointMake(0.0, 0.0) withAttributes:fontAttributes]; + }]; } - (NSString *)randomText { - NSInteger index = arc4random() % [_textArray count]; - NSString *text = [_textArray objectAtIndex:index]; + NSInteger index = arc4random() % [self.textArray count]; + NSString *text = [self.textArray objectAtIndex:index]; return text; } @@ -98,29 +93,31 @@ - (UIImage *)randomImage { } - (void)handleGeneratedButtonClick:(id)sender { - [_testImageView setImage:[self randomImage]]; + [self.testImageView setImage:[self randomImage]]; } - (void)saveImageToAlbum { - if (!_alertController) { - _alertController = [QMUIAlertController alertControllerWithTitle:@"保存到指定相册" message:nil preferredStyle:QMUIAlertControllerStyleActionSheet]; + if (!self.alertController) { + self.alertController = [QMUIAlertController alertControllerWithTitle:@"保存到指定相册" message:nil preferredStyle:QMUIAlertControllerStyleActionSheet]; // 显示空相册,不显示智能相册 [[QMUIAssetsManager sharedInstance] enumerateAllAlbumsWithAlbumContentType:QMUIAlbumContentTypeAll showEmptyAlbum:YES showSmartAlbumIfSupported:NO usingBlock:^(QMUIAssetsGroup *resultAssetsGroup) { if (resultAssetsGroup) { - [_albumsArray addObject:resultAssetsGroup]; - QMUIAlertAction *action = [QMUIAlertAction actionWithTitle:[resultAssetsGroup name] style:QMUIAlertActionStyleDefault handler:^(QMUIAlertAction *action) { - QMUIImageWriteToSavedPhotosAlbumWithAlbumAssetsGroup(_testImageView.image, resultAssetsGroup, ^(QMUIAsset *asset, NSError *error) { - [QMUITips showSucceed:[NSString stringWithFormat:@"已保存到相册-%@", [resultAssetsGroup name]] inView:self.navigationController.view hideAfterDelay:2]; + [self.albumsArray addObject:resultAssetsGroup]; + QMUIAlertAction *action = [QMUIAlertAction actionWithTitle:[resultAssetsGroup name] style:QMUIAlertActionStyleDefault handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { + QMUIImageWriteToSavedPhotosAlbumWithAlbumAssetsGroup(self.testImageView.image, resultAssetsGroup, ^(QMUIAsset *asset, NSError *error) { + if (asset) { + [QMUITips showSucceed:[NSString stringWithFormat:@"已保存到相册-%@", [resultAssetsGroup name]] inView:self.navigationController.view hideAfterDelay:2]; + } }); }]; - [_alertController addAction:action]; + [self.alertController addAction:action]; } else { QMUIAlertAction *cancelAction = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:nil]; - [_alertController addAction:cancelAction]; + [self.alertController addAction:cancelAction]; } }]; } - [_alertController showWithAnimated:YES]; + [self.alertController showWithAnimated:YES]; } - (void)handleSaveButtonClick:(id)sender { @@ -128,7 +125,7 @@ - (void)handleSaveButtonClick:(id)sender { [QMUIAssetsManager requestAuthorization:^(QMUIAssetAuthorizationStatus status) { // requestAuthorization:(void(^)(QMUIAssetAuthorizationStatus status))handler 不在主线程执行,因此涉及 UI 相关的操作需要手工放置到主流程执行。 dispatch_async(dispatch_get_main_queue(), ^{ - if (status == QMUIAssetAuthorizationStatusAuthorized || status == QMUIAssetAuthorizationStatusNotUsingPhotoKit) { + if (status == QMUIAssetAuthorizationStatusAuthorized) { [self saveImageToAlbum]; } else { [QDUIHelper showAlertWhenSavedPhotoFailureByPermissionDenied]; diff --git a/qmuidemo/Modules/Demos/Components/QDSaveVideoToSpecifiedAlbumViewController.h b/qmuidemo/Modules/Demos/Components/QDSaveVideoToSpecifiedAlbumViewController.h index b4941537..98408c98 100644 --- a/qmuidemo/Modules/Demos/Components/QDSaveVideoToSpecifiedAlbumViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDSaveVideoToSpecifiedAlbumViewController.h @@ -2,7 +2,7 @@ // QDSaveVideoToSpecifiedAlbumViewController.h // qmuidemo // -// Created by Kayo Lee on 15/6/10. +// Created by QMUI Team on 15/6/10. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDSaveVideoToSpecifiedAlbumViewController.m b/qmuidemo/Modules/Demos/Components/QDSaveVideoToSpecifiedAlbumViewController.m index 5a0199dc..855b519a 100644 --- a/qmuidemo/Modules/Demos/Components/QDSaveVideoToSpecifiedAlbumViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDSaveVideoToSpecifiedAlbumViewController.m @@ -2,38 +2,33 @@ // QDSaveVideoToSpecifiedAlbumViewController.m // qmuidemo // -// Created by Kayo Lee on 15/6/10. +// Created by QMUI Team on 15/6/10. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #import "QDSaveVideoToSpecifiedAlbumViewController.h" #import -#import @interface QDSaveVideoToSpecifiedAlbumViewController () @property(nonatomic,copy) NSString *videoPath; +@property(nonatomic, strong) QMUIButton *takeVideoButton; +@property(nonatomic, strong) UIImagePickerController *pickerController; +@property(nonatomic, strong) QMUIAlertController *actionSheet; +@property(nonatomic, strong) NSMutableArray *albumsArray; @end -@implementation QDSaveVideoToSpecifiedAlbumViewController { - QMUIButton *_takeVideoButton; - UIImagePickerController *_pickerController; - QMUIAlertController *_actionSheet; - - ALAssetsLibrary *_assetsLibrary; - NSMutableArray *_albumsArray; -} +@implementation QDSaveVideoToSpecifiedAlbumViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - _assetsLibrary = [[ALAssetsLibrary alloc] init]; - _albumsArray = [[NSMutableArray alloc] init]; + self.albumsArray = [[NSMutableArray alloc] init]; } return self; } -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; +- (void)setupNavigationItems { + [super setupNavigationItems]; self.title = @"保存视频到指定相册"; } @@ -51,48 +46,46 @@ - (void)viewDidLayoutSubviews { } - (void)saveVideoToAlbumWithMediaInfo:(NSDictionary *)info { - if (!_actionSheet) { - _actionSheet = [QMUIAlertController alertControllerWithTitle:@"保存到指定相册" message:nil preferredStyle:QMUIAlertControllerStyleActionSheet]; + if (!self.actionSheet) { + self.actionSheet = [QMUIAlertController alertControllerWithTitle:@"保存到指定相册" message:nil preferredStyle:QMUIAlertControllerStyleActionSheet]; // 显示空相册,不显示智能相册 [[QMUIAssetsManager sharedInstance] enumerateAllAlbumsWithAlbumContentType:QMUIAlbumContentTypeAll showEmptyAlbum:YES showSmartAlbumIfSupported:NO usingBlock:^(QMUIAssetsGroup *resultAssetsGroup) { if (resultAssetsGroup) { - [_albumsArray addObject:resultAssetsGroup]; + [self.albumsArray addObject:resultAssetsGroup]; __weak typeof(self) weakSelf = self; - QMUIAlertAction *action = [QMUIAlertAction actionWithTitle:[resultAssetsGroup name] style:QMUIAlertActionStyleDefault handler:^(QMUIAlertAction *action) { + QMUIAlertAction *action = [QMUIAlertAction actionWithTitle:[resultAssetsGroup name] style:QMUIAlertActionStyleDefault handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(weakSelf.videoPath)) { QMUISaveVideoAtPathToSavedPhotosAlbumWithAlbumAssetsGroup(weakSelf.videoPath, resultAssetsGroup, ^(QMUIAsset *asset, NSError *error) { - [QMUITips showSucceed:[NSString stringWithFormat:@"已保存到相册-%@", [resultAssetsGroup name]] inView:self.navigationController.view hideAfterDelay:2]; + [QMUITips showSucceed:[NSString stringWithFormat:@"已保存到相册-%@", [resultAssetsGroup name]] inView:weakSelf.navigationController.view hideAfterDelay:2]; }); } else { - [QMUITips showError:@"保存失败,视频格式不符合当前设备要求" inView:self.view hideAfterDelay:2]; + [QMUITips showError:@"保存失败,视频格式不符合当前设备要求" inView:weakSelf.view hideAfterDelay:2]; } }]; - [_actionSheet addAction:action]; + [self.actionSheet addAction:action]; } else { // group 为 nil,即遍历相册完毕 QMUIAlertAction *cancelAction = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:nil]; - [_actionSheet addAction:cancelAction]; + [self.actionSheet addAction:cancelAction]; } }]; } NSURL *videoURL = [info objectForKey:UIImagePickerControllerMediaURL]; self.videoPath = [videoURL path]; - [_actionSheet showWithAnimated:YES]; + [self.actionSheet showWithAnimated:YES]; } - (void)handleTakeVideoButtonClick:(id)sender { if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - if (!_pickerController) { - _pickerController = [[UIImagePickerController alloc] init]; - _pickerController.sourceType = UIImagePickerControllerSourceTypeCamera; - _pickerController.mediaTypes = [[NSArray alloc] initWithObjects:(NSString *)kUTTypeMovie, nil];; - _pickerController.delegate = self; + if (!self.pickerController) { + self.pickerController = [[UIImagePickerController alloc] init]; + self.pickerController.sourceType = UIImagePickerControllerSourceTypeCamera; + self.pickerController.mediaTypes = [[NSArray alloc] initWithObjects:(NSString *)kUTTypeMovie, nil];; + self.pickerController.delegate = self; } - [self presentViewController:_pickerController animated:YES completion:^(void) { - [[UIApplication sharedApplication] setStatusBarHidden:YES]; - }]; + [self presentViewController:self.pickerController animated:YES completion:nil]; } else { [QMUITips showError:@"检测不到该设备中有可使用的摄像头" inView:self.view hideAfterDelay:2]; } @@ -106,7 +99,7 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking [QMUIAssetsManager requestAuthorization:^(QMUIAssetAuthorizationStatus status) { // requestAuthorization:(void(^)(QMUIAssetAuthorizationStatus status))handler 不在主线程执行,因此涉及 UI 相关的操作需要手工放置到主流程执行。 dispatch_async(dispatch_get_main_queue(), ^{ - if (status == QMUIAssetAuthorizationStatusAuthorized || status == QMUIAssetAuthorizationStatusNotUsingPhotoKit) { + if (status == QMUIAssetAuthorizationStatusAuthorized) { [self saveVideoToAlbumWithMediaInfo:info]; } else { [QDUIHelper showAlertWhenSavedPhotoFailureByPermissionDenied]; @@ -118,14 +111,11 @@ - (void)imagePickerController:(UIImagePickerController *)picker didFinishPicking } else { [self saveVideoToAlbumWithMediaInfo:info]; } - [[UIApplication sharedApplication] setStatusBarHidden:NO]; }]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { - [picker dismissViewControllerAnimated:YES completion:^(void) { - [[UIApplication sharedApplication] setStatusBarHidden:NO]; - }]; + [picker dismissViewControllerAnimated:YES completion:nil]; } @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/QDSingleImagePickerPreviewViewController.h b/qmuidemo/Modules/Demos/Components/QDSingleImagePickerPreviewViewController.h index cd773453..35c7d8c5 100644 --- a/qmuidemo/Modules/Demos/Components/QDSingleImagePickerPreviewViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDSingleImagePickerPreviewViewController.h @@ -2,7 +2,7 @@ // QDSingleImagePickerPreviewViewController.h // qmuidemo // -// Created by Kayo Lee on 15/5/17. +// Created by QMUI Team on 15/5/17. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDSingleImagePickerPreviewViewController.m b/qmuidemo/Modules/Demos/Components/QDSingleImagePickerPreviewViewController.m index 5407defe..77916e77 100644 --- a/qmuidemo/Modules/Demos/Components/QDSingleImagePickerPreviewViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDSingleImagePickerPreviewViewController.m @@ -2,7 +2,7 @@ // QDSingleImagePickerPreviewViewController.m // qmuidemo // -// Created by Kayo Lee on 15/5/17. +// Created by QMUI Team on 15/5/17. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -49,21 +49,11 @@ - (void)setDownloadStatus:(QMUIAssetDownloadStatus)downloadStatus { } } -- (void)viewDidLoad { - [super viewDidLoad]; - // Do any additional setup after loading the view. -} - - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; _confirmButton.frame = CGRectSetXY(_confirmButton.frame, CGRectGetWidth(self.topToolBarView.frame) - CGRectGetWidth(_confirmButton.frame) - 10, CGRectGetMinY(self.backButton.frame) + CGFloatGetCenter(CGRectGetHeight(self.backButton.frame), CGRectGetHeight(_confirmButton.frame))); } -- (void)didReceiveMemoryWarning { - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - - (void)handleUserAvatarButtonClick:(id)sender { [self.navigationController dismissViewControllerAnimated:YES completion:^(void) { if (self.delegate && [self.delegate respondsToSelector:@selector(imagePickerPreviewViewController:didSelectImageWithImagesAsset:)]) { diff --git a/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.h b/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.h index d6123043..98891eb0 100644 --- a/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.h @@ -2,7 +2,7 @@ // QDStaticTableViewController.h // qmuidemo // -// Created by MoLice on 15/5/3. +// Created by QMUI Team on 15/5/3. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.m b/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.m index 346d0b21..2ba8ef9d 100644 --- a/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDStaticTableViewController.m @@ -2,7 +2,7 @@ // QDStaticTableViewController.m // qmuidemo // -// Created by MoLice on 15/5/3. +// Created by QMUI Team on 15/5/3. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -140,11 +140,12 @@ - (void)handleCheckmarkCellEvent:(QMUIStaticTableViewCellData *)cellData { // 刷新除了被点击的那个 cell 外的其他单选 cell NSMutableArray *indexPathsAnimated = [[NSMutableArray alloc] init]; - for (NSInteger i = 0, l = [self.tableView.dataSource tableView:self.tableView numberOfRowsInSection:cellData.indexPath.section]; i < l; i++) { + for (NSInteger i = 0, l = [self.tableView numberOfRowsInSection:cellData.indexPath.section]; i < l; i++) { if (i != cellData.indexPath.row) { [indexPathsAnimated addObject:[NSIndexPath indexPathForRow:i inSection:cellData.indexPath.section]]; } } + [self.tableView reloadRowsAtIndexPaths:indexPathsAnimated withRowAnimation:UITableViewRowAnimationNone]; // 直接拿到 cell 去修改 accessoryType,保证动画不受 reload 的影响 @@ -172,26 +173,4 @@ - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInte return section == 2 ? @"单选" : nil; } -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - // 因为需要自定义 cell 的内容,所以才需要重写 tableView:cellForRowAtIndexPath: 方法。 - // 当重写这个方法时,请通过 qmui_staticCellDataSource 同名方法获取到 cell 实例 - QMUITableViewCell *cell = [tableView.qmui_staticCellDataSource cellForRowAtIndexPath:indexPath]; - - if ([cell.accessoryView isKindOfClass:[UISwitch class]]) { - UISwitch *switchControl = (UISwitch *)cell.accessoryView; - switchControl.onTintColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; - switchControl.tintColor = switchControl.onTintColor; - } - - return cell; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - // 因为需要自定义 cell 的内容,所以才需要重写 tableView:didSelectRowAtIndexPath: 方法。 - // 当重写这个方法时,请调用 qmui_staticCellDataSource 的同名方法以保证功能的完整 - [tableView.qmui_staticCellDataSource didSelectRowAtIndexPath:indexPath]; - - [tableView deselectRowAtIndexPath:indexPath animated:YES]; -} - @end 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 84% rename from qmuidemo/Modules/Demos/Lab/QDThemeViewController.h rename to qmuidemo/Modules/Demos/Components/QDThemeViewController.h index cde7c5e1..a99ca847 100644 --- a/qmuidemo/Modules/Demos/Lab/QDThemeViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDThemeViewController.h @@ -2,7 +2,7 @@ // QDThemeViewController.h // qmuidemo // -// Created by MoLice on 2017/5/10. +// Created by QMUI Team on 2017/5/10. // Copyright © 2017年 QMUI Team. All rights reserved. // 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/Components/QDToastListViewController.h b/qmuidemo/Modules/Demos/Components/QDToastListViewController.h index 802fa8cf..a52e588b 100644 --- a/qmuidemo/Modules/Demos/Components/QDToastListViewController.h +++ b/qmuidemo/Modules/Demos/Components/QDToastListViewController.h @@ -2,7 +2,7 @@ // QDToastListViewController.h // qmuidemo // -// Created by zhoonchen on 2016/12/13. +// Created by QMUI Team on 2016/12/13. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Components/QDToastListViewController.m b/qmuidemo/Modules/Demos/Components/QDToastListViewController.m index f7f990fd..aed9ed8f 100644 --- a/qmuidemo/Modules/Demos/Components/QDToastListViewController.m +++ b/qmuidemo/Modules/Demos/Components/QDToastListViewController.m @@ -2,7 +2,7 @@ // QDToastListViewController.m // qmuidemo // -// Created by zhoonchen on 2016/12/13. +// Created by QMUI Team on 2016/12/13. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -29,7 +29,7 @@ - (void)initDataSource { } - (void)didSelectCellWithTitle:(NSString *)title { - UIView *parentView = self.navigationController.view; + UIView *parentView = self.view; if ([title isEqualToString:@"Loading"]) { // 如果不需要修改contentView的样式,可以直接使用下面这个工具方法 diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDActivityIndicator.h b/qmuidemo/Modules/Demos/Lab/Animation/QDActivityIndicator.h index 3e3020c3..18607ddd 100644 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDActivityIndicator.h +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDActivityIndicator.h @@ -2,7 +2,7 @@ // QDActivityIndicator.h // WeRead // -// Created by MoLice on 15/5/13. +// Created by QMUI Team on 15/5/13. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -10,10 +10,10 @@ #define QDActivityIndicatorColorDefault UIColorSeparator -typedef enum { +typedef NS_ENUM(NSUInteger, QDActivityIndicatorStyle) { QDActivityIndicatorStyleNormal, // 默认大小 QDActivityIndicatorStyleSmall, // 小一点的,用于想法圈的下拉刷新 -} QDActivityIndicatorStyle; +}; @interface QDActivityIndicator : UIView diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDActivityIndicator.m b/qmuidemo/Modules/Demos/Lab/Animation/QDActivityIndicator.m index 31ab5745..2a13df4c 100644 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDActivityIndicator.m +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDActivityIndicator.m @@ -2,7 +2,7 @@ // QDActivityIndicator.m // WeRead // -// Created by MoLice on 15/5/13. +// Created by QMUI Team on 15/5/13. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -11,11 +11,7 @@ #define QDActivityIndicatorAnimationKey @"lineAnimations" #define AnimationDuration 1.5 -#ifdef IOS10_SDK_ALLOWED @interface QDActivityIndicator () -#else -@interface QDActivityIndicator () -#endif @end diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDAllAnimationViewController.h b/qmuidemo/Modules/Demos/Lab/Animation/QDAllAnimationViewController.h index c0111190..cd59601f 100644 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDAllAnimationViewController.h +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDAllAnimationViewController.h @@ -2,7 +2,7 @@ // QDAllAnimationViewController.h // qmui // -// Created by ZhoonChen on 14-9-23. +// Created by QMUI Team on 14-9-23. // Copyright (c) 2014年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDAllAnimationViewController.m b/qmuidemo/Modules/Demos/Lab/Animation/QDAllAnimationViewController.m index 2769a66e..c3448614 100644 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDAllAnimationViewController.m +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDAllAnimationViewController.m @@ -2,7 +2,7 @@ // QDAllAnimationViewController.m // qmui // -// Created by ZhoonChen on 14-9-23. +// Created by QMUI Team on 14-9-23. // Copyright (c) 2014年 QMUI Team. All rights reserved. // 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 06751e0f..00000000 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDAnimationViewController.h -// qmui -// -// Created by ZhoonChen 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 9325bb0b..00000000 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDAnimationViewController.m +++ /dev/null @@ -1,43 +0,0 @@ -// -// QDAnimationViewController.m -// qmui -// -// Created by ZhoonChen 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/QDCAShapeLoadingViewController.h b/qmuidemo/Modules/Demos/Lab/Animation/QDCAShapeLoadingViewController.h index 24d0e57e..5b2a58a7 100644 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDCAShapeLoadingViewController.h +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDCAShapeLoadingViewController.h @@ -2,7 +2,7 @@ // QDCAShapeLoadingViewController.h // qmuidemo // -// Created by ZhoonChen on 15/9/16. +// Created by QMUI Team on 15/9/16. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDCAShapeLoadingViewController.m b/qmuidemo/Modules/Demos/Lab/Animation/QDCAShapeLoadingViewController.m index bdae3d07..e59b838f 100644 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDCAShapeLoadingViewController.m +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDCAShapeLoadingViewController.m @@ -2,7 +2,7 @@ // QDCAShapeLoadingViewController.m // qmuidemo // -// Created by ZhoonChen on 15/9/16. +// Created by QMUI Team on 15/9/16. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -85,7 +85,7 @@ - (void)viewDidLayoutSubviews { CGFloat lineSpace = 40; CGFloat minY = lineSpace; - _shapeLayer1.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), kLayerSizeValue), NavigationContentTop + minY, kLayerSizeValue, kLayerSizeValue); + _shapeLayer1.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), kLayerSizeValue), self.qmui_navigationBarMaxYInViewCoordinator + minY, kLayerSizeValue, kLayerSizeValue); minY = CGRectGetMaxY(_shapeLayer1.frame) + lineSpace; diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDReplicatorLayerViewController.h b/qmuidemo/Modules/Demos/Lab/Animation/QDReplicatorLayerViewController.h index 74df59d9..a1ba4dc5 100644 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDReplicatorLayerViewController.h +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDReplicatorLayerViewController.h @@ -2,7 +2,7 @@ // QDReplicatorLayerViewController.h // qmuidemo // -// Created by ZhoonChen on 15/9/23. +// Created by QMUI Team on 15/9/23. // Copyright © 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDReplicatorLayerViewController.m b/qmuidemo/Modules/Demos/Lab/Animation/QDReplicatorLayerViewController.m index f13745e2..e3226b89 100644 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDReplicatorLayerViewController.m +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDReplicatorLayerViewController.m @@ -2,7 +2,7 @@ // QDReplicatorLayerViewController.m // qmuidemo // -// Created by ZhoonChen on 15/9/23. +// Created by QMUI Team on 15/9/23. // Copyright © 2015年 QMUI Team. All rights reserved. // @@ -83,7 +83,7 @@ - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; CGFloat lineSpace = 60; - CGFloat minY = NavigationContentTop + lineSpace; + CGFloat minY = self.qmui_navigationBarMaxYInViewCoordinator + lineSpace; CGFloat width1 = kSubLayerWidth * kSubLayerCount + (kSubLayerCount - 1) * kSubLayerSpace; _containerLayer1.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), width1), minY, width1, kSubLayerHeiht); diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.h b/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.h index 9c1efde1..5d7f1462 100644 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.h +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.h @@ -2,7 +2,7 @@ // QDRippleAnimationViewController.h // qmuidemo // -// Created by ZhoonChen on 15/9/11. +// Created by QMUI Team on 15/9/11. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.m b/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.m index 6e15fb88..930d3e63 100644 --- a/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.m +++ b/qmuidemo/Modules/Demos/Lab/Animation/QDRippleAnimationViewController.m @@ -2,7 +2,7 @@ // QDRippleAnimationViewController.m // qmuidemo // -// Created by ZhoonChen on 15/9/11. +// Created by QMUI Team on 15/9/11. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -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]; @@ -94,9 +94,7 @@ - (void)viewDidLayoutSubviews { _scrollView.frame = self.view.bounds; CGFloat insetLeft = 20; - CGFloat labelWidth = CGRectGetWidth(self.view.bounds) - insetLeft * 2; - CGSize labelSize = [_textLabel sizeThatFits:CGSizeMake(labelWidth, CGFLOAT_MAX)]; - _textLabel.frame = CGRectFlatMake(insetLeft, 40, labelWidth, labelSize.height); + _textLabel.frame = CGRectFlatMake(insetLeft, 40, CGRectGetWidth(self.view.bounds) - insetLeft * 2, QMUIViewSelfSizingHeight); _avatarWrapView1.frame = CGRectMake(CGFloatGetCenter(CGRectGetWidth(self.view.bounds), RippleAnimationAvatarSize.width), CGRectGetMaxY(_textLabel.frame) + 70, RippleAnimationAvatarSize.width, RippleAnimationAvatarSize.height); _avatarWrapView2.frame = CGRectMake(CGRectGetMinX(_avatarWrapView1.frame), CGRectGetMaxY(_avatarWrapView1.frame) + 100, RippleAnimationAvatarSize.width, RippleAnimationAvatarSize.height); diff --git a/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.h b/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.h index e69aed93..3a18ecf7 100644 --- a/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.h +++ b/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.h @@ -2,7 +2,7 @@ // QDAllSystemFontsViewController.h // qmuidemo // -// Created by MoLice on 16/9/20. +// Created by QMUI Team on 16/9/20. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.m b/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.m index 62e37490..53e8aa1a 100644 --- a/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.m +++ b/qmuidemo/Modules/Demos/Lab/QDAllSystemFontsViewController.m @@ -2,7 +2,7 @@ // QDAllSystemFontsViewController.m // qmuidemo // -// Created by MoLice on 16/9/20. +// Created by QMUI Team on 16/9/20. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -11,34 +11,41 @@ @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_main_queue(), ^{ - 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]]; } } - if ([self isViewLoaded]) { - [self.tableView reloadData]; - } + dispatch_async(dispatch_get_main_queue(), ^{ + if ([self isViewLoaded]) { + [self.tableView reloadData]; + } + }); }); } return self; } - (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; - if ([fontName qmui_includesString:@"Zapfino"]) { + NSArray *fonts = tableView == self.tableView ? self.allFonts : self.searchResultFonts; + NSString *fontName = fonts[indexPath.row].fontName; + if ([fontName containsString:@"Zapfino"]) { // 这个字体很飘逸,不够高是显示不全的 return TableViewCellNormalHeight + 60; } @@ -46,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:self.tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]; + 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; @@ -64,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.h b/qmuidemo/Modules/Demos/Lab/QDFontPointSizeAndLineHeightViewController.h index bf5bf9f9..ffbeaca3 100644 --- a/qmuidemo/Modules/Demos/Lab/QDFontPointSizeAndLineHeightViewController.h +++ b/qmuidemo/Modules/Demos/Lab/QDFontPointSizeAndLineHeightViewController.h @@ -2,7 +2,7 @@ // QDFontPointSizeAndLineHeightViewController.h // qmuidemo // -// Created by MoLice on 2016/10/30. +// Created by QMUI Team on 2016/10/30. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Lab/QDFontPointSizeAndLineHeightViewController.m b/qmuidemo/Modules/Demos/Lab/QDFontPointSizeAndLineHeightViewController.m index 644b104a..acdaa2eb 100644 --- a/qmuidemo/Modules/Demos/Lab/QDFontPointSizeAndLineHeightViewController.m +++ b/qmuidemo/Modules/Demos/Lab/QDFontPointSizeAndLineHeightViewController.m @@ -2,20 +2,28 @@ // QDFontPointSizeAndLineHeightViewController.m // qmuidemo // -// Created by MoLice on 2016/10/30. +// Created by QMUI Team on 2016/10/30. // Copyright © 2016年 QMUI Team. All rights reserved. // #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] 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,59 +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(CGRectGetMaxY(self.navigationController.navigationBar.frame) + 24, 24, 24, 24); + 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)); - - CGSize exampleLabelSize = [self.exampleLabel sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]; - self.exampleLabel.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.fontPointSizeSlider.frame) + 40, contentWidth, exampleLabelSize.height); + 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.h b/qmuidemo/Modules/Demos/Lab/QDLabViewController.h index 8d70cfdd..4af58e15 100644 --- a/qmuidemo/Modules/Demos/Lab/QDLabViewController.h +++ b/qmuidemo/Modules/Demos/Lab/QDLabViewController.h @@ -2,7 +2,7 @@ // QDLabViewController.h // qmui // -// Created by ZhoonChen on 14/11/5. +// Created by QMUI Team on 14/11/5. // Copyright (c) 2014年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/Lab/QDLabViewController.m b/qmuidemo/Modules/Demos/Lab/QDLabViewController.m index e01b6f3e..3147f547 100644 --- a/qmuidemo/Modules/Demos/Lab/QDLabViewController.m +++ b/qmuidemo/Modules/Demos/Lab/QDLabViewController.m @@ -2,16 +2,25 @@ // QDLabViewController.m // qmui // -// Created by ZhoonChen on 14/11/5. +// Created by QMUI Team on 14/11/5. // Copyright (c) 2014年 QMUI Team. All rights reserved. // #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,31 +30,89 @@ @implementation QDLabViewController - (void)initDataSource { [super initDataSource]; self.dataSource = @[@"All System Fonts", - @"Default Line Height", - @"Theme", + @"Font & LineHeight", @"Animation", + @"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) { + NSString *projectPrefix = @"QMUI"; + if ([logName hasPrefix:projectPrefix]) { + return [logName substringFromIndex:projectPrefix.length]; + } + 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)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; +- (void)didInitialize { + [super didInitialize]; self.title = @"Lab"; - self.navigationItem.rightBarButtonItem = [QMUINavigationButton barButtonItemWithImage:UIImageMake(@"icon_nav_about") position:QMUINavigationButtonPositionRight target:self action:@selector(handleAboutItemEvent)]; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_nav_about") target:self action:@selector(handleAboutItemEvent)]; + AddAccessibilityLabel(self.navigationItem.rightBarButtonItem, @"打开关于界面"); } - (void)handleAboutItemEvent { 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 7d6d32a5..00000000 --- a/qmuidemo/Modules/Demos/Lab/QDThemeViewController.m +++ /dev/null @@ -1,129 +0,0 @@ -// -// QDThemeViewController.m -// qmuidemo -// -// Created by MoLice 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)didInitialized { - [super didInitialized]; - self.themes = @[[[QMUIConfigurationTemplate alloc] init], - [[QMUIConfigurationTemplateGrapefruit alloc] init], - [[QMUIConfigurationTemplateGrass alloc] init], - [[QMUIConfigurationTemplatePinkRose alloc] init]]; - - 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, 24, 24); - 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.h b/qmuidemo/Modules/Demos/UIKit/QDAlertController.h index 982e8c04..7cffa233 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDAlertController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDAlertController.h @@ -2,7 +2,7 @@ // QDAlertController.h // qmuidemo // -// Created by ZhoonChen on 15/7/20. +// Created by QMUI Team on 15/7/20. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDAlertController.m b/qmuidemo/Modules/Demos/UIKit/QDAlertController.m index eff836cb..3b55557b 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDAlertController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDAlertController.m @@ -2,7 +2,7 @@ // QDAlertController.m // qmuidemo // -// Created by ZhoonChen on 15/7/20. +// Created by QMUI Team on 15/7/20. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -21,11 +21,15 @@ - (void)initDataSource { self.dataSource = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: kSectionTitleForAlert, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: @"显示一个 alert 弹窗", @"", + @"支持 alert 背景磨砂", @"使用 mainVisualEffectView 指定整个弹窗的磨砂", @"支持自定义 alert 样式", @"支持以 UIAppearance 方式设置全局统一样式", + @"支持添加输入框", @"升起弹窗时会自动聚焦第一个输入框,也可自定义布局", @"支持自定义内容", @"可以将一个 UIView 作为 QMUIAlertController 的 contentView", nil], kSectionTitleForActionSheet, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: @"显示一个 actionSheet 菜单", @"", + @"支持按钮分列", @"可左右显示两列", + @"支持 actionSheet 背景磨砂", @"可分别为取消按钮和其他按钮指定不同的磨砂", @"支持自定义 actionSheet 样式", @"支持以 UIAppearance 方式设置全局统一样式", nil], kSectionTitleForSystem, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: @@ -39,13 +43,26 @@ - (void)didSelectCellWithTitle:(NSString *)title { [self.tableView qmui_clearsSelection]; if ([title isEqualToString:@"显示一个 alert 弹窗"]) { - QMUIAlertAction *action1 = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:^(QMUIAlertAction *action) { - }]; - QMUIAlertAction *action2 = [QMUIAlertAction actionWithTitle:@"删除" style:QMUIAlertActionStyleDestructive handler:^(QMUIAlertAction *action) { - }]; + QMUIAlertAction *action1 = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:NULL]; + QMUIAlertAction *action2 = [QMUIAlertAction actionWithTitle:@"删除" style:QMUIAlertActionStyleDestructive handler:NULL]; + QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"确定删除?" message:@"删除后将无法恢复,请慎重考虑" preferredStyle:QMUIAlertControllerStyleAlert]; + [alertController addAction:action1]; + [alertController addAction:action2]; + [alertController showWithAnimated:YES]; + return; + } + + if ([title isEqualToString:@"支持 alert 背景磨砂"]) { + QMUIAlertAction *action1 = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:NULL]; + QMUIAlertAction *action2 = [QMUIAlertAction actionWithTitle:@"删除" style:QMUIAlertActionStyleDestructive handler:NULL]; QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"确定删除?" message:@"删除后将无法恢复,请慎重考虑" preferredStyle:QMUIAlertControllerStyleAlert]; [alertController addAction:action1]; [alertController addAction:action2]; + 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; [alertController showWithAnimated:YES]; return; } @@ -54,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_imageWithScaleToSize:CGSizeMake(18, 18) contentMode:UIViewContentModeScaleToFill] 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); // 弹窗 @@ -65,8 +82,8 @@ - (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.alertSeperatorColor = alertController.alertButtonBackgroundColor; + alertController.alertHeaderBackgroundColor = UIColor.qd_tintColor; + alertController.alertSeparatorColor = alertController.alertButtonBackgroundColor; alertController.alertTitleMessageSpacing = 7; NSMutableDictionary *buttonAttributes = [[NSMutableDictionary alloc] initWithDictionary:alertController.alertButtonAttributes]; @@ -83,11 +100,35 @@ - (void)didSelectCellWithTitle:(NSString *)title { return; } - if ([title isEqualToString:@"支持自定义内容"]) { - QMUIAlertAction *action1 = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:^(QMUIAlertAction *action) { + 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 = @"姓"; }]; - QMUIAlertAction *action2 = [QMUIAlertAction actionWithTitle:@"确定" style:QMUIAlertActionStyleDestructive handler:^(QMUIAlertAction *action) { + [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]; UIView *customView = [self animationView]; QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"正在加载" message:@"加载结束之前请勿取消" preferredStyle:QMUIAlertControllerStyleAlert]; [alertController addAction:action1]; @@ -98,11 +139,11 @@ - (void)didSelectCellWithTitle:(NSString *)title { } if ([title isEqualToString:@"显示一个 actionSheet 菜单"]) { - QMUIAlertAction *action1 = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:^(QMUIAlertAction *action) { + QMUIAlertAction *action1 = [QMUIAlertAction actionWithTitle:@"取消" style:QMUIAlertActionStyleCancel handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { }]; - QMUIAlertAction *action2 = [QMUIAlertAction actionWithTitle:@"删除" style:QMUIAlertActionStyleDestructive handler:^(QMUIAlertAction *action) { + QMUIAlertAction *action2 = [QMUIAlertAction actionWithTitle:@"删除" style:QMUIAlertActionStyleDestructive handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { }]; - QMUIAlertAction *action3 = [QMUIAlertAction actionWithTitle:@"置灰按钮" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertAction *action) { + QMUIAlertAction *action3 = [QMUIAlertAction actionWithTitle:@"置灰按钮" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { }]; action3.enabled = NO; QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"确定删除?" message:@"删除后将无法恢复,请慎重考虑" preferredStyle:QMUIAlertControllerStyleActionSheet]; @@ -113,10 +154,47 @@ - (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) { + }]; + QMUIAlertAction *action2 = [QMUIAlertAction actionWithTitle:@"删除" style:QMUIAlertActionStyleDestructive handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { + }]; + QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"确定删除?" message:@"删除后将无法恢复,请慎重考虑" preferredStyle:QMUIAlertControllerStyleActionSheet]; + [alertController addAction:action1]; + [alertController addAction:action2]; + UIVisualEffectView *visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + visualEffectView.qmui_foregroundColor = UIColorMakeWithRGBA(255, 255, 255, .6);// 一般用默认值就行,不用主动去改,这里只是为了展示用法 + alertController.mainVisualEffectView = visualEffectView;// 这个负责上半部分的磨砂 + + visualEffectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]]; + visualEffectView.qmui_foregroundColor = UIColorMakeWithRGBA(255, 255, 255, .6);// 一般用默认值就行,不用主动去改,这里只是为了展示用法 + alertController.cancelButtonVisualEffectView = visualEffectView;// 这个负责取消按钮的磨砂 + alertController.sheetHeaderBackgroundColor = nil; + alertController.sheetButtonBackgroundColor = nil; + [alertController showWithAnimated:YES]; + return; + } + 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_imageWithScaleToSize:CGSizeMake(22, 22) contentMode:UIViewContentModeScaleToFill] 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]; @@ -125,8 +203,8 @@ - (void)didSelectCellWithTitle:(NSString *)title { NSMutableDictionary *messageAttributs = [[NSMutableDictionary alloc] initWithDictionary:alertController.sheetMessageAttributes]; messageAttributs[NSForegroundColorAttributeName] = UIColorWhite; alertController.sheetMessageAttributes = messageAttributs; - alertController.sheetHeaderBackgroundColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; - alertController.sheetSeperatorColor = alertController.sheetButtonBackgroundColor; + alertController.sheetHeaderBackgroundColor = UIColor.qd_tintColor; + alertController.sheetSeparatorColor = alertController.sheetButtonBackgroundColor; NSMutableDictionary *buttonAttributes = [[NSMutableDictionary alloc] initWithDictionary:alertController.sheetButtonAttributes]; buttonAttributes[NSForegroundColorAttributeName] = alertController.sheetHeaderBackgroundColor; 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.h b/qmuidemo/Modules/Demos/UIKit/QDButtonEdgeInsetsViewController.h index 1b2d74cb..3c66f192 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDButtonEdgeInsetsViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDButtonEdgeInsetsViewController.h @@ -2,7 +2,7 @@ // QDButtonEdgeInsetsViewController.h // qmuidemo // -// Created by MoLice on 2017/7/12. +// Created by QMUI Team on 2017/7/12. // Copyright © 2017年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDButtonEdgeInsetsViewController.m b/qmuidemo/Modules/Demos/UIKit/QDButtonEdgeInsetsViewController.m index 34abf117..2244a4e6 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDButtonEdgeInsetsViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDButtonEdgeInsetsViewController.m @@ -2,168 +2,63 @@ // QDButtonEdgeInsetsViewController.m // qmuidemo // -// Created by MoLice on 2017/7/12. +// Created by QMUI Team on 2017/7/12. // Copyright © 2017年 QMUI Team. All rights reserved. // #import "QDButtonEdgeInsetsViewController.h" const NSInteger TagForStaticSizeView = 111; +const CGSize SizeForStaticSizeView = {140, 60}; + +@interface QDButtonConfigurePopupView : QMUIPopupContainerView + +@property(nonatomic, weak) UIButton *bindButton; +@end @interface QDButtonEdgeInsetsViewController () @property(nonatomic, strong) UIScrollView *scrollView; +@property(nonatomic, strong) QDButtonConfigurePopupView *configurePopupView; @end @implementation QDButtonEdgeInsetsViewController -- (void)viewDidLoad { - [super viewDidLoad]; +- (void)initSubviews { + [super initSubviews]; self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; [self.view addSubview:self.scrollView]; ({ - [self generateLabelWithTitle:@"default"]; + [self generateLabelWithTitle:@"sizeToFit,无 insets"]; [self generateSystemButton]; [self generateQMUIButton]; }); ({ - [self generateLabelWithTitle:@"contentEdgeInsets(0, 8, 0, 8)"]; + [self generateLabelWithTitle:@"sizeToFit\ncontentEdgeInsets(0, 8, 0, 8)\nimageEdgeInsets(0, 8, 0, 8)\ntitleEdgeInsets(0, 8, 0, 8)"]; UIButton *systemButton = [self generateSystemButton]; systemButton.contentEdgeInsets = UIEdgeInsetsMake(0, 8, 0, 8); + systemButton.imageEdgeInsets = UIEdgeInsetsMake(0, 8, 0, 8); + systemButton.titleEdgeInsets = UIEdgeInsetsMake(0, 8, 0, 8); QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.contentEdgeInsets = systemButton.contentEdgeInsets; - }); - - ({ - [self generateLabelWithTitle:@"imageEdgeInsets(0, 0, 0, 8)"]; - UIButton *systemButton = [self generateSystemButton]; - systemButton.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 8); - QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.imageEdgeInsets = systemButton.imageEdgeInsets; - }); - - ({ - [self generateLabelWithTitle:@"imageEdgeInsets(0, 8, 0, 0)"]; - UIButton *systemButton = [self generateSystemButton]; - systemButton.imageEdgeInsets = UIEdgeInsetsMake(0, 8, 0, 0); - QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.imageEdgeInsets = systemButton.imageEdgeInsets; - }); - - ({ - [self generateLabelWithTitle:@"titleEdgeInsets(0, 8, 0, 0)"]; - UIButton *systemButton = [self generateSystemButton]; - systemButton.titleEdgeInsets = UIEdgeInsetsMake(0, 8, 0, 0); - QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.titleEdgeInsets = systemButton.titleEdgeInsets; - }); - - ({ - [self generateLabelWithTitle:@"titleEdgeInsets(0, 0, 0, 8)"]; - UIButton *systemButton = [self generateSystemButton]; - systemButton.titleEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 8); - QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.titleEdgeInsets = systemButton.titleEdgeInsets; - }); - - ({ - [self generateLabelWithTitle:@"UIControlContentHorizontalAlignmentFill"]; - UIButton *systemButton = [self generateSystemButton]; - systemButton.tag = TagForStaticSizeView; - systemButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill; - QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.tag = systemButton.tag; - qmuiButton.contentHorizontalAlignment = systemButton.contentHorizontalAlignment; - }); - - // 只显示 image - ({ - [self generateLabelWithTitle:@"Fill Only Image"]; - UIButton *systemButton = [self generateSystemButton]; - systemButton.tag = TagForStaticSizeView; - systemButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill; - [systemButton setTitle:nil forState:UIControlStateNormal]; - QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.tag = systemButton.tag; - qmuiButton.contentHorizontalAlignment = systemButton.contentHorizontalAlignment; - [qmuiButton setTitle:nil forState:UIControlStateNormal]; + [self copyButtonPropertyFromButton:systemButton toButton:qmuiButton]; }); - // 只显示 title ({ - [self generateLabelWithTitle:@"Fill Only Title"]; + [self generateLabelWithTitle:@"固定宽高\ncontentEdgeInsets(0, 8, 0, 8)\nimageEdgeInsets(0, 8, 0, 8)\ntitleEdgeInsets(0, 8, 0, 8)"]; UIButton *systemButton = [self generateSystemButton]; systemButton.tag = TagForStaticSizeView; - systemButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill; - [systemButton setImage:nil forState:UIControlStateNormal]; - [systemButton setTitle:@"UIControlContentHorizontalAlignmentFill" forState:UIControlStateNormal]; - QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.tag = systemButton.tag; - qmuiButton.contentHorizontalAlignment = systemButton.contentHorizontalAlignment; - [qmuiButton setImage:nil forState:UIControlStateNormal]; - [qmuiButton setTitle:@"UIControlContentHorizontalAlignmentFill" forState:UIControlStateNormal]; - }); - - ({ - [self generateLabelWithTitle:@"UIControlContentHorizontalAlignmentRight"]; - UIButton *systemButton = [self generateSystemButton]; - systemButton.tag = TagForStaticSizeView; - systemButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight; - QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.tag = systemButton.tag; - qmuiButton.contentHorizontalAlignment = systemButton.contentHorizontalAlignment; - }); - - ({ - [self generateLabelWithTitle:@"UIControlContentVerticalAlignmentFill"]; - UIButton *systemButton = [self generateSystemButton]; - systemButton.tag = TagForStaticSizeView; - systemButton.contentVerticalAlignment = UIControlContentVerticalAlignmentFill; - QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.tag = systemButton.tag; - qmuiButton.contentVerticalAlignment = systemButton.contentVerticalAlignment; - }); - - // 只显示 image - ({ - [self generateLabelWithTitle:@"Fill Only Image"]; - UIButton *systemButton = [self generateSystemButton]; - systemButton.tag = TagForStaticSizeView; - systemButton.contentVerticalAlignment = UIControlContentVerticalAlignmentFill; - [systemButton setTitle:nil forState:UIControlStateNormal]; - QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.tag = systemButton.tag; - qmuiButton.contentVerticalAlignment = systemButton.contentVerticalAlignment; - [qmuiButton setTitle:nil forState:UIControlStateNormal]; - }); - - // 只显示 title - ({ - [self generateLabelWithTitle:@"Fill Only Title"]; - UIButton *systemButton = [self generateSystemButton]; - systemButton.tag = TagForStaticSizeView; - systemButton.contentVerticalAlignment = UIControlContentVerticalAlignmentFill; - [systemButton setTitle:@"UIControlContentVerticalAlignmentFill" forState:UIControlStateNormal]; - [systemButton setImage:nil forState:UIControlStateNormal]; + systemButton.contentEdgeInsets = UIEdgeInsetsMake(0, 8, 0, 8); + systemButton.imageEdgeInsets = UIEdgeInsetsMake(0, 8, 0, 8); + systemButton.titleEdgeInsets = UIEdgeInsetsMake(0, 8, 0, 8); QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.tag = systemButton.tag; - qmuiButton.contentVerticalAlignment = systemButton.contentVerticalAlignment; - [qmuiButton setTitle:@"UIControlContentVerticalAlignmentFill" forState:UIControlStateNormal]; - [qmuiButton setImage:nil forState:UIControlStateNormal]; + [self copyButtonPropertyFromButton:systemButton toButton:qmuiButton]; }); - ({ - [self generateLabelWithTitle:@"UIControlContentVerticalAlignmentBottom"]; - UIButton *systemButton = [self generateSystemButton]; - systemButton.tag = TagForStaticSizeView; - systemButton.contentVerticalAlignment = UIControlContentVerticalAlignmentBottom; - QMUIButton *qmuiButton = [self generateQMUIButton]; - qmuiButton.tag = systemButton.tag; - qmuiButton.contentVerticalAlignment = systemButton.contentVerticalAlignment; - }); + self.configurePopupView = [[QDButtonConfigurePopupView alloc] init]; + self.configurePopupView.automaticallyHidesWhenUserTap = YES; } - (UIButton *)generateSystemButton { @@ -176,7 +71,7 @@ - (QMUIButton *)generateQMUIButton { - (__kindof UIButton *)generateButtonWithClass:(Class)buttonClass { UIButton *button = [[buttonClass alloc] init]; - [button setTitle:@"Button" forState:UIControlStateNormal]; + [button setTitle:@"短标题" forState:UIControlStateNormal]; [button setImage:[UIImage qmui_imageWithColor:UIColorBlue size:CGSizeMake(20, 20) cornerRadius:0] forState:UIControlStateNormal]; button.backgroundColor = UIColorTestRed; button.imageView.layer.borderWidth = PixelOne; @@ -185,12 +80,33 @@ - (__kindof UIButton *)generateButtonWithClass:(Class)buttonClass { button.titleLabel.font = UIFontMake(18); button.titleLabel.layer.borderWidth = PixelOne; 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; } +- (void)copyButtonPropertyFromButton:(UIButton *)fromButton toButton:(UIButton *)toButton { + toButton.tag = fromButton.tag; + 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] 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; [self.scrollView addSubview:label]; return label; @@ -205,7 +121,7 @@ - (void)viewDidLayoutSubviews { for (NSInteger i = 0; i < self.scrollView.subviews.count; i++) { UIView *subview = self.scrollView.subviews[i]; if (subview.tag == TagForStaticSizeView) { - subview.frame = CGRectSetSize(subview.frame, CGSizeMake(100, 40)); + subview.frame = CGRectSetSize(subview.frame, SizeForStaticSizeView); } else { [subview sizeToFit]; } @@ -224,11 +140,11 @@ - (void)viewDidLayoutSubviews { self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.view.bounds), minY); } -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; +- (void)setupNavigationItems { + [super setupNavigationItems]; self.title = @"Button EdgeInsets Testing"; if (self.qmui_isPresented) { - self.navigationItem.leftBarButtonItem = [QMUINavigationButton closeBarButtonItemWithTarget:self action:@selector(handleCloseItemEvent)]; + self.navigationItem.leftBarButtonItem = [UIBarButtonItem qmui_closeItemWithTarget:self action:@selector(handleCloseItemEvent)]; } } @@ -236,11 +152,262 @@ - (void)handleCloseItemEvent { [self dismissViewControllerAnimated:YES completion:NULL]; } +- (void)handleButtonEvent:(UIButton *)button { + self.configurePopupView.bindButton = button; + self.configurePopupView.sourceView = button; + [self.configurePopupView showWithAnimated:YES]; +} + #pragma mark - -- (UIImage *)navigationBarBackgroundImage { +- (UIImage *)qmui_navigationBarBackgroundImage { // debug warning return [UIImage qmui_imageWithColor:UIColorMake(232, 46, 46)]; } @end + +@implementation QDButtonConfigurePopupView { + UILabel *_shouldShowImageLabel; + UISwitch *_shouldShowImageSwitch; + CALayer *_shouldShowImageSeparatorLayer; + + UILabel *_shouldShowTitleLabel; + UISwitch *_shouldShowTitleSwitch; + CALayer *_shouldShowTitleSeparatorLayer; + + UILabel *_shouldShowSubtitleLabel; + UISwitch *_shouldShowSubtitleSwitch; + CALayer *_shouldShowSubtitleSeparatorLayer; + + UILabel *_bigImageLabel; + UISwitch *_bigImageSwitch; + CALayer *_bigImageSeparatorLayer; + + UILabel *_longTitleLabel; + UISwitch *_longTitleSwitch; + CALayer *_longTitleSeparatorLayer; + + UILabel *_imagePositionLabel; + UISegmentedControl *_imagePositionSegmented; + CALayer *_imagePositionSeparatorLayer; + + UILabel *_horizontalAlignmentLabel; + UISegmentedControl *_horizontalAlignmentSegmented; + CALayer *_horizontalAlignmentSeparatorLayer; + + UILabel *_verticalAlignmentLabel; + UISegmentedControl *_verticalAlignmentSegmented; + CALayer *_verticalAlignmentSeparatorLayer; +} + +- (void)didInitialize { + [super didInitialize]; + + self.contentEdgeInsets = UIEdgeInsetsSetTop(self.contentEdgeInsets, 0); + + // 是否要显示图片 + _shouldShowImageLabel = [self generateLabelWithText:@"setImage"]; + _shouldShowImageSwitch = [self generateSwitch]; + _shouldShowImageSeparatorLayer = [CALayer qmui_separatorLayer]; + [self.contentView.layer addSublayer:_shouldShowImageSeparatorLayer]; + + // 是否要显示标题 + _shouldShowTitleLabel = [self generateLabelWithText:@"setTitle"]; + _shouldShowTitleSwitch = [self generateSwitch]; + _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]; + _bigImageSeparatorLayer = [CALayer qmui_separatorLayer]; + [self.contentView.layer addSublayer:_bigImageSeparatorLayer]; + + // 是否要显示超长标题 + _longTitleLabel = [self generateLabelWithText:@"longTitle"]; + _longTitleSwitch = [self generateSwitch]; + _longTitleSeparatorLayer = [CALayer qmui_separatorLayer]; + [self.contentView.layer addSublayer:_longTitleSeparatorLayer]; + + // 改变图片的位置(仅对 QMUIButton 生效) + _imagePositionLabel = [self generateLabelWithText:@"imagePosition"]; + _imagePositionSegmented = [self generateSegmentedWithItems:@[@"Top", @"Left", @"Bottom", @"Right"]]; + _imagePositionSeparatorLayer = [CALayer qmui_separatorLayer]; + [self.contentView.layer addSublayer:_imagePositionSeparatorLayer]; + + // 内容的水平对齐方式 + _horizontalAlignmentLabel = [self generateLabelWithText:@"horizontal"]; + _horizontalAlignmentSegmented = [self generateSegmentedWithItems:@[@"Center", @"Left", @"Right", @"Fill"]]; + _horizontalAlignmentSeparatorLayer = [CALayer qmui_separatorLayer]; + [self.contentView.layer addSublayer:_horizontalAlignmentSeparatorLayer]; + + // 内容的垂直对齐方式 + _verticalAlignmentLabel = [self generateLabelWithText:@"vertical"]; + _verticalAlignmentSegmented = [self generateSegmentedWithItems:@[@"Center", @"Top", @"Bottom", @"Fill"]]; + _verticalAlignmentSeparatorLayer = [CALayer qmui_separatorLayer]; + [self.contentView.layer addSublayer:_verticalAlignmentSeparatorLayer]; +} + +- (CGSize)sizeThatFitsInContentView:(CGSize)size { + NSInteger rowCount = 8; + return CGSizeMake(size.width, (45 + PixelOne * 2) * rowCount); +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + CGFloat minY = 14; + CGPoint switchCenter = CGPointMake(flat(CGRectGetWidth(self.contentView.bounds) - CGRectGetWidth(_shouldShowImageSwitch.frame) / 2), 0); + + _shouldShowImageLabel.frame = CGRectSetY(_shouldShowImageLabel.frame, minY); + _shouldShowImageSwitch.center = CGPointMake(switchCenter.x, _shouldShowImageLabel.center.y); + _shouldShowImageSeparatorLayer.frame = CGRectMake(0, CGRectGetMaxY(_shouldShowImageLabel.frame) + 14, CGRectGetWidth(self.contentView.bounds), PixelOne); + minY = ceil(CGRectGetMaxY(_shouldShowImageSeparatorLayer.frame)); + + _shouldShowTitleLabel.frame = CGRectSetY(_shouldShowTitleLabel.frame, minY + 14); + _shouldShowTitleSwitch.center = CGPointMake(switchCenter.x, _shouldShowTitleLabel.center.y); + _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); + minY = ceil(CGRectGetMaxY(_bigImageSeparatorLayer.frame)); + + _longTitleLabel.frame = CGRectSetY(_longTitleLabel.frame, minY + 14); + _longTitleSwitch.center = CGPointMake(switchCenter.x, _longTitleLabel.center.y); + _longTitleSeparatorLayer.frame = CGRectSetY(_shouldShowImageSeparatorLayer.frame, CGRectGetMaxY(_longTitleLabel.frame) + 14); + minY = ceil(CGRectGetMaxY(_longTitleSeparatorLayer.frame)); + + _imagePositionLabel.frame = CGRectSetY(_imagePositionLabel.frame, minY + 14); + _imagePositionSegmented.center = CGPointMake(flat(CGRectGetWidth(self.contentView.bounds) - CGRectGetWidth(_imagePositionSegmented.frame) / 2), _imagePositionLabel.center.y); + _imagePositionSeparatorLayer.frame = CGRectSetY(_shouldShowImageSeparatorLayer.frame, CGRectGetMaxY(_imagePositionLabel.frame) + 14); + minY = ceil(CGRectGetMaxY(_imagePositionSeparatorLayer.frame)); + + _horizontalAlignmentLabel.frame = CGRectSetY(_horizontalAlignmentLabel.frame, minY + 14); + _horizontalAlignmentSegmented.center = CGPointMake(flat(CGRectGetWidth(self.contentView.bounds) - CGRectGetWidth(_horizontalAlignmentSegmented.frame) / 2), _horizontalAlignmentLabel.center.y); + _horizontalAlignmentSeparatorLayer.frame = CGRectSetY(_shouldShowImageSeparatorLayer.frame, CGRectGetMaxY(_horizontalAlignmentLabel.frame) + 14); + minY = ceil(CGRectGetMaxY(_horizontalAlignmentSeparatorLayer.frame)); + + _verticalAlignmentLabel.frame = CGRectSetY(_verticalAlignmentLabel.frame, minY + 14); + _verticalAlignmentSegmented.center = CGPointMake(flat(CGRectGetWidth(self.contentView.bounds) - CGRectGetWidth(_verticalAlignmentSegmented.frame) / 2), _verticalAlignmentLabel.center.y); + _verticalAlignmentSeparatorLayer.frame = CGRectSetY(_shouldShowImageSeparatorLayer.frame, CGRectGetMaxY(_verticalAlignmentLabel.frame) + 14); + minY = ceil(CGRectGetMaxY(_verticalAlignmentSeparatorLayer.frame)); +} + +- (UILabel *)generateLabelWithText:(NSString *)text { + UILabel *label = [[UILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; + label.text = text; + [label sizeToFit]; + [self.contentView addSubview:label]; + return label; +} + +- (UISwitch *)generateSwitch { + UISwitch *switchControl = [[UISwitch alloc] init]; + [switchControl sizeToFit]; + switchControl.transform = CGAffineTransformMakeScale(.7, .7); + [switchControl addTarget:self action:@selector(handleSwitchControlEvent:) forControlEvents:UIControlEventValueChanged]; + [self.contentView addSubview:switchControl]; + return switchControl; +} + +- (UISegmentedControl *)generateSegmentedWithItems:(NSArray *)items { + UISegmentedControl *segmentedControl = [[UISegmentedControl alloc] initWithItems:items]; + segmentedControl.tintColor = UIColor.qd_tintColor; + segmentedControl.frame = CGRectSetWidth(segmentedControl.frame, 240);// 统一按照最长的来就行啦 + segmentedControl.transform = CGAffineTransformMakeScale(.8, .8); + [segmentedControl addTarget:self action:@selector(handleSegmentedControlEvent:) forControlEvents:UIControlEventValueChanged]; + [self.contentView addSubview:segmentedControl]; + return segmentedControl; +} + +- (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]]; + _imagePositionSegmented.selectedSegmentIndex = [bindButton isKindOfClass:[QMUIButton class]] ? ((QMUIButton *)bindButton).imagePosition : -1; + _horizontalAlignmentSegmented.selectedSegmentIndex = bindButton.contentHorizontalAlignment; + _verticalAlignmentSegmented.selectedSegmentIndex = bindButton.contentVerticalAlignment; +} + +- (void)handleSwitchControlEvent:(UISwitch *)switchControl { + if (switchControl == _shouldShowImageSwitch || switchControl == _bigImageSwitch) { + [self updateImageForButton:self.bindButton shouldShowImage:_shouldShowImageSwitch.on shouldShowBigImage:_bigImageSwitch.on]; + } + 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]; +} + +- (void)handleSegmentedControlEvent:(UISegmentedControl *)segmentedControl { + if (segmentedControl == _imagePositionSegmented) { + ((QMUIButton *)self.bindButton).imagePosition = segmentedControl.selectedSegmentIndex; + } else if (segmentedControl == _horizontalAlignmentSegmented) { + self.bindButton.contentHorizontalAlignment = segmentedControl.selectedSegmentIndex; + } else if (segmentedControl == _verticalAlignmentSegmented) { + self.bindButton.contentVerticalAlignment = segmentedControl.selectedSegmentIndex; + } + + [self updateLayoutForButton:self.bindButton]; +} + +- (void)updateImageForButton:(UIButton *)button shouldShowImage:(BOOL)shouldShowImage shouldShowBigImage:(BOOL)shouldShowBigImage { + if (!shouldShowImage) { + [button setImage:nil forState:UIControlStateNormal]; + } else { + UIImage *image = [UIImage qmui_imageWithColor:UIColorBlue size:shouldShowBigImage ? CGSizeMake(80, 80) : CGSizeMake(20, 20) cornerRadius:0]; + [button setImage:image forState:UIControlStateNormal]; + } +} + +- (void)updateTitleForButton:(UIButton *)button shouldShowTitle:(BOOL)shouldShowTitle shouldShowLongTitle:(BOOL)shouldShowLongTitle { + if (!shouldShowTitle) { + [button setTitle:nil forState:UIControlStateNormal]; + } else { + NSString *title = shouldShowLongTitle ? @"很长很长的标题很长很长的标题很长很长的标题" : @"短标题"; + [button setTitle:title forState:UIControlStateNormal]; + } +} + +- (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]; + } + [button setNeedsLayout]; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.h b/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.h index 7551b9d2..227fd287 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.h @@ -2,7 +2,7 @@ // QDButtonViewController.h // qmui // -// Created by ZhoonChen on 14/11/6. +// Created by QMUI Team on 14/11/6. // Copyright (c) 2014年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.m b/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.m index 84525658..22fc61f0 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDButtonViewController.m @@ -2,15 +2,12 @@ // QDButtonViewController.m // qmui // -// Created by ZhoonChen on 14/11/6. +// Created by QMUI Team on 14/11/6. // Copyright (c) 2014年 QMUI Team. All rights reserved. // #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.h b/qmuidemo/Modules/Demos/UIKit/QDCAAnimationViewController.h new file mode 100644 index 00000000..3ffab1b7 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDCAAnimationViewController.h @@ -0,0 +1,13 @@ +// +// QDCAAnimationViewController.h +// qmuidemo +// +// Created by QMUI Team on 2018/7/31. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +@interface QDCAAnimationViewController : QDCommonViewController + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDCAAnimationViewController.m b/qmuidemo/Modules/Demos/UIKit/QDCAAnimationViewController.m new file mode 100644 index 00000000..4adbe4f5 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDCAAnimationViewController.m @@ -0,0 +1,77 @@ +// +// QDCAAnimationViewController.m +// qmuidemo +// +// Created by QMUI Team on 2018/7/31. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import "QDCAAnimationViewController.h" + +@interface QDCAAnimationViewController () + +@property(nonatomic, strong) CALayer *layer; +@property(nonatomic, strong) QMUIButton *actionButton; +@property(nonatomic, strong) UILabel *tipsLabel; +@end + +@implementation QDCAAnimationViewController + +- (void)initSubviews { + [super initSubviews]; + + self.actionButton = [QDUIHelper generateLightBorderedButton]; + [self.actionButton setTitle:@"点击开始动画" forState:UIControlStateNormal]; + [self.actionButton addTarget:self action:@selector(handleActionButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.actionButton]; + + 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: UIColor.qd_descriptionTextColor, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:20]}]; + NSDictionary *codeAttributes = CodeAttributes(12); + [tips.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { + [tips addAttributes:codeAttributes range:codeRange]; + }]; + self.tipsLabel.attributedText = tips; + [self.view addSubview:self.tipsLabel]; + + self.layer = [CALayer layer]; + [self.layer qmui_removeDefaultAnimations]; + self.layer.cornerRadius = self.actionButton.layer.cornerRadius; + self.layer.backgroundColor = UIColor.qd_tintColor.CGColor; + [self.view.layer addSublayer:self.layer]; +} + +- (void)handleActionButtonEvent:(QMUIButton *)button { + if ([button.currentTitle isEqualToString:@"回到初始状态"]) { + [self.layer removeAnimationForKey:@"move"]; + [self.actionButton setTitle:@"点击开始动画" forState:UIControlStateNormal]; + return; + } + + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"]; + CATransform3D transform = CATransform3DMakeTranslation(CGRectGetWidth(self.view.bounds) - 24 - CGRectGetMaxX(self.layer.frame), 0, 0); + animation.toValue = [NSValue valueWithCATransform3D:transform]; + animation.duration = 2; + animation.fillMode = kCAFillModeForwards; + animation.removedOnCompletion = NO; + animation.qmui_animationDidStartBlock = ^(__kindof CAAnimation *aAnimation) { + button.enabled = NO; + [button setTitle:@"动画中..." forState:UIControlStateNormal]; + }; + animation.qmui_animationDidStopBlock = ^(__kindof CAAnimation *aAnimation, BOOL finished) { + button.enabled = YES; + [button setTitle:@"回到初始状态" forState:UIControlStateNormal]; + }; + [self.layer addAnimation:animation forKey:@"move"]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + 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); +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.h b/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.h index a1be479d..982ce536 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.h @@ -2,7 +2,7 @@ // QDChangeNavBarStyleViewController.h // qmuidemo // -// Created by zhoonchen on 16/9/5. +// Created by QMUI Team on 16/9/5. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -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 db53eb93..5d4efcdb 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDChangeNavBarStyleViewController.m @@ -2,7 +2,7 @@ // QDChangeNavBarStyleViewController.m // qmuidemo // -// Created by zhoonchen on 16/9/5. +// Created by QMUI Team on 16/9/5. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -11,7 +11,6 @@ @interface QDChangeNavBarStyleViewController () @property(nonatomic, assign) QDNavigationBarStyle barStyle; -@property(nonatomic, strong) QDChangeNavBarStyleViewController *viewController; @end @@ -26,40 +25,37 @@ - (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]; } -#pragma mark - QMUINavigationControllerDelegate - -- (BOOL)shouldSetStatusBarStyleLight { +- (UIStatusBarStyle)preferredStatusBarStyle { if (self.barStyle == QDNavigationBarStyleOrigin || self.barStyle == QDNavigationBarStyleDark) { - return YES; + return UIStatusBarStyleLightContent; } else { - return NO; + return UIStatusBarStyleDefault; } } -- (UIImage *)navigationBarBackgroundImage { +#pragma mark - QMUINavigationControllerDelegate + +- (UIImage *)qmui_navigationBarBackgroundImage { if (self.barStyle == QDNavigationBarStyleOrigin) { return NavBarBackgroundImage; } else if (self.barStyle == QDNavigationBarStyleLight) { @@ -71,58 +67,36 @@ - (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; - } else if (self.barStyle == QDNavigationBarStyleDark) { - return NavBarTintColor; - } else { - return NavBarTintColor; - } -} - -- (UIColor *)titleViewTintColor { - if (self.barStyle == QDNavigationBarStyleOrigin) { - return [QMUINavigationTitleView appearance].tintColor; } else if (self.barStyle == QDNavigationBarStyleLight) { return UIColorBlack; } else if (self.barStyle == QDNavigationBarStyleDark) { - return [QMUINavigationTitleView appearance].tintColor; + return UIColorWhite; } else { - return [QMUINavigationTitleView appearance].tintColor; + return NavBarTintColor; } } -#pragma mark - NavigationBarTransition - -//- (BOOL)shouldCustomNavigationBarTransitionWhenPushAppearing { -// return self.customNavBarTransition; -//} - -- (BOOL)shouldCustomNavigationBarTransitionWhenPushDisappearing { - return self.customNavBarTransition && (self.barStyle != self.viewController.barStyle); +- (UIColor *)qmui_titleViewTintColor { + return [self qmui_navigationBarTintColor]; } -//- (BOOL)shouldCustomNavigationBarTransitionWhenPopAppearing { -// return self.customNavBarTransition; -//} +#pragma mark - -- (BOOL)shouldCustomNavigationBarTransitionWhenPopDisappearing { - return self.customNavBarTransition && (self.barStyle != self.previousBarStyle); +- (NSString *)customNavigationBarTransitionKey { + // 不同的 barStyle 返回不同的 key,这样在不同 barStyle 的界面之间切换时就能使用自定义的 navigationBar 样式,会带来更好的视觉体验 + // 返回 nil 则表示当前界面没有修改过导航栏样式 + // 注意,如果你使用配置表,建议打开 AutomaticCustomNavigationBarTransitionStyle,由 QMUI 自动帮你判断是否需要使用自定义样式,这样就无需再实现 customNavigationBarTransitionKey 方法。QMUI Demo 里为了展示接口的使用,没有打开这个开关。 + return self.barStyle == QDNavigationBarStyleOrigin ? nil : [NSString qmui_stringWithNSInteger:self.barStyle]; } @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDCollectionDemoViewController.m b/qmuidemo/Modules/Demos/UIKit/QDCollectionDemoViewController.m deleted file mode 100644 index 9e872edc..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDCollectionDemoViewController.m +++ /dev/null @@ -1,128 +0,0 @@ -// -// QDCollectionDemoViewController.m -// qmuidemo -// -// Created by zhoonchen on 16/9/8. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QDCollectionDemoViewController.h" -#import "QDCollectionViewDemoCell.h" - -@interface QDCollectionDemoViewController () - -@property(nonatomic, assign) BOOL debug; -@property(nonatomic, strong) CALayer *debugLayer; - -@end - -@implementation QDCollectionDemoViewController - -- (instancetype)initWithLayoutStyle:(QMUICollectionViewPagingLayoutStyle)style { - if (self = [super initWithNibName:nil bundle:nil]) { - _collectionViewLayout = [[QMUICollectionViewPagingLayout alloc] initWithStyle:style]; - } - return self; -} - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { - return [self initWithLayoutStyle:QMUICollectionViewPagingLayoutStyleDefault]; -} - -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; - - self.titleView.userInteractionEnabled = YES; - [self.titleView addTarget:self action:@selector(handleTitleViewTouchEvent) forControlEvents:UIControlEventTouchUpInside]; - - self.navigationItem.rightBarButtonItem = [QMUINavigationButton barButtonItemWithType:QMUINavigationButtonTypeNormal title:self.debug ? @"普通模式" : @"调试模式" position:QMUINavigationButtonPositionRight target:self action:@selector(handleDebugItemEvent)]; -} - -- (void)initSubviews { - [super initSubviews]; - - _collectionView = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:self.collectionViewLayout]; - self.collectionView.backgroundColor = UIColorClear; - self.collectionView.showsHorizontalScrollIndicator = NO; - self.collectionView.delegate = self; - self.collectionView.dataSource = self; - [self.collectionView registerClass:[QDCollectionViewDemoCell class] forCellWithReuseIdentifier:@"cell"]; - [self.view addSubview:self.collectionView]; - - self.collectionViewLayout.sectionInset = [self sectionInset]; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - if (!CGSizeEqualToSize(self.collectionView.bounds.size, self.view.bounds.size)) { - self.collectionView.frame = self.view.bounds; - self.collectionViewLayout.sectionInset = [self sectionInset]; - [self.collectionViewLayout invalidateLayout]; - } - - if (self.debugLayer) { - self.debugLayer.frame = CGRectMake(self.view.center.x, 0, PixelOne, CGRectGetHeight(self.view.bounds)); - } -} - -- (void)handleTitleViewTouchEvent { - [self.collectionView qmui_scrollToTopAnimated:YES]; -} - -- (void)handleDebugItemEvent { - self.debug = !self.debug; - - self.collectionViewLayout.sectionInset = [self sectionInset]; - [self.collectionViewLayout invalidateLayout]; - [self.collectionView qmui_scrollToTopAnimated:YES]; - - if (self.debug) { - self.debugLayer = [CALayer layer]; - [self.debugLayer qmui_removeDefaultAnimations]; - self.debugLayer.backgroundColor = UIColorRed.CGColor; - [self.view.layer addSublayer:self.debugLayer]; - }else { - [self.debugLayer removeFromSuperlayer]; - self.debugLayer = nil; - } - - [self setNavigationItemsIsInEditMode:NO animated:NO]; -} - -- (UIEdgeInsets)sectionInset { - if (self.debug) { - CGSize itemSize = CGSizeMake(100, 100); - CGFloat horizontalInset = (CGRectGetWidth(self.collectionView.bounds) - itemSize.width) / 2; - CGFloat verticalInset = (CGRectGetHeight(self.collectionView.bounds) - UIEdgeInsetsGetVerticalValue(self.collectionView.contentInset) - itemSize.height) / 2; - return UIEdgeInsetsMake(verticalInset, horizontalInset, verticalInset, horizontalInset); - } else { - return UIEdgeInsetsMake(36, 36, 36, 36); - } -} - -#pragma mark - - -- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { - return 1; -} - -- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - return 20; -} - -- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - QDCollectionViewDemoCell *cell = (QDCollectionViewDemoCell *)[self.collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; - cell.contentLabel.text = [NSString qmui_stringWithNSInteger:indexPath.item]; - cell.backgroundColor = [QDCommonUI randomThemeColor]; - [cell setNeedsLayout]; - return cell; -} - -#pragma mark - - -- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { - CGSize size = CGSizeMake(CGRectGetWidth(collectionView.bounds) - UIEdgeInsetsGetHorizontalValue(self.collectionViewLayout.sectionInset), CGRectGetHeight(collectionView.bounds) - UIEdgeInsetsGetVerticalValue(self.collectionViewLayout.sectionInset) - CGRectGetMaxY(self.navigationController.navigationBar.frame)); - return size; -} - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDCollectionListViewController.h b/qmuidemo/Modules/Demos/UIKit/QDCollectionListViewController.h deleted file mode 100644 index dee962a7..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDCollectionListViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDCollectionListViewController.h -// qmuidemo -// -// Created by ZhoonChen on 15/9/24. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import "QDCommonListViewController.h" - -@interface QDCollectionListViewController : QDCommonListViewController - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDCollectionListViewController.m b/qmuidemo/Modules/Demos/UIKit/QDCollectionListViewController.m deleted file mode 100644 index e73d444d..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDCollectionListViewController.m +++ /dev/null @@ -1,48 +0,0 @@ -// -// QDCollectionListViewController.m -// qmuidemo -// -// Created by ZhoonChen 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/UIKit/QDCollectionStackDemoViewController.m b/qmuidemo/Modules/Demos/UIKit/QDCollectionStackDemoViewController.m deleted file mode 100644 index e2ceb780..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDCollectionStackDemoViewController.m +++ /dev/null @@ -1,125 +0,0 @@ -// -// QDCollectionStackDemoViewController.m -// qmuidemo -// -// Created by ZhoonChen on 15/10/6. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import "QDCollectionStackDemoViewController.h" -#import "QDFoldCollectionViewLayout.h" -#import "QDCollectionViewDemoCell.h" - -@interface QDCollectionStackDemoViewController () - -@end - -@implementation QDCollectionStackDemoViewController { - UIPanGestureRecognizer *_panGesture; - UICollectionView *_collectionView; - QDFoldCollectionViewLayout *_collectionViewLayout; - NSMutableArray *_datas; -} - -- (void)viewDidLoad { - [super viewDidLoad]; - _datas = [[NSMutableArray alloc] initWithObjects:@"0", @"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"10", nil]; -} - -- (void)initSubviews { - [super initSubviews]; - _collectionViewLayout = [[QDFoldCollectionViewLayout alloc] init]; - _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_collectionViewLayout]; - _collectionView.backgroundColor = UIColorClear; // 不设置貌似会变黑色 - _collectionView.showsHorizontalScrollIndicator = NO; // 隐藏滚动条 - _collectionView.delegate = self; - _collectionView.dataSource = self; - [_collectionView registerClass:[QDCollectionViewDemoCell class] forCellWithReuseIdentifier:@"cell"]; - [self.view addSubview:_collectionView]; - // 初始化收拾 - [self initGesture]; -} - -- (void)initGesture { - if (!_panGesture) { - _panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)]; - _panGesture.delegate = self; - [_collectionView addGestureRecognizer:_panGesture]; - } -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - _collectionView.frame = self.view.bounds; -} - -- (void)didReceiveMemoryWarning { - [super didReceiveMemoryWarning]; -} - -// delegate - -- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { - return 1; -} - -- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - return _datas.count; -} - -- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - QDCollectionViewDemoCell *cell = (QDCollectionViewDemoCell *)[_collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; - cell.contentLabel.text = [NSString stringWithFormat:@"%@", [_datas objectAtIndex:indexPath.item]]; - [cell setNeedsLayout]; - return cell; -} - -// gesture - -- (void)handlePanGesture:(UIPanGestureRecognizer *)gesture { - if (gesture.state == UIGestureRecognizerStateBegan) { - NSLog(@"gesture begin"); - // CGPoint point = [gesture locationInView:_collectionView]; - // NSIndexPath *indexPath = [_collectionView indexPathForItemAtPoint:point]; - NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; - if (indexPath) { - _collectionViewLayout.curIndexPath = indexPath; - _collectionViewLayout.isMoving = YES; - } - } - else if (gesture.state == UIGestureRecognizerStateChanged) { - NSLog(@"gesture chagned"); - CGPoint point = [gesture translationInView:_collectionView]; - _collectionViewLayout.curPoint = point; - [_collectionViewLayout invalidateLayout]; - } - else if (gesture.state == UIGestureRecognizerStateCancelled || gesture.state == UIGestureRecognizerStateEnded) { - NSLog(@"gesture canceled or ended"); - CGFloat maxDistance = fmax(fabs(_collectionViewLayout.curPoint.x), fabs(_collectionViewLayout.curPoint.y)); - _collectionViewLayout.isMoving = NO; - _collectionViewLayout.curIndexPath = nil; - if (maxDistance > 80) { - NSIndexPath *deleteIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; - [_datas removeObjectAtIndex:0]; - [UIView animateWithDuration:.5 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ - QDCollectionViewDemoCell *cell = (QDCollectionViewDemoCell *)[_collectionView cellForItemAtIndexPath:deleteIndexPath]; - cell.layer.transform = CATransform3DMakeTranslation(_collectionViewLayout.curPoint.x * 10, _collectionViewLayout.curPoint.y * 10, 0); - cell.alpha = 0; - } completion:^(BOOL finished) { - [_collectionView performBatchUpdates:^{ - [_collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:deleteIndexPath]]; - } completion:^(BOOL finished) { - _collectionViewLayout.curPoint = CGPointZero; - }]; - }]; - } else { - [_collectionView performBatchUpdates:^{ - [_collectionView reloadData]; - } completion:^(BOOL finished) { - _collectionViewLayout.curPoint = CGPointZero; - }]; - } - } -} - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDCollectionViewDemoCell.h b/qmuidemo/Modules/Demos/UIKit/QDCollectionViewDemoCell.h deleted file mode 100644 index 4dcdd09c..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDCollectionViewDemoCell.h +++ /dev/null @@ -1,14 +0,0 @@ -// -// QDCollectionViewDemoCell.h -// qmuidemo -// -// Created by ZhoonChen on 15/9/24. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import - -@interface QDCollectionViewDemoCell : UICollectionViewCell - -@property(nonatomic, strong, readonly) UILabel *contentLabel; -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDCollectionViewDemoCell.m b/qmuidemo/Modules/Demos/UIKit/QDCollectionViewDemoCell.m deleted file mode 100644 index 96e46b4e..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDCollectionViewDemoCell.m +++ /dev/null @@ -1,31 +0,0 @@ -// -// QDCollectionViewDemoCell.m -// qmuidemo -// -// Created by ZhoonChen on 15/9/24. -// Copyright © 2015年 QMUI Team. All rights reserved. -// - -#import "QDCollectionViewDemoCell.h" - -@implementation QDCollectionViewDemoCell - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - self.layer.cornerRadius = 3; - - _contentLabel = [[UILabel alloc] initWithFont:UIFontLightMake(100) textColor:UIColorWhite]; - self.contentLabel.textAlignment = NSTextAlignmentCenter; - [self.contentView addSubview:self.contentLabel]; - } - return self; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - [self.contentLabel sizeToFit]; - self.contentLabel.center = CGPointMake(CGRectGetWidth(self.contentView.bounds) / 2, CGRectGetHeight(self.contentView.bounds) / 2); -} - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDColorViewController.h b/qmuidemo/Modules/Demos/UIKit/QDColorViewController.h index df733801..785a0d21 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDColorViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDColorViewController.h @@ -2,7 +2,7 @@ // QDColorViewController.h // qmui // -// Created by MoLice on 14-7-13. +// Created by QMUI Team on 14-7-13. // Copyright (c) 2014年 QMUI Team. All rights reserved. // @@ -20,8 +20,6 @@ @property(nonatomic, assign) UIEdgeInsets contentViewInsets; // default to (0, 0, 0, 0) @property(nonatomic, assign) CGFloat titleLabelMarginBottom; // default to 12 -- (void)initSubviews; - - (UIView *)generateCircleWithColor:(UIColor *)color; - (UIImageView *)generateArrowIcon; - (UIImageView *)generatePlusIcon; @@ -52,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 7c847027..dbddf0e6 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDColorViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDColorViewController.m @@ -2,7 +2,7 @@ // QDColorViewController.m // qmui // -// Created by MoLice on 14-7-13. +// Created by QMUI Team on 14-7-13. // Copyright (c) 2014年 QMUI Team. All rights reserved. // @@ -18,30 +18,18 @@ @implementation QDColorViewController #pragma mark - 生命周期函数 -- (void)initSubviews { - [super initSubviews]; +- (void)initTableView { + [super initTableView]; self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; - self.tableView.contentInset = UIEdgeInsetsMake(32, 0, 32, 0); -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; -} - -#pragma mark - 工具方法 - -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; -} - -- (void)setToolbarItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setToolbarItemsIsInEditMode:isInEditMode animated:animated]; + + CGFloat topInset = 32; + self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.tableView.bounds), topInset)]; } #pragma mark - TableView Delegate & DataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return 7; + return 8; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { @@ -75,6 +63,8 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N case 6: cell = [[QDColorCellThatAdjustAlphaAndBlend alloc] init]; break; + case 7: + cell = [[QDColorCellThatGetDistance alloc] init]; default: break; } @@ -84,28 +74,18 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N return cell; } - - @end @implementation QDColorTableViewCell -- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { - if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { - self.titleLabelMarginBottom = 12; - - [self initSubviews]; - } - return self; -} - -- (void)initSubviews { +- (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.text = @"测"; - [self.titleLabel sizeToFit]; - self.titleLabel.text = @""; + [self.titleLabel qmui_calculateHeightAfterSetAppearance]; [self.contentView addSubview:self.titleLabel]; } @@ -121,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; @@ -132,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")]; @@ -151,8 +140,8 @@ @implementation QDColorCellThatGenerateFromHex { QMUILabel *_label; } -- (void)initSubviews { - [super initSubviews]; +- (void)didInitializeWithStyle:(UITableViewCellStyle)style { + [super didInitializeWithStyle:style]; self.titleLabel.text = @"通过HEX创建"; UIColor *resultColor = [UIColor qmui_colorWithHexString:@"#cddc39"]; // 关键方法 @@ -164,7 +153,7 @@ - (void)initSubviews { _label.text = @"[UIColor qmui_colorWithHexString:@\"#cddc39\"]"; _label.font = UIFontMake(12); [_label sizeToFit]; - _label.textColor = UIColorGray7; + _label.textColor = UIColor.qd_descriptionTextColor; [self.contentView addSubview:_label]; } @@ -184,8 +173,9 @@ @implementation QDColorCellThatGetColorInfo { NSMutableArray *_labels; } -- (void)initSubviews { - [super initSubviews]; +- (void)didInitializeWithStyle:(UITableViewCellStyle)style { + [super didInitializeWithStyle:style]; + self.titleLabel.text = @"获取颜色信息"; _labels = [NSMutableArray array]; @@ -223,7 +213,7 @@ - (void)initSubviews { 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]; @@ -232,7 +222,7 @@ - (void)initSubviews { 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,8 +257,8 @@ @implementation QDColorCellThatResetAlpha { QMUILabel *_label2; } -- (void)initSubviews { - [super initSubviews]; +- (void)didInitializeWithStyle:(UITableViewCellStyle)style { + [super didInitializeWithStyle:style]; self.titleLabel.text = @"去除alpha通道"; @@ -287,7 +277,7 @@ - (void)initSubviews { _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]; @@ -317,8 +307,8 @@ @implementation QDColorCellThatInverseColor { UIView *_arrow; } -- (void)initSubviews { - [super initSubviews]; +- (void)didInitializeWithStyle:(UITableViewCellStyle)style { + [super didInitializeWithStyle:style]; self.titleLabel.text = @"计算反色"; @@ -354,8 +344,8 @@ @implementation QDColorCellThatNeutralizeColors { UIView *_arrow; } -- (void)initSubviews { - [super initSubviews]; +- (void)didInitializeWithStyle:(UITableViewCellStyle)style { + [super didInitializeWithStyle:style]; self.titleLabel.text = @"计算过渡色"; @@ -399,8 +389,8 @@ @implementation QDColorCellThatBlendColors { UIView *_arrow; } -- (void)initSubviews { - [super initSubviews]; +- (void)didInitializeWithStyle:(UITableViewCellStyle)style { + [super didInitializeWithStyle:style]; self.titleLabel.text = @"计算叠加色"; @@ -431,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; @@ -441,8 +510,8 @@ @implementation QDColorCellThatAdjustAlphaAndBlend { QMUILabel *_label; } -- (void)initSubviews { - [super initSubviews]; +- (void)didInitializeWithStyle:(UITableViewCellStyle)style { + [super didInitializeWithStyle:style]; self.titleLabel.text = @"先更改alpha,再与另一个颜色叠加"; @@ -458,7 +527,7 @@ - (void)initSubviews { _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 738230d7..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDFillButtonViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDFillButtonViewController.h -// qmuidemo -// -// Created by ZhoonChen 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 a4fa7b2a..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDFillButtonViewController.m +++ /dev/null @@ -1,83 +0,0 @@ -// -// QDFillButtonViewController.m -// qmuidemo -// -// Created by ZhoonChen 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 = CGRectGetMaxY(self.navigationController.navigationBar.frame); - 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.h b/qmuidemo/Modules/Demos/UIKit/QDFoldCollectionViewLayout.h index 40265210..f76e87b2 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDFoldCollectionViewLayout.h +++ b/qmuidemo/Modules/Demos/UIKit/QDFoldCollectionViewLayout.h @@ -2,7 +2,7 @@ // QDFoldCollectionViewLayout.h // qmuidemo // -// Created by ZhoonChen on 15/10/6. +// Created by QMUI Team on 15/10/6. // Copyright © 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDFoldCollectionViewLayout.m b/qmuidemo/Modules/Demos/UIKit/QDFoldCollectionViewLayout.m index 0d349adb..d1f74458 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDFoldCollectionViewLayout.m +++ b/qmuidemo/Modules/Demos/UIKit/QDFoldCollectionViewLayout.m @@ -2,7 +2,7 @@ // QDFoldCollectionViewLayout.m // qmuidemo // -// Created by ZhoonChen on 15/10/6. +// Created by QMUI Team on 15/10/6. // Copyright © 2015年 QMUI Team. All rights reserved. // @@ -10,7 +10,7 @@ @interface QDFoldCollectionViewLayout () -@property (nonatomic, strong) NSMutableArray *deleteIndexPaths; +@property(nonatomic, strong) NSMutableArray *deleteIndexPaths; @end @@ -32,7 +32,7 @@ - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { } - (CGSize)collectionViewContentSize { - return CGSizeMake(self.collectionView.frame.size.width, self.collectionView.frame.size.height - NavigationContentTop); + 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.h b/qmuidemo/Modules/Demos/UIKit/QDFontViewController.h index c5503c69..e371e8e6 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDFontViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDFontViewController.h @@ -2,7 +2,7 @@ // QDFontViewController.h // qmuidemo // -// Created by MoLice on 2017/5/29. +// Created by QMUI Team on 2017/5/29. // Copyright © 2017年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDFontViewController.m b/qmuidemo/Modules/Demos/UIKit/QDFontViewController.m index 7af71d3c..09db2661 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDFontViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDFontViewController.m @@ -2,7 +2,7 @@ // QDFontViewController.m // qmuidemo // -// Created by MoLice on 2017/5/29. +// Created by QMUI Team on 2017/5/29. // Copyright © 2017年 QMUI Team. All rights reserved. // @@ -13,21 +13,28 @@ @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]; } -- (void)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; +- (void)setupNavigationItems { + [super setupNavigationItems]; self.title = @"UIFont+QMUI"; } @@ -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 ffcd769b..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDGhostButtonViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDGhostButtonViewController.h -// qmuidemo -// -// Created by ZhoonChen 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 f3783cd1..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDGhostButtonViewController.m +++ /dev/null @@ -1,77 +0,0 @@ -// -// QDGhostButtonViewController.m -// qmuidemo -// -// Created by ZhoonChen 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 = CGRectGetMaxY(self.navigationController.navigationBar.frame); - 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.h b/qmuidemo/Modules/Demos/UIKit/QDImageViewController.h index 320ae4b2..9f0af585 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDImageViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDImageViewController.h @@ -2,7 +2,7 @@ // QDImageViewController.h // qmui // -// Created by MoLice on 14-7-13. +// Created by QMUI Team on 14-7-13. // Copyright (c) 2014年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDImageViewController.m b/qmuidemo/Modules/Demos/UIKit/QDImageViewController.m index 22e86b36..f0a562a9 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDImageViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDImageViewController.m @@ -2,7 +2,7 @@ // QDImageViewController.m // qmui // -// Created by MoLice on 14-7-13. +// Created by QMUI Team on 14-7-13. // Copyright (c) 2014年 QMUI Team. All rights reserved. // @@ -11,19 +11,12 @@ @interface QDImageViewController () @property(nonatomic, strong) UIView *contentView; -@property(nonatomic, strong) UIScrollView *contentScrollView; +@property(nonatomic, strong) UIScrollView *scrollView; @property(nonatomic, strong) UILabel *methodNameLabel; @end @implementation QDImageViewController -- (instancetype)initWithStyle:(UITableViewStyle)style { - if (self = [super initWithStyle:style]) { - self.tableViewInitialContentInset = UIEdgeInsetsMake(NavigationContentStaticTop, 0, 0, 0); - } - return self; -} - - (void)initDataSource { self.dataSourceWithDetailText = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: @"- qmui_averageColor", @"获取整张图片的平均颜色", @@ -34,8 +27,8 @@ - (void)initDataSource { @"- qmui_imageWithImageAbove:atPoint:", @"将一张图片叠在当前图片上方的指定位置", @"- qmui_imageWithSpacingExtensionInsets:", @"拓展当前图片外部边距,拓展的区域填充透明", @"- qmui_imageWithClippedRect:", @"将图片内指定区域的矩形裁剪出来,返回裁剪出来的区域", - @"- qmui_imageWithScaleToSize:contentMode:", @"将当前图片缩放到指定的大小,缩放策略可以指定不同的contentMode,经过缩放后的图片倍数保持不变", - @"- qmui_imageWithScaleToSize:contentMode:scale:", @"同上,只是可以指定倍数", + @"- qmui_imageWithClippedCornerRadius:", @"将图片按指定圆角裁剪出来,返回裁剪出来的区域", + @"- qmui_imageResizedInLimitedSize:resizingMode:", @"将当前图片缩放到指定的大小,缩放策略可以指定不同的resizingMode,经过缩放后的图片倍数保持不变", @"- qmui_imageWithOrientation:", @"将图片旋转到指定方向,支持上下左右、水平&垂直翻转", @"- qmui_imageWithBorderColor:path:", @"在当前图片上叠加绘制一条路径", @"- qmui_imageWithBorderColor:borderWidth:cornerRadius:", @"在当前图片上加上一条外边框,可指定边框大小和圆角", @@ -45,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:", @"生成一张指定大小的矩形图片,允许在各个方向选择添加边框", @@ -58,33 +52,34 @@ - (void)initDataSource { - (void)initSubviews { [super initSubviews]; - CGFloat maximumContentViewWidth = fminf(CGRectGetWidth(self.view.bounds), [QMUIHelper screenSizeFor47Inch].width) - 20 * 2; + 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.contentScrollView = [[UIScrollView alloc] initWithFrame:self.contentView.bounds]; - self.contentScrollView.contentInset = UIEdgeInsetsMake(20, 20, 20, 20); - self.contentScrollView.alwaysBounceHorizontal = NO; - self.contentScrollView.alwaysBounceVertical = NO; - self.contentScrollView.scrollsToTop = NO; - [self.contentView addSubview:self.contentScrollView]; + self.scrollView = [[UIScrollView alloc] initWithFrame:self.contentView.bounds]; + self.scrollView.contentInset = UIEdgeInsetsMake(20, 20, 20, 20); + self.scrollView.alwaysBounceHorizontal = NO; + self.scrollView.alwaysBounceVertical = NO; + self.scrollView.scrollsToTop = NO; + self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + [self.contentView addSubview:self.scrollView]; - self.methodNameLabel = [[UILabel alloc] 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.contentScrollView addSubview:self.methodNameLabel]; + [self.scrollView addSubview:self.methodNameLabel]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - self.contentScrollView.frame = self.contentView.bounds; + self.scrollView.frame = self.contentView.bounds; } - (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); @@ -95,8 +90,7 @@ - (void)didSelectCellWithTitle:(NSString *)title { CGFloat contentViewLimitWidth = [self contentViewLimitWidth]; self.methodNameLabel.text = title; - CGSize methodLabelSize = [self.methodNameLabel sizeThatFits:CGSizeMake(contentViewLimitWidth, CGFLOAT_MAX)]; - self.methodNameLabel.frame = CGRectMake(0, 0, contentViewLimitWidth, methodLabelSize.height); + self.methodNameLabel.frame = CGRectMake(0, 0, contentViewLimitWidth, QMUIViewSelfSizingHeight); CGFloat contentSizeHeight = 0; if ([title isEqualToString:@"- qmui_averageColor"]) { @@ -113,10 +107,12 @@ - (void)didSelectCellWithTitle:(NSString *)title { contentSizeHeight = [self generateExampleViewForImageWithImageAbove]; } else if ([title isEqualToString:@"- qmui_imageWithSpacingExtensionInsets:"]) { contentSizeHeight = [self generateExampleViewForImageWithSpacingExtensionInsets]; + } else if ([title isEqualToString:@"- qmui_imageWithClippedCornerRadius:"]) { + contentSizeHeight = [self generateExampleViewForImageWithClippedCornerRadius]; } else if ([title isEqualToString:@"- qmui_imageWithClippedRect:"]) { contentSizeHeight = [self generateExampleViewForImageWithClippedRect]; - } else if ([title isEqualToString:@"- qmui_imageWithScaleToSize:contentMode:"] || [title isEqualToString:@"- qmui_imageWithScaleToSize:contentMode:scale:"]) { - contentSizeHeight = [self generateExampleViewForImageWithScaleToSize]; + } else if ([title isEqualToString:@"- qmui_imageResizedInLimitedSize:resizingMode:"]) { + contentSizeHeight = [self generateExampleViewForResizedImage]; } else if ([title isEqualToString:@"- qmui_imageWithOrientation:"]) { contentSizeHeight = [self generateExampleViewForImageWithDirection]; } else if ([title isEqualToString:@"- qmui_imageWithBorderColor:path:"]) { @@ -145,20 +141,21 @@ - (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.contentScrollView.contentSize = CGSizeMake(contentViewLimitWidth, contentSizeHeight); + self.scrollView.contentSize = CGSizeMake(contentViewLimitWidth, contentSizeHeight); - CGFloat contentViewPreferHeight = UIEdgeInsetsGetVerticalValue(self.contentScrollView.contentInset) + self.contentScrollView.contentSize.height; + CGFloat contentViewPreferHeight = UIEdgeInsetsGetVerticalValue(self.scrollView.contentInset) + self.scrollView.contentSize.height; CGFloat contentViewLimitHeight = CGRectGetHeight(self.view.bounds) - 40 * 2; - self.contentView.frame = CGRectSetHeight(self.contentView.frame, fminf(contentViewLimitHeight, contentViewPreferHeight)); - self.contentScrollView.frame = self.contentView.bounds; + self.contentView.frame = CGRectSetHeight(self.contentView.frame, fmin(contentViewLimitHeight, contentViewPreferHeight)); + self.scrollView.frame = self.contentView.bounds; __weak QDImageViewController *weakSelf = self; QMUIModalPresentationViewController *modalPresentationViewController = [[QMUIModalPresentationViewController alloc] init]; - modalPresentationViewController.maximumContentViewWidth = CGFLOAT_MAX; modalPresentationViewController.contentView = self.contentView; modalPresentationViewController.didHideByDimmingViewTappedBlock = ^{ - for (UIView *subview in weakSelf.contentScrollView.subviews) { + for (UIView *subview in weakSelf.scrollView.subviews) { if (subview != weakSelf.methodNameLabel) { [subview removeFromSuperview]; } @@ -170,7 +167,7 @@ - (void)didSelectCellWithTitle:(NSString *)title { } - (CGFloat)contentViewLimitWidth { - return CGRectGetWidth(self.contentScrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.contentScrollView.contentInset); + return CGRectGetWidth(self.scrollView.bounds) - UIEdgeInsetsGetHorizontalValue(self.scrollView.contentInset); } - (CGFloat)contentViewLayoutStartingMinY { @@ -183,17 +180,17 @@ - (CGFloat)generateExampleViewForAverageColor { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"image0")]; originImageView.contentMode = UIViewContentModeScaleAspectFit; originImageView.frame = CGRectMake(0, minY, contentWidth, flat(contentWidth * originImageView.image.size.height / originImageView.image.size.width)); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UIColor *qmui_averageColor = [originImageView.image qmui_averageColor]; @@ -203,12 +200,12 @@ - (CGFloat)generateExampleViewForAverageColor { afterLabel.text = [NSString stringWithFormat:@"计算出的平均色:RGB(%d, %d, %d)", (int)(qmui_averageColor.qmui_red * 255), (int)(qmui_averageColor.qmui_green * 255), (int)(qmui_averageColor.qmui_blue * 255)]; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIView *averageColorView = [[UIView alloc] initWithFrame:CGRectMake(0, minY, contentWidth, 100)]; averageColorView.backgroundColor = [originImageView.image qmui_averageColor]; - [self.contentScrollView addSubview:averageColorView]; + [self.scrollView addSubview:averageColorView]; minY = CGRectGetMaxY(averageColorView.frame); return minY; @@ -218,18 +215,18 @@ - (CGFloat)generateExampleViewForGrayImage { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"image0")]; originImageView.contentMode = UIViewContentModeScaleAspectFit; [originImageView qmui_sizeToFitKeepingImageAspectRatioInSize:CGSizeMake(contentWidth, CGFLOAT_MAX)]; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -237,13 +234,13 @@ - (CGFloat)generateExampleViewForGrayImage { afterLabel.text = @"置灰后的图片"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIImageView *afterImageView = [[UIImageView alloc] initWithFrame:CGRectSetY(originImageView.frame, minY)]; afterImageView.contentMode = originImageView.contentMode; afterImageView.image = [originImageView.image qmui_grayImage]; - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame); return minY; @@ -253,18 +250,18 @@ - (CGFloat)generateExampleViewForImageWithAlpha { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"image0")]; originImageView.contentMode = UIViewContentModeScaleAspectFit; [originImageView qmui_sizeToFitKeepingImageAspectRatioInSize:CGSizeMake(contentWidth, CGFLOAT_MAX)]; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -272,7 +269,7 @@ - (CGFloat)generateExampleViewForImageWithAlpha { afterLabel.text = @"叠加0.5的apha之后"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIImage *imageAddedAlpha = [originImageView.image qmui_imageWithAlpha:.5]; @@ -282,12 +279,12 @@ - (CGFloat)generateExampleViewForImageWithAlpha { UIImageView *afterImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, minY, imageViewSize.width, imageViewSize.height)]; afterImageView.contentMode = originImageView.contentMode; afterImageView.image = imageAddedAlpha; - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; UIImageView *afterImageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(20, CGRectGetMinY(afterImageView.frame) + 20, imageViewSize.width, imageViewSize.height)]; afterImageView2.contentMode = afterImageView.contentMode; afterImageView2.image = imageAddedAlpha; - [self.contentScrollView addSubview:afterImageView2]; + [self.scrollView addSubview:afterImageView2]; minY = CGRectGetMaxY(afterImageView2.frame); @@ -297,17 +294,17 @@ - (CGFloat)generateExampleViewForImageWithAlpha { - (CGFloat)generateExampleViewForImageWithTintColor { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"icon_emotion")]; originImageView.contentMode = UIViewContentModeScaleAspectFit; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -315,14 +312,14 @@ - (CGFloat)generateExampleViewForImageWithTintColor { afterLabel.text = @"将图片换个颜色"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIImage *afterImage = [originImageView.image qmui_imageWithTintColor:[QDCommonUI randomThemeColor]]; UIImageView *afterImageView = [[UIImageView alloc] initWithImage:afterImage]; afterImageView.contentMode = originImageView.contentMode; afterImageView.frame = CGRectSetY(afterImageView.frame, minY); - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame); return minY; @@ -332,17 +329,17 @@ - (CGFloat)generateExampleViewForImageWithBlendColor { CGFloat minY = [self contentViewLayoutStartingMinY]; CGFloat contentWidth = [self contentViewLimitWidth]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; - UIImageView *originImageView = [[UIImageView alloc] initWithImage:[UIImageMake(@"image0") qmui_imageWithScaleToSize:CGSizeMake(contentWidth, contentWidth)]]; + UIImageView *originImageView = [[UIImageView alloc] initWithImage:[UIImageMake(@"image0") qmui_imageResizedInLimitedSize:CGSizeMake(contentWidth, contentWidth)]]; originImageView.contentMode = UIViewContentModeScaleAspectFit; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -350,14 +347,14 @@ - (CGFloat)generateExampleViewForImageWithBlendColor { afterLabel.text = @"将图片换个颜色"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIImage *afterImage = [originImageView.image qmui_imageWithBlendColor:[QDCommonUI randomThemeColor]]; UIImageView *afterImageView = [[UIImageView alloc] initWithImage:afterImage]; afterImageView.contentMode = originImageView.contentMode; afterImageView.frame = CGRectSetY(afterImageView.frame, minY); - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame); return minY; @@ -366,17 +363,17 @@ - (CGFloat)generateExampleViewForImageWithBlendColor { - (CGFloat)generateExampleViewForImageWithImageAbove { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [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.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -384,7 +381,7 @@ - (CGFloat)generateExampleViewForImageWithImageAbove { afterLabel.text = @"在图片上叠加一张未读红点的图片"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIImage *redDotImage = [UIImage qmui_imageWithColor:UIColorRed size:CGSizeMake(6, 6) cornerRadius:6.0 / 2.0]; @@ -392,7 +389,7 @@ - (CGFloat)generateExampleViewForImageWithImageAbove { UIImageView *afterImageView = [[UIImageView alloc] initWithImage:afterImage]; afterImageView.contentMode = originImageView.contentMode; afterImageView.frame = CGRectSetY(afterImageView.frame, minY); - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame); return minY; @@ -401,18 +398,18 @@ - (CGFloat)generateExampleViewForImageWithImageAbove { - (CGFloat)generateExampleViewForImageWithSpacingExtensionInsets { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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); - [self.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"icon_emotion")]; originImageView.layer.borderWidth = PixelOne; originImageView.layer.borderColor = [QDCommonUI randomThemeColor].CGColor; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -420,7 +417,7 @@ - (CGFloat)generateExampleViewForImageWithSpacingExtensionInsets { afterLabel.text = @"在图片右边加了padding之后"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIImage *afterImage = [originImageView.image qmui_imageWithSpacingExtensionInsets:UIEdgeInsetsMake(0, 0, 0, 10)]; @@ -428,7 +425,47 @@ - (CGFloat)generateExampleViewForImageWithSpacingExtensionInsets { afterImageView.layer.borderWidth = originImageView.layer.borderWidth; afterImageView.layer.borderColor = originImageView.layer.borderColor; afterImageView.frame = CGRectSetY(afterImageView.frame, minY); - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; + minY = CGRectGetMaxY(afterImageView.frame); + + return minY; +} + +- (CGFloat)generateExampleViewForImageWithClippedCornerRadius { + CGFloat contentWidth = [self contentViewLimitWidth]; + 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 = UIImageMake(@"image1"); + originImage = [originImage qmui_imageWithClippedRect:CGRectFlatMake(CGFloatGetCenter(originImage.size.width, 200), CGFloatGetCenter(originImage.size.height, 200), 200, 200)]; + UIImageView *originImageView = [[UIImageView alloc] initWithImage:originImage]; + originImageView.contentMode = UIViewContentModeScaleAspectFit; + [originImageView qmui_sizeToFitKeepingImageAspectRatioInSize:CGSizeMake(contentWidth, CGFLOAT_MAX)]; + originImageView.frame = CGRectFlatMake(0, minY, originImage.size.width, originImage.size.height); + originImageView.clipsToBounds = YES; + [self.scrollView addSubview:originImageView]; + minY = CGRectGetMaxY(originImageView.frame) + 16; + + UILabel *afterLabel = [[UILabel alloc] init]; + [afterLabel qmui_setTheSameAppearanceAsLabel:originImageLabel]; + afterLabel.text = @"按宽度一半作为圆角裁剪"; + [afterLabel sizeToFit]; + afterLabel.frame = CGRectSetY(afterLabel.frame, minY); + [self.scrollView addSubview:afterLabel]; + minY = CGRectGetMaxY(afterLabel.frame) + 6; + + CGFloat cornerRadius = CGRectGetWidth(originImageView.bounds) / 2; + UIImage *afterImage = [originImageView.image qmui_imageWithClippedCornerRadius:cornerRadius]; + UIImageView *afterImageView = [[UIImageView alloc] initWithImage:afterImage]; + afterImageView.contentMode = originImageView.contentMode; + afterImageView.frame = CGRectSetY(afterImageView.frame, minY); + [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame); return minY; @@ -438,11 +475,11 @@ - (CGFloat)generateExampleViewForImageWithClippedRect { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"image0")]; @@ -450,7 +487,7 @@ - (CGFloat)generateExampleViewForImageWithClippedRect { [originImageView qmui_sizeToFitKeepingImageAspectRatioInSize:CGSizeMake(contentWidth, CGFLOAT_MAX)]; originImageView.frame = CGRectSetY(originImageView.frame, minY); originImageView.clipsToBounds = YES; - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -458,28 +495,28 @@ - (CGFloat)generateExampleViewForImageWithClippedRect { afterLabel.text = @"裁剪出中间的区域"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIImage *afterImage = [originImageView.image qmui_imageWithClippedRect:CGRectMake(originImageView.image.size.width / 4, originImageView.image.size.height / 4, originImageView.image.size.width / 2, originImageView.image.size.height / 2)]; UIImageView *afterImageView = [[UIImageView alloc] initWithImage:afterImage]; afterImageView.contentMode = originImageView.contentMode; afterImageView.frame = CGRectSetY(afterImageView.frame, minY); - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame); return minY; } -- (CGFloat)generateExampleViewForImageWithScaleToSize { +- (CGFloat)generateExampleViewForResizedImage { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"image0")]; @@ -487,7 +524,7 @@ - (CGFloat)generateExampleViewForImageWithScaleToSize { [originImageView qmui_sizeToFitKeepingImageAspectRatioInSize:CGSizeMake(contentWidth, CGFLOAT_MAX)]; originImageView.frame = CGRectSetY(originImageView.frame, minY); originImageView.clipsToBounds = YES; - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -495,15 +532,16 @@ - (CGFloat)generateExampleViewForImageWithScaleToSize { afterLabel.text = @"缩小之后的图"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; - // 对原图进行缩放操作 - UIImage *afterImage = [originImageView.image qmui_imageWithScaleToSize:CGSizeMake(80, 80)]; + // 对原图进行缩放操作,以保证缩放后的图的 size 不超过 limitedSize 的大小,至于缩放策略则由 resizingMode 决定。resizingMode 默认是 QMUIImageResizingModeScaleAspectFit。 + // 特别的,对于 ScaleAspectFit 类型,你可以对不关心大小的那一边传 CGFLOAT_MAX 来表示“我不关心这一边缩放后的大小限制”,但对其他类型的 contentMode 则宽高都必须传一个确切的值。 + UIImage *afterImage = [originImageView.image qmui_imageResizedInLimitedSize:CGSizeMake(100, 50) resizingMode:QMUIImageResizingModeScaleAspectFill]; UIImageView *afterImageView = [[UIImageView alloc] initWithImage:afterImage]; afterImageView.contentMode = originImageView.contentMode; afterImageView.frame = CGRectSetY(afterImageView.frame, minY); - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame); return minY; @@ -512,18 +550,18 @@ - (CGFloat)generateExampleViewForImageWithScaleToSize { - (CGFloat)generateExampleViewForImageWithDirection { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"icon_emotion")]; originImageView.contentMode = UIViewContentModeCenter; originImageView.frame = CGRectSetY(originImageView.frame, minY); originImageView.clipsToBounds = YES; - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -531,7 +569,7 @@ - (CGFloat)generateExampleViewForImageWithDirection { afterLabel.text = @"吓得我旋转了360°图"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIImage *rightImge = [originImageView.image qmui_imageWithOrientation:UIImageOrientationRight]; @@ -542,7 +580,7 @@ - (CGFloat)generateExampleViewForImageWithDirection { afterImageView.contentMode = UIViewContentModeCenter; afterImageView.animationImages = @[originImageView.image, rightImge, bottomImage, leftImage]; afterImageView.animationDuration = 2; - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; [afterImageView startAnimating]; minY = CGRectGetMaxY(afterImageView.frame); @@ -552,16 +590,16 @@ - (CGFloat)generateExampleViewForImageWithDirection { - (CGFloat)generateExampleViewForImageWithBorder { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"icon_emotion")]; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -569,7 +607,7 @@ - (CGFloat)generateExampleViewForImageWithBorder { afterLabel.text = @"加了边框之后的图(边框路径要考虑像素对齐)"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; CGFloat lineWidth = PixelOne; @@ -579,7 +617,7 @@ - (CGFloat)generateExampleViewForImageWithBorder { UIImageView *afterImageView = [[UIImageView alloc] initWithImage:afterImage]; afterImageView.frame = CGRectSetY(afterImageView.frame, minY); - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame); return minY; @@ -588,16 +626,16 @@ - (CGFloat)generateExampleViewForImageWithBorder { - (CGFloat)generateExampleViewForImageWithBorderColorAndCornerRadiusWithDashedBorder:(BOOL)dashedBorder { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"icon_emotion")]; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -605,7 +643,7 @@ - (CGFloat)generateExampleViewForImageWithBorderColorAndCornerRadiusWithDashedBo afterLabel.text = @"加了边框之后的图"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIImage *afterImage; @@ -618,7 +656,7 @@ - (CGFloat)generateExampleViewForImageWithBorderColorAndCornerRadiusWithDashedBo UIImageView *afterImageView = [[UIImageView alloc] initWithImage:afterImage]; afterImageView.frame = CGRectSetY(afterImageView.frame, minY); - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame); return minY; @@ -627,16 +665,16 @@ - (CGFloat)generateExampleViewForImageWithBorderColorAndCornerRadiusWithDashedBo - (CGFloat)generateExampleViewForImageWithBorderColorAndCornerRadiusAndPosition { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"icon_emotion")]; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -644,14 +682,14 @@ - (CGFloat)generateExampleViewForImageWithBorderColorAndCornerRadiusAndPosition afterLabel.text = @"加了下边框之后的图"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIImage *afterImage = [originImageView.image qmui_imageWithBorderColor:[QDCommonUI randomThemeColor] borderWidth:PixelOne borderPosition:QMUIImageBorderPositionBottom]; UIImageView *afterImageView = [[UIImageView alloc] initWithImage:afterImage]; afterImageView.frame = CGRectSetY(afterImageView.frame, minY); - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame); return minY; @@ -661,11 +699,11 @@ - (CGFloat)generateExampleViewForImageWithMaskImage { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImageView *originImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"image0")]; @@ -673,14 +711,14 @@ - (CGFloat)generateExampleViewForImageWithMaskImage { [originImageView qmui_sizeToFitKeepingImageAspectRatioInSize:CGSizeMake(contentWidth, CGFLOAT_MAX)]; originImageView.frame = CGRectSetY(originImageView.frame, minY); originImageView.clipsToBounds = YES; - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; - UILabel *maskImageLabel = [[UILabel alloc] 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); - [self.contentScrollView addSubview:maskImageLabel]; + [self.scrollView addSubview:maskImageLabel]; minY = CGRectGetMaxY(maskImageLabel.frame) + 6; UIImageView *maskImageView = [[UIImageView alloc] initWithImage:UIImageMake(@"image1")]; @@ -688,7 +726,7 @@ - (CGFloat)generateExampleViewForImageWithMaskImage { [maskImageView qmui_sizeToFitKeepingImageAspectRatioInSize:CGSizeMake(contentWidth, CGFLOAT_MAX)]; maskImageView.frame = CGRectSetY(originImageView.frame, minY); maskImageView.clipsToBounds = YES; - [self.contentScrollView addSubview:maskImageView]; + [self.scrollView addSubview:maskImageView]; minY = CGRectGetMaxY(maskImageView.frame) + 16; UILabel *afterLabel = [[UILabel alloc] init]; @@ -696,20 +734,20 @@ - (CGFloat)generateExampleViewForImageWithMaskImage { afterLabel.text = @"A.加了遮罩后的图片"; [afterLabel sizeToFit]; afterLabel.frame = CGRectSetY(afterLabel.frame, minY); - [self.contentScrollView addSubview:afterLabel]; + [self.scrollView addSubview:afterLabel]; minY = CGRectGetMaxY(afterLabel.frame) + 6; UIImage *afterImage = [originImageView.image qmui_imageWithMaskImage:maskImageView.image usingMaskImageMode:YES]; UIImageView *afterImageView = [[UIImageView alloc] initWithFrame:CGRectSetY(originImageView.frame, minY)]; afterImageView.image = afterImage; - [self.contentScrollView addSubview:afterImageView]; + [self.scrollView addSubview:afterImageView]; minY = CGRectGetMaxY(afterImageView.frame) + 16; - UILabel *maskImageLabel2 = [[UILabel alloc] 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); - [self.contentScrollView addSubview:maskImageLabel2]; + [self.scrollView addSubview:maskImageLabel2]; minY = CGRectGetMaxY(maskImageLabel2.frame) + 6; UIImageView *maskImageView2 = [[UIImageView alloc] initWithImage:[UIImageMake(@"image1") qmui_grayImage]]; @@ -717,7 +755,7 @@ - (CGFloat)generateExampleViewForImageWithMaskImage { [maskImageView2 qmui_sizeToFitKeepingImageAspectRatioInSize:CGSizeMake(contentWidth, CGFLOAT_MAX)]; maskImageView2.frame = CGRectSetY(maskImageView2.frame, minY); maskImageView2.clipsToBounds = YES; - [self.contentScrollView addSubview:maskImageView2]; + [self.scrollView addSubview:maskImageView2]; minY = CGRectGetMaxY(maskImageView2.frame) + 16; UILabel *afterLabel2 = [[UILabel alloc] init]; @@ -725,14 +763,14 @@ - (CGFloat)generateExampleViewForImageWithMaskImage { afterLabel2.text = @"B.加了遮罩后的图片"; [afterLabel2 sizeToFit]; afterLabel2.frame = CGRectSetY(afterLabel2.frame, minY); - [self.contentScrollView addSubview:afterLabel2]; + [self.scrollView addSubview:afterLabel2]; minY = CGRectGetMaxY(afterLabel2.frame) + 6; UIImage *afterImage2 = [originImageView.image qmui_imageWithMaskImage:maskImageView2.image usingMaskImageMode:NO]; UIImageView *afterImageView2 = [[UIImageView alloc] initWithFrame:CGRectSetY(originImageView.frame, minY)]; afterImageView2.image = afterImage2; - [self.contentScrollView addSubview:afterImageView2]; + [self.scrollView addSubview:afterImageView2]; minY = CGRectGetMaxY(afterImageView2.frame); return minY; @@ -742,17 +780,17 @@ - (CGFloat)generateExampleViewForImageWithColor { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImage *originImage = [UIImage qmui_imageWithColor:[QDCommonUI randomThemeColor] size:CGSizeMake(contentWidth / 2, 40) cornerRadius:10]; UIImageView *originImageView = [[UIImageView alloc] initWithImage:originImage]; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; return minY; @@ -762,17 +800,36 @@ - (CGFloat)generateExampleViewForImageWithColorAndCornerRadiusArray { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImage *originImage = [UIImage qmui_imageWithColor:[QDCommonUI randomThemeColor] size:CGSizeMake(contentWidth / 2, 40) cornerRadiusArray:@[@0, @0, @10, @10]]; UIImageView *originImageView = [[UIImageView alloc] initWithImage:originImage]; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; + minY = CGRectGetMaxY(originImageView.frame) + 16; + + 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; @@ -782,11 +839,11 @@ - (CGFloat)generateExampleViewForImageWithStrokeColorAndPath { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; CGFloat lineWidth = 1.0f; @@ -795,7 +852,7 @@ - (CGFloat)generateExampleViewForImageWithStrokeColorAndPath { UIImage *originImage = [UIImage qmui_imageWithStrokeColor:[QDCommonUI randomThemeColor] size:CGSizeMake(contentWidth / 2, 40) path:path addClip:NO]; UIImageView *originImageView = [[UIImageView alloc] initWithImage:originImage]; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; return minY; @@ -805,17 +862,17 @@ - (CGFloat)generateExampleViewForImageWithStrokeColorAndCornerRadius { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImage *originImage = [UIImage qmui_imageWithStrokeColor:[QDCommonUI randomThemeColor] size:CGSizeMake(contentWidth / 2, 40) lineWidth:1 cornerRadius:10]; UIImageView *originImageView = [[UIImageView alloc] initWithImage:originImage]; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; return minY; @@ -825,17 +882,17 @@ - (CGFloat)generateExampleViewForImageWithStrokeColorAndBorderPosition { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImage *originImage = [UIImage qmui_imageWithStrokeColor:[QDCommonUI randomThemeColor] size:CGSizeMake(contentWidth / 2, 40) lineWidth:1 borderPosition:QMUIImageBorderPositionLeft|QMUIImageBorderPositionRight|QMUIImageBorderPositionBottom]; UIImageView *originImageView = [[UIImageView alloc] initWithImage:originImage]; originImageView.frame = CGRectSetY(originImageView.frame, minY); - [self.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; return minY; @@ -845,14 +902,14 @@ - (CGFloat)generateExampleViewForImageWithShape { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *titleLabel = [[UILabel alloc] 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.contentScrollView addSubview:titleLabel]; + [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]; @@ -873,15 +930,15 @@ - (CGFloat)generateExampleViewForImageWithShape { } - (CGFloat)generateExampleLabelAndImageViewWithImage:(UIImage *)image shapeName:(NSString *)shapeName minY:(CGFloat)minY { - UILabel *exampleLabel = [[UILabel alloc] 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); - [self.contentScrollView addSubview:exampleLabel]; + [self.scrollView addSubview:exampleLabel]; minY = CGRectGetMaxY(exampleLabel.frame) + 6; UIImageView *exampleImageView = [[UIImageView alloc] initWithImage:image]; - [self.contentScrollView addSubview:exampleImageView]; + [self.scrollView addSubview:exampleImageView]; exampleImageView.frame = CGRectSetY(exampleImageView.frame, minY); minY = CGRectGetMaxY(exampleImageView.frame) + 16; return minY; @@ -890,11 +947,11 @@ - (CGFloat)generateExampleLabelAndImageViewWithImage:(UIImage *)image shapeName: - (CGFloat)generateExampleViewForImageWithAttributedString { CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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); - [self.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 16; NSDictionary *attributes = @{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: [QDCommonUI randomThemeColor]}; @@ -905,14 +962,14 @@ - (CGFloat)generateExampleViewForImageWithAttributedString { exampleLabel.attributedText = attributedString1; [exampleLabel sizeToFit]; exampleLabel.frame = CGRectSetY(exampleLabel.frame, minY); - [self.contentScrollView addSubview:exampleLabel]; + [self.scrollView addSubview:exampleLabel]; minY = CGRectGetMaxY(exampleLabel.frame) + 16; UIImage *exampleImage = [UIImage qmui_imageWithAttributedString:attributedString2]; UIImageView *exampleImageView = [[UIImageView alloc] initWithImage:exampleImage]; exampleImageView.frame = CGRectSetY(exampleImageView.frame, minY); exampleImageView.backgroundColor = UIColorTestRed; - [self.contentScrollView addSubview:exampleImageView]; + [self.scrollView addSubview:exampleImageView]; minY = CGRectGetMaxY(exampleImageView.frame) + 16; return minY; @@ -922,21 +979,21 @@ - (CGFloat)generateExampleViewForImageWithView { CGFloat contentWidth = [self contentViewLimitWidth]; CGFloat minY = [self contentViewLayoutStartingMinY]; - UILabel *originImageLabel = [[UILabel alloc] 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); - [self.contentScrollView addSubview:originImageLabel]; + [self.scrollView addSubview:originImageLabel]; minY = CGRectGetMaxY(originImageLabel.frame) + 6; UIImage *originImage = [UIImage qmui_imageWithView:self.navigationController.view]; 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.contentScrollView addSubview:originImageView]; + [self.scrollView addSubview:originImageView]; minY = CGRectGetMaxY(originImageView.frame) + 16; return minY; diff --git a/qmuidemo/Modules/Demos/UIKit/QDImageViewViewController.h b/qmuidemo/Modules/Demos/UIKit/QDImageViewViewController.h new file mode 100644 index 00000000..b9940cc6 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDImageViewViewController.h @@ -0,0 +1,17 @@ +// +// QDImageViewViewController.h +// qmuidemo +// +// Created by MoLice on 2019/A/28. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDCommonTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDImageViewViewController : QDCommonTableViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/UIKit/QDImageViewViewController.m b/qmuidemo/Modules/Demos/UIKit/QDImageViewViewController.m new file mode 100644 index 00000000..55e683f2 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDImageViewViewController.m @@ -0,0 +1,99 @@ +// +// QDImageViewViewController.m +// qmuidemo +// +// Created by MoLice on 2019/A/28. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDImageViewViewController.h" + +@interface QDImageViewTableViewCell : UITableViewCell + +@end + +@interface QDImageViewViewController () + +@property(nonatomic, assign) BOOL usingSmoothAnimation; +@property(nonatomic, strong) UIImage *animatedImage; +@end + +@implementation QDImageViewViewController + +- (void)didInitialize { + [super didInitialize]; + self.usingSmoothAnimation = YES; +} + +- (void)initTableView { + [super initTableView]; + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + self.animatedImage = [UIImage qmui_animatedImageNamed:@"animatedImage"]; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 2; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return section == 0 ? 1 : 20; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + return indexPath.section == 0 ? 140 : 100; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + NSString *identifier = indexPath.section == 0 ? @"desc" : @"image"; + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if ([identifier isEqualToString:@"desc"]) { + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + 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]; + cell.accessoryView = switchControl; + } + ((UISwitch *)cell.accessoryView).on = self.usingSmoothAnimation; + } else if ([identifier isEqualToString:@"image"]) { + if (!cell) { + cell = [[QDImageViewTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + cell.backgroundColor = TableViewCellBackgroundColor; + cell.imageView.image = self.animatedImage; + } + cell.imageView.qmui_smoothAnimation = self.usingSmoothAnimation; + if (!cell.imageView.qmui_smoothAnimation) { + [cell.imageView startAnimating]; + } + } + + return cell; +} + +- (void)handleSwitchEvent:(UISwitch *)switchControl { + self.usingSmoothAnimation = switchControl.on; + [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationNone]; +} + +@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.h b/qmuidemo/Modules/Demos/UIKit/QDInterceptBackButtonEventViewController.h index 670ddbae..3074d60a 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDInterceptBackButtonEventViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDInterceptBackButtonEventViewController.h @@ -2,7 +2,7 @@ // QDInterceptBackButtonEventViewController.h // qmuidemo // -// Created by zhoonchen on 16/9/5. +// Created by QMUI Team on 16/9/5. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDInterceptBackButtonEventViewController.m b/qmuidemo/Modules/Demos/UIKit/QDInterceptBackButtonEventViewController.m index 7eabeeb9..0cf74930 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDInterceptBackButtonEventViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDInterceptBackButtonEventViewController.m @@ -2,7 +2,7 @@ // QDInterceptBackButtonEventViewController.m // qmuidemo // -// Created by zhoonchen on 16/9/5. +// Created by QMUI Team on 16/9/5. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -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]; @@ -58,9 +52,7 @@ - (void)viewWillAppear:(BOOL)animated { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; CGFloat inset = 12; - CGFloat contentWidht = CGRectGetWidth(self.view.bounds) - 2 * inset; - CGSize labelSize = [self.textCountLabel sizeThatFits:CGSizeMake(contentWidht, CGFLOAT_MAX)]; - self.textCountLabel.frame = CGRectMake(inset, CGRectGetMaxY(self.navigationController.navigationBar.frame) + 20, contentWidht, labelSize.height); + self.textCountLabel.frame = CGRectMake(inset, self.qmui_navigationBarMaxYInViewCoordinator + 20, CGRectGetWidth(self.view.bounds) - 2 * inset, QMUIViewSelfSizingHeight); self.textView.frame = CGRectMake(inset, CGRectGetMaxY(self.textCountLabel.frame) + 10, CGRectGetWidth(self.view.bounds) - 2 * inset, 100); } @@ -79,19 +71,15 @@ - (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]; QMUIAlertController *alertController = [QMUIAlertController alertControllerWithTitle:@"是否返回?" message:@"返回后输入框的数据将不会自动保存" preferredStyle:QMUIAlertControllerStyleAlert]; - QMUIAlertAction *backActioin = [QMUIAlertAction actionWithTitle:@"返回" style:QMUIAlertActionStyleCancel handler:^(QMUIAlertAction *action) { + QMUIAlertAction *backActioin = [QMUIAlertAction actionWithTitle:@"返回" style:QMUIAlertActionStyleCancel handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { [self.navigationController popViewControllerAnimated:YES]; }]; - QMUIAlertAction *continueAction = [QMUIAlertAction actionWithTitle:@"继续编辑" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertAction *action) { + QMUIAlertAction *continueAction = [QMUIAlertAction actionWithTitle:@"继续编辑" style:QMUIAlertActionStyleDefault handler:^(QMUIAlertController *aAlertController, QMUIAlertAction *action) { [self.textView becomeFirstResponder]; }]; [alertController addAction:backActioin]; diff --git a/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.h b/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.h index 4b356302..a28840ea 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.h @@ -2,7 +2,7 @@ // QDLabelViewController.h // qmui // -// Created by MoLice on 14-7-13. +// Created by QMUI Team on 14-7-13. // Copyright (c) 2014年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.m b/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.m index 4d77d5f2..78c45e55 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDLabelViewController.m @@ -2,12 +2,21 @@ // QDLabelViewController.m // qmui // -// Created by MoLice on 14-7-13. +// Created by QMUI Team on 14-7-13. // Copyright (c) 2014年 QMUI Team. All rights reserved. // #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,8 +39,11 @@ - (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:@"已复制"]; + }; [self.label1 sizeToFit]; [self.view addSubview:self.label1]; @@ -39,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]; @@ -61,9 +86,14 @@ - (void)initSubviews { } +- (void)handleTruncatingTailViewEvent:(QDLabelTruncatingTailView *)view { + self.label3.numberOfLines = 0; + [self.view setNeedsLayout]; +} + - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - CGFloat contentMinY = CGRectGetMaxY(self.navigationController.navigationBar.frame); + CGFloat contentMinY = self.qmui_navigationBarMaxYInViewCoordinator; CGFloat buttonSpacingHeight = QDButtonSpacingHeight; self.label1.frame = CGRectSetXY(self.label1.frame, CGFloatGetCenter(CGRectGetWidth(self.view.bounds), CGRectGetWidth(self.label1.bounds)), contentMinY + CGFloatGetCenter(buttonSpacingHeight, CGRectGetHeight(self.label1.bounds))); @@ -74,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 f98f914f..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDLinkButtonViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDLinkButtonViewController.h -// qmuidemo -// -// Created by MoLice 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 2daaba02..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDLinkButtonViewController.m +++ /dev/null @@ -1,73 +0,0 @@ -// -// QDLinkButtonViewController.m -// qmuidemo -// -// Created by MoLice 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 = CGRectGetMaxY(self.navigationController.navigationBar.frame); - 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.h b/qmuidemo/Modules/Demos/UIKit/QDNavigationBarMaxYViewController.h new file mode 100644 index 00000000..442682e2 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDNavigationBarMaxYViewController.h @@ -0,0 +1,18 @@ +// +// QDNavigationBarMaxYViewController.h +// qmuidemo +// +// Created by MoLice on 2019/A/13. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDCommonListViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface QDNavigationBarMaxYViewController : QDCommonListViewController + +@property(nonatomic, assign) BOOL navigationBarHidden; +@end + +NS_ASSUME_NONNULL_END diff --git a/qmuidemo/Modules/Demos/UIKit/QDNavigationBarMaxYViewController.m b/qmuidemo/Modules/Demos/UIKit/QDNavigationBarMaxYViewController.m new file mode 100644 index 00000000..62f1c320 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDNavigationBarMaxYViewController.m @@ -0,0 +1,85 @@ +// +// QDNavigationBarMaxYViewController.m +// qmuidemo +// +// Created by MoLice on 2019/A/13. +// Copyright © 2019 QMUI Team. All rights reserved. +// + +#import "QDNavigationBarMaxYViewController.h" + +@interface QDNavigationBarMaxYViewController () + +@property(nonatomic, strong) UIView *testView; +@end + +@implementation QDNavigationBarMaxYViewController + +- (void)initDataSource { + [super initDataSource]; + self.dataSource = @[@"进入显示 navigationBar 的界面", + @"进入隐藏 navigationBar 的界面"]; +} + +- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { + return @"\n色块顶部的 y 值也即 self.qmui_navigationBarMaxYInViewCoordinator 的值,选择不同显隐状态进入下一个界面,观察 push/pop/手势返回过程中色块布局是否会跳动"; +} + +- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section { + QMUITableViewHeaderFooterView *footerView = (QMUITableViewHeaderFooterView *)[super tableView:tableView viewForFooterInSection:section]; + footerView.backgroundView.backgroundColor = UIColorClear; + footerView.contentEdgeInsets = UIEdgeInsetsSetTop(footerView.contentEdgeInsets, 0); + footerView.titleLabel.font = UIFontMake(12); + footerView.titleLabel.textColor = UIColor.qd_descriptionTextColor; + footerView.titleLabel.qmui_borderPosition = QMUIViewBorderPositionTop; + footerView.titleLabel.qmui_borderColor = TableViewSeparatorColor; + return footerView; +} + +- (void)didSelectCellWithTitle:(NSString *)title { + QDNavigationBarMaxYViewController *viewController = [[QDNavigationBarMaxYViewController alloc] init]; + viewController.navigationBarHidden = [title isEqualToString:@"进入隐藏 navigationBar 的界面"]; + [self.navigationController pushViewController:viewController animated:YES]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + // 主动在转场过程中触发布局的重新运算 + [self.view setNeedsLayout]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self.view setNeedsLayout]; + }); +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + // 主动在转场过程中触发布局的重新运算 + [self.view setNeedsLayout]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self.view setNeedsLayout]; + }); +} + +- (void)initSubviews { + [super initSubviews]; + self.testView = [[UIView alloc] qmui_initWithSize:CGSizeMake(100, 100)]; + self.testView.userInteractionEnabled = NO; + self.testView.backgroundColor = [[QDCommonUI randomThemeColor] colorWithAlphaComponent:.3]; + [self.view addSubview:self.testView]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + // y 紧贴着导航栏底部,以表示当前的 self.qmui_navigationBarMaxYInViewCoordinator 的值 + self.testView.frame = CGRectSetXY(self.testView.frame, CGFloatGetCenter(CGRectGetWidth(self.view.bounds), CGRectGetWidth(self.testView.frame)), self.qmui_navigationBarMaxYInViewCoordinator); +} + +- (BOOL)preferredNavigationBarHidden { + return self.navigationBarHidden; +} + +- (BOOL)forceEnableInteractivePopGestureRecognizer { + return YES; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDNavigationButtonViewController.h b/qmuidemo/Modules/Demos/UIKit/QDNavigationButtonViewController.h index fe2ec137..c3eeca6b 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDNavigationButtonViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDNavigationButtonViewController.h @@ -2,12 +2,12 @@ // QDNavigationButtonViewController.h // qmuidemo // -// Created by zhoonchen on 2016/10/13. +// Created by QMUI Team on 2016/10/13. // Copyright © 2016年 QMUI Team. All rights reserved. // -#import "QDCommonListViewController.h" +#import "QDCommonGroupListViewController.h" -@interface QDNavigationButtonViewController : QDCommonListViewController +@interface QDNavigationButtonViewController : QDCommonGroupListViewController @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDNavigationButtonViewController.m b/qmuidemo/Modules/Demos/UIKit/QDNavigationButtonViewController.m index 90633894..16c18f10 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDNavigationButtonViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDNavigationButtonViewController.m @@ -2,49 +2,83 @@ // QDNavigationButtonViewController.m // qmuidemo // -// Created by zhoonchen on 2016/10/13. -// Copyright © 2016年 QMUI Team. All rights reserved. +// Created by QMUI Team on 2018/04/17. +// Copyright © 2018年 QMUI Team. All rights reserved. // #import "QDNavigationButtonViewController.h" +NSString *const kSectionTitleForNormalButton = @"文本按钮"; +NSString *const kSectionTitleForBoldButton = @"加粗文本按钮"; +NSString *const kSectionTitleForImageButton = @"图片按钮"; +NSString *const kSectionTitleForBackButton = @"返回按钮"; +NSString *const kSectionTitleForCloseButton = @"关闭按钮"; + @interface QDNavigationButtonViewController () @property(nonatomic, assign) BOOL forceEnableBackGesture; - @end @implementation QDNavigationButtonViewController - (void)initDataSource { - self.dataSource = @[@"普通导航栏按钮", - @"加粗导航栏按钮", - @"图标导航栏按钮", - @"关闭导航栏按钮(支持手势返回)", - @"自定义返回按钮(支持手势返回)"]; + self.dataSource = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + kSectionTitleForNormalButton, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"[系统]文本按钮", @"", + @"[QMUI]文本按钮", @"", + nil], + kSectionTitleForBoldButton, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"[系统]加粗文本按钮", @"", + @"[QMUI]加粗文本按钮", @"", + nil], + kSectionTitleForImageButton, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"[系统]图片按钮", @"", + @"[QMUI]图片按钮", @"", + nil], + kSectionTitleForBackButton, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"[系统]返回按钮", @"", + @"[QMUI]返回按钮", @"", + nil], + kSectionTitleForCloseButton, [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: + @"[QMUI]关闭按钮", @"在 present 的场景经常使用这种关闭按钮", + nil], + nil]; } +// 可通过切换“系统”和“QMUI”,看 QMUI 的自定义按钮布局是否与系统的不一致,产生跳动 +// 能用系统的尽量用系统的,QMUINavigationButton 只在必要的时候才使用 - (void)didSelectCellWithTitle:(NSString *)title { - if ([title isEqualToString:@"普通导航栏按钮"]) { - // 最右边的按钮,position 为 Right - UIBarButtonItem *normalItem = [QMUINavigationButton barButtonItemWithType:QMUINavigationButtonTypeNormal title:@"默认" position:QMUINavigationButtonPositionRight target:nil action:NULL]; - - // 支持用 tintColor 参数指定不一样的颜色 - // 不是最右边的按钮,position 为 None - UIBarButtonItem *colorfulItem = [QMUINavigationButton barButtonItemWithType:QMUINavigationButtonTypeNormal title:@"颜色" tintColor:[QDCommonUI randomThemeColor] position:QMUINavigationButtonPositionNone target:nil action:NULL]; - self.navigationItem.rightBarButtonItems = @[normalItem, colorfulItem]; - } else if ([title isEqualToString:@"加粗导航栏按钮"]) { - self.navigationItem.rightBarButtonItems = @[[QMUINavigationButton barButtonItemWithType:QMUINavigationButtonTypeBold title:@"完成(5)" position:QMUINavigationButtonPositionRight target:nil action:NULL]]; - } else if ([title isEqualToString:@"图标导航栏按钮"]) { - UIImage *image = [UIImage qmui_imageWithStrokeColor:UIColorWhite size:CGSizeMake(20, 20) lineWidth:3 cornerRadius:10]; - self.navigationItem.rightBarButtonItems = @[[QMUINavigationButton barButtonItemWithImage:image position:QMUINavigationButtonPositionRight target:nil action:NULL]]; - } else if ([title isEqualToString:@"关闭导航栏按钮(支持手势返回)"]) { - self.forceEnableBackGesture = YES; - self.navigationItem.leftBarButtonItem = [QMUINavigationButton closeBarButtonItemWithTarget:self action:@selector(handleCloseButtonEvent:)]; - } else if ([title isEqualToString:@"自定义返回按钮(支持手势返回)"]) { + if ([title isEqualToString:@"[系统]文本按钮"]) { + UIBarButtonItem *item = [UIBarButtonItem qmui_itemWithTitle:@"文字" target:nil action:NULL]; + self.navigationItem.rightBarButtonItems = @[item]; + } else if ([title isEqualToString:@"[QMUI]文本按钮"]) { + UIBarButtonItem *item = [UIBarButtonItem qmui_itemWithButton:[[QMUINavigationButton alloc] initWithType:QMUINavigationButtonTypeNormal title:@"文字"] target:nil action:NULL]; + self.navigationItem.rightBarButtonItems = @[item]; + } else if ([title isEqualToString:@"[系统]加粗文本按钮"]) { + UIBarButtonItem *item = [UIBarButtonItem qmui_itemWithBoldTitle:@"加粗" target:nil action:NULL]; + self.navigationItem.rightBarButtonItems = @[item]; + } else if ([title isEqualToString:@"[QMUI]加粗文本按钮"]) { + UIBarButtonItem *item = [UIBarButtonItem qmui_itemWithButton:[[QMUINavigationButton alloc] initWithType:QMUINavigationButtonTypeBold title:@"加粗"] target:nil action:NULL]; + self.navigationItem.rightBarButtonItems = @[item]; + } else if ([title isEqualToString:@"[系统]图片按钮"]) { + self.navigationItem.rightBarButtonItems = @[[UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_nav_about") target:nil action:NULL], + [UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_nav_about") target:nil action:NULL]]; + } else if ([title isEqualToString:@"[QMUI]图片按钮"]) { + self.navigationItem.rightBarButtonItems = @[[UIBarButtonItem qmui_itemWithButton:[[QMUINavigationButton alloc] initWithImage:UIImageMake(@"icon_nav_about")] target:nil action:NULL], + [UIBarButtonItem qmui_itemWithButton:[[QMUINavigationButton alloc] initWithImage:UIImageMake(@"icon_nav_about")] target:nil action:NULL]]; + } else if ([title isEqualToString:@"[系统]返回按钮"]) { + self.navigationItem.leftBarButtonItem = nil;// 只要不设置 leftBarButtonItem,就会显示系统的返回按钮 + self.navigationItem.rightBarButtonItems = nil; + } else if ([title isEqualToString:@"[QMUI]返回按钮"]) { + UIBarButtonItem *item = [UIBarButtonItem qmui_backItemWithTarget:self action:@selector(handleBackButtonEvent:)];// 自定义返回按钮要自己写代码去 pop 界面 + self.navigationItem.leftBarButtonItem = item; + self.forceEnableBackGesture = YES;// 当系统的返回按钮被屏蔽的时候,系统的手势返回也会跟着失效,所以这里要手动强制打开手势返回 + self.navigationItem.rightBarButtonItems = nil; + } else if ([title isEqualToString:@"[QMUI]关闭按钮"]) { + UIBarButtonItem *item = [UIBarButtonItem qmui_closeItemWithTarget:self action:@selector(handleCloseButtonEvent:)]; + self.navigationItem.leftBarButtonItem = item; self.forceEnableBackGesture = YES; - QMUINavigationButton *backButton = [[QMUINavigationButton alloc] initWithType:QMUINavigationButtonTypeBack title:@"返回"]; - self.navigationItem.leftBarButtonItem = [QMUINavigationButton barButtonItemWithNavigationButton:backButton position:QMUINavigationButtonPositionLeft target:self action:@selector(handleBackButtonEvent:)]; + self.navigationItem.rightBarButtonItems = nil; } [self.tableView qmui_clearsSelection]; } diff --git a/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.h b/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.h index ba5ae88e..28598204 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.h @@ -2,7 +2,7 @@ // QDNavigationListViewController.h // qmuidemo // -// Created by zhoonchen on 16/9/5. +// Created by QMUI Team on 16/9/5. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.m b/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.m index 1c3b1ab8..48e098d3 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDNavigationListViewController.m @@ -2,23 +2,54 @@ // QDNavigationListViewController.m // qmuidemo // -// Created by zhoonchen on 16/9/5. +// Created by QMUI Team on 16/9/5. // Copyright © 2016年 QMUI Team. All rights reserved. // #import "QDNavigationListViewController.h" #import "QDInterceptBackButtonEventViewController.h" #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 { @@ -26,12 +57,21 @@ - (void)didSelectCellWithTitle:(NSString *)title { if ([title isEqualToString:@"拦截系统navBar返回按钮事件"]) { viewController = [[QDInterceptBackButtonEventViewController alloc] init]; } + else if ([title isEqualToString:@"感知系统的手势返回"]) { + viewController = [[QDNavigationTransitionViewController alloc] init]; + } else if ([title isEqualToString:@"方便控制界面导航栏样式"]) { viewController = [[QDChangeNavBarStyleViewController alloc] init]; } 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.h b/qmuidemo/Modules/Demos/UIKit/QDNavigationTransitionViewController.h new file mode 100644 index 00000000..f1cfe399 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDNavigationTransitionViewController.h @@ -0,0 +1,13 @@ +// +// QDNavigationTransitionViewController.h +// qmuidemo +// +// Created by QMUI Team on 2018/2/5. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +@interface QDNavigationTransitionViewController : QDCommonViewController + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDNavigationTransitionViewController.m b/qmuidemo/Modules/Demos/UIKit/QDNavigationTransitionViewController.m new file mode 100644 index 00000000..03a66c06 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDNavigationTransitionViewController.m @@ -0,0 +1,70 @@ +// +// QDNavigationTransitionViewController.m +// qmuidemo +// +// Created by QMUI Team on 2018/2/5. +// Copyright © 2018年 QMUI Team. All rights reserved. +// + +#import "QDNavigationTransitionViewController.h" + +@interface QDNavigationTransitionViewController () + +@property(nonatomic, strong) QMUILabel *stateLabel; +@end + +@implementation QDNavigationTransitionViewController + +- (void)initSubviews { + [super initSubviews]; + self.stateLabel = [[QMUILabel alloc] qmui_initWithFont:UIFontMake(16) textColor:UIColorWhite]; + self.stateLabel.textAlignment = NSTextAlignmentCenter; + self.stateLabel.contentEdgeInsets = UIEdgeInsetsMake(8, 16, 8, 16); + [self resetStateLabel]; + [self.stateLabel sizeToFit]; + [self.view addSubview:self.stateLabel]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + self.stateLabel.frame = CGRectMake(24, self.qmui_navigationBarMaxYInViewCoordinator + 24, CGRectGetWidth(self.view.bounds) - 24 * 2, CGRectGetHeight(self.stateLabel.frame)); +} + +- (void)resetStateLabel { + self.stateLabel.text = @"请慢慢手势返回"; + self.stateLabel.backgroundColor = [UIColorGray colorWithAlphaComponent:.3]; +} + +// QMUICommonViewController 默认已经实现了 QMUINavigationControllerDelegate,如果你的 vc 并非继承自 QMUICommonViewController,则需要自行实现 。 +// 注意,这一切都需要在 QMUINavigationController 里才有效。 +#pragma mark - + +- (void)navigationController:(QMUINavigationController *)navigationController poppingByInteractiveGestureRecognizer:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer isCancelled:(BOOL)isCancelled viewControllerWillDisappear:(UIViewController *)viewControllerWillDisappear viewControllerWillAppear:(UIViewController *)viewControllerWillAppear { + + if (gestureRecognizer.state == UIGestureRecognizerStateEnded) { + if (isCancelled) { + [QMUITips showInfo:@"松手了,没有触发界面切换"]; + } else { + [QMUITips showSucceed:@"松手了,界面发生切换"]; + } + [self resetStateLabel]; + return; + } + + NSString *stateString = nil; + UIColor *stateColor = nil; + if (gestureRecognizer.state == UIGestureRecognizerStateBegan) { + stateString = @"触发手势返回"; + stateColor = [UIColorBlue colorWithAlphaComponent:.5]; + } else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) { + stateString = @"手势返回中"; + stateColor = [UIColorGreen colorWithAlphaComponent:.5]; + } else { + return; + } + + self.stateLabel.text = stateString; + self.stateLabel.backgroundColor = stateColor; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.h b/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.h index 394e74d2..43bd73a0 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.h @@ -2,7 +2,7 @@ // QDNormalButtonViewController.h // qmuidemo // -// Created by MoLice on 16/10/12. +// Created by QMUI Team on 16/10/12. // Copyright (c) 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.m b/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.m index 71da3c34..9f2fe8d9 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDNormalButtonViewController.m @@ -2,7 +2,7 @@ // QDNormalButtonViewController.m // qmuidemo // -// Created by MoLice on 16/10/12. +// Created by QMUI Team on 16/10/12. // Copyright (c) 2016年 QMUI Team. All rights reserved. // @@ -14,8 +14,9 @@ @interface QDNormalButtonViewController () @property(nonatomic, strong) QMUIButton *borderedButton; @property(nonatomic, strong) QMUIButton *imagePositionButton1; @property(nonatomic, strong) QMUIButton *imagePositionButton2; - @property(nonatomic, strong) CALayer *separatorLayer; +@property(nonatomic, strong) CAShapeLayer *imageButtonSeparatorLayer; + @end @implementation QDNormalButtonViewController @@ -25,48 +26,47 @@ - (void)initSubviews { // 普通按钮 self.normalButton = [QDUIHelper generateDarkFilledButton]; - [self.normalButton setTitle:@"按钮,支持高亮背景色" forState:UIControlStateNormal]; + 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 layer]; - [self.separatorLayer qmui_removeDefaultAnimations]; - self.separatorLayer.backgroundColor = UIColorSeparator.CGColor; + self.separatorLayer = [CALayer qmui_separatorLayer]; [self.view.layer addSublayer:self.separatorLayer]; // 边框按钮 self.borderedButton = [QDUIHelper generateLightBorderedButton]; - [self.borderedButton setTitle:@"边框支持高亮的按钮" forState:UIControlStateNormal]; + [self.borderedButton setTitle:NSLocalizedString(@"QMUIButton_Bordered_Button_Title", @"边框支持高亮的按钮") forState:UIControlStateNormal]; [self.view addSubview:self.borderedButton]; // 图片+文字按钮 self.imagePositionButton1 = [[QMUIButton alloc] init]; - self.imagePositionButton1.adjustsImageTintColorAutomatically = YES; - self.imagePositionButton1.adjustsTitleTintColorAutomatically = YES; - self.imagePositionButton1.tintColor = [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]; - [self.imagePositionButton1 setTitle:@"图片在上方的按钮" forState:UIControlStateNormal]; + [self.imagePositionButton1 setTitle:NSLocalizedString(@"QMUIButton_Image_Position_Button_Title_1", @"Text below image") forState:UIControlStateNormal]; self.imagePositionButton1.titleLabel.font = UIFontMake(11); - self.imagePositionButton1.qmui_borderPosition = QMUIBorderViewPositionTop | QMUIBorderViewPositionRight | QMUIBorderViewPositionBottom; + self.imagePositionButton1.qmui_borderPosition = QMUIViewBorderPositionTop | QMUIViewBorderPositionBottom; [self.view addSubview:self.imagePositionButton1]; self.imagePositionButton2 = [[QMUIButton alloc] init]; - self.imagePositionButton2.adjustsImageTintColorAutomatically = YES; - self.imagePositionButton2.adjustsTitleTintColorAutomatically = YES; - self.imagePositionButton2.tintColor = [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]; - [self.imagePositionButton2 setTitle:@"图片在下方的按钮" forState:UIControlStateNormal]; + [self.imagePositionButton2 setTitle:NSLocalizedString(@"QMUIButton_Image_Position_Button_Title_2", @"Text above image") forState:UIControlStateNormal]; self.imagePositionButton2.titleLabel.font = UIFontMake(11); - self.imagePositionButton2.qmui_borderPosition = QMUIBorderViewPositionTop | QMUIBorderViewPositionBottom; + self.imagePositionButton2.qmui_borderPosition = QMUIViewBorderPositionTop | QMUIViewBorderPositionBottom; [self.view addSubview:self.imagePositionButton2]; + + self.imageButtonSeparatorLayer = [CAShapeLayer qmui_separatorDashLayerWithLineLength:3 lineSpacing:2 lineWidth:PixelOne lineColor:UIColorSeparator.CGColor isHorizontal:NO]; + [self.view.layer addSublayer:self.imageButtonSeparatorLayer]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; - CGFloat contentMinY = CGRectGetMaxY(self.navigationController.navigationBar.frame); + CGFloat contentMinY = self.qmui_navigationBarMaxYInViewCoordinator; CGFloat buttonSpacingHeight = QDButtonSpacingHeight; // 普通按钮 @@ -74,11 +74,13 @@ - (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); self.imagePositionButton2.frame = CGRectSetX(self.imagePositionButton1.frame, CGRectGetMaxX(self.imagePositionButton1.frame)); + + self.imageButtonSeparatorLayer.frame = CGRectFlatMake(CGRectGetMaxX(self.imagePositionButton1.frame), CGRectGetMinY(self.imagePositionButton1.frame), PixelOne, buttonSpacingHeight); } @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.h b/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.h index cda5e2cd..77f12486 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.h @@ -2,13 +2,13 @@ // QDObjectMethodsListViewController.h // qmuidemo // -// Created by MoLice on 2017/3/24. +// Created by QMUI Team on 2017/3/24. // Copyright © 2017年 QMUI Team. All rights reserved. // -#import "QDCommonViewController.h" +#import "QDCommonTableViewController.h" -@interface QDObjectMethodsListViewController : QDCommonViewController +@interface QDObjectMethodsListViewController : QDCommonTableViewController - (instancetype)initWithClass:(Class)aClass; @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.m b/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.m index 1877ac18..8c766412 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDObjectMethodsListViewController.m @@ -2,7 +2,7 @@ // QDObjectMethodsListViewController.m // qmuidemo // -// Created by MoLice on 2017/3/24. +// Created by QMUI Team on 2017/3/24. // Copyright © 2017年 QMUI Team. All rights reserved. // @@ -10,44 +10,211 @@ @interface QDObjectMethodsListViewController () -@property(nonatomic, strong) NSMutableArray *selectorNames; -@property(nonatomic, strong) UITextView *textView; +@property(nonatomic, strong) NSMutableArray *properties;// 如果存在 property 则它放在 section0 +@property(nonatomic, strong) NSMutableArray *ivarNames;// 如果存在 ivar 则它放在 section1 +@property(nonatomic, strong) NSMutableArray *> *selectorNames; +@property(nonatomic, strong) NSMutableArray *indexesString; +@property(nonatomic, strong) NSMutableArray *searchResults; @end @implementation QDObjectMethodsListViewController - (instancetype)initWithClass:(Class)aClass { - if (self = [super initWithNibName:nil bundle:nil]) { - self.automaticallyAdjustsScrollViewInsets = NO; + if (self = [self initWithStyle:UITableViewStylePlain]) { - self.selectorNames = [[NSMutableArray alloc] init]; + // 显示搜索框 + self.shouldShowSearchBar = YES; + self.searchResults = [[NSMutableArray alloc] init]; + + // 属性 + self.properties = [[NSMutableArray alloc] init]; + [NSObject qmui_enumratePropertiesOfClass:aClass includingInherited:NO usingBlock:^(objc_property_t property, NSString *propertyName) { + QMUIPropertyDescriptor *descriptor = [QMUIPropertyDescriptor descriptorWithProperty:property]; + [self.properties addObject:descriptor.description]; + }]; + self.properties = [[[NSOrderedSet alloc] initWithArray:self.properties].array sortedArrayUsingSelector:@selector(compare:)].mutableCopy; + + // 成员变量 + self.ivarNames = [[NSMutableArray alloc] init]; + [NSObject qmui_enumrateIvarsOfClass:aClass includingInherited:NO usingBlock:^(Ivar ivar, NSString *ivarName) { + [self.ivarNames addObject:ivarName]; + }]; + self.ivarNames = [self.ivarNames sortedArrayUsingSelector:@selector(compare:)].mutableCopy; - [NSObject qmui_enumrateInstanceMethodsOfClass:aClass usingBlock:^(SEL selector) { - [self.selectorNames addObject:[NSString stringWithFormat:@"- %@", NSStringFromSelector(selector)]]; + // 方法 + self.selectorNames = [[NSMutableArray alloc] init]; + NSMutableArray *selectorNames = [[NSMutableArray alloc] init]; + [NSObject qmui_enumrateInstanceMethodsOfClass:aClass includingInherited:NO usingBlock:^(Method method, SEL selector) { + [selectorNames addObject:[NSString stringWithFormat:@"- %@", NSStringFromSelector(selector)]]; }]; + selectorNames = [selectorNames sortedArrayUsingSelector:@selector(compare:)].mutableCopy; + + self.indexesString = [[NSMutableArray alloc] init]; + NSMutableArray *selectorNamesInCurrentSection = nil; + for (NSInteger i = 0; i < selectorNames.count; i++) { + NSString *selectorName = selectorNames[i]; + NSString *index = [selectorName substringWithRange:NSMakeRange(2, 1)]; + if (![self.indexesString containsObject:index]) { + [self.indexesString addObject:index]; + + selectorNamesInCurrentSection = [[NSMutableArray alloc] init]; + [self.selectorNames addObject:selectorNamesInCurrentSection]; + } + [selectorNamesInCurrentSection addObject:selectorName]; + } + + // 处理完 selectorName 再将 ivars 插入 dataSource,是为了避免 selectorName 里也存在字母“V”,会导致一些逻辑判断错误 + if (self.ivarNames.count > 0) { + [self.indexesString insertObject:@"V" atIndex:0]; + } + + if (self.properties.count > 0) { + [self.indexesString insertObject:@"P" atIndex:0]; + } + + self.titleView.subtitle = [NSString stringWithFormat:@"%@个属性,%@个成员变量,%@个方法", @(self.properties.count), @(self.ivarNames.count), @(selectorNames.count)]; + self.titleView.style = QMUINavigationTitleViewStyleSubTitleVertical; } return self; } -- (void)initSubviews { - [super initSubviews]; +- (void)initTableView { + [super initTableView]; + self.tableView.rowHeight = 50; +} + +- (void)initSearchController { + [super initSearchController]; + self.searchBar.placeholder = @"支持模糊搜索"; +} + +- (BOOL)isPropertiesSection:(NSInteger)section { + return self.properties.count > 0 && section == 0; +} + +- (BOOL)isIvarSection:(NSInteger)section { + return self.ivarNames.count > 0 && ((self.properties.count > 0 && section == 1) || (self.properties.count <=0 && section == 0)); +} + +#pragma mark - + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + if (tableView == self.tableView) { + return self.selectorNames.count + (self.properties.count > 0 ? 1 : 0) + (self.ivarNames.count > 0 ? 1 : 0); + } + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + if (tableView == self.tableView) { + if ([self isPropertiesSection:section]) { + return self.properties.count; + } + if ([self isIvarSection:section]) { + return self.ivarNames.count; + } + return self.selectorNames[section - (self.properties.count > 0 ? 1 : 0) - (self.ivarNames.count > 0 ? 1 : 0)].count; + } + return self.searchResults.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *identifier = @"cell"; + QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[QMUITableViewCell alloc] initForTableView:tableView withReuseIdentifier:identifier]; + cell.textLabel.adjustsFontSizeToFitWidth = YES; + } - self.textView = [[UITextView alloc] init]; - self.textView.textContainerInset = UIEdgeInsetsMake(20, 16, 20, 16); - self.textView.editable = NO; - self.textView.attributedText = [self attributedStringForTextView]; - [self.view addSubview:self.textView]; + if (tableView == self.tableView) { + cell.textLabel.font = CodeFontMake(14); + cell.textLabel.textColor = TableViewCellTitleLabelColor; + NSString *name = nil; + if ([self isPropertiesSection:indexPath.section]) { + name = self.properties[indexPath.row]; + } else if ([self isIvarSection:indexPath.section]) { + name = self.ivarNames[indexPath.row]; + } else { + name = self.selectorNames[indexPath.section - ((self.properties.count ? 1 : 0)) - (self.ivarNames.count ? 1 : 0)][indexPath.row]; + } + cell.textLabel.text = name; + } else { + // 有时候清空了 searchResults 后还会因为一些原因导致 tableView reload,然后就会越界,所以这里做个保护 + if (indexPath.row < self.searchResults.count) { + cell.textLabel.attributedText = self.searchResults[indexPath.row]; + } + } + [cell updateCellAppearanceWithIndexPath:indexPath]; + return cell; } -- (NSAttributedString *)attributedStringForTextView { - NSDictionary *attributes = @{NSFontAttributeName: CodeFontMake(14), NSForegroundColorAttributeName: UIColorGray1, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:24]}; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:[self.selectorNames componentsJoinedByString:@"\n"] attributes:attributes]; - return attributedString; +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + if (tableView == self.tableView) { + if ([self isPropertiesSection:section]) { + return @"Properties"; + } + if ([self isIvarSection:section]) { + return @"Ivars"; + } + return self.indexesString[section]; + } + return nil; } -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - self.textView.frame = CGRectInsetEdges(self.view.bounds, UIEdgeInsetsMake(CGRectGetMaxY(self.navigationController.navigationBar.frame), 0, 0, 0)); +- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { + if (tableView == self.tableView) { + return self.indexesString; + } + return nil; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:YES]; +} + +#pragma mark - + +- (void)searchController:(QMUISearchController *)searchController updateResultsForSearchString:(NSString *)searchString { + [self.searchResults removeAllObjects]; + + NSArray *searchStringArray = searchString.qmui_toTrimmedArray; + if (!searchStringArray.count) return; + + void (^searchBlock)(NSString *obj, BOOL *stop) = ^(NSString *obj, BOOL *stop) { + NSUInteger lastLocation = NSNotFound; + NSMutableArray *highlightedLocation = [[NSMutableArray alloc] init]; + for (NSInteger i = 0; i < searchStringArray.count; i++) { + NSString *searchChar = searchStringArray[i].lowercaseString; + NSString *selectorString = lastLocation == NSNotFound ? obj : [obj substringFromIndex:lastLocation + 1];// 从上一次查询到的位置往后查询,避免某个字符在 obj 里重复出现,则每次都会取到第一次出现的 location,就不准了 + NSUInteger location = [selectorString.lowercaseString rangeOfString:searchChar].location; + if (location != NSNotFound) { + location += (lastLocation == NSNotFound ? 0 : lastLocation + 1); + } + if (location == NSNotFound || (lastLocation != NSNotFound && location < lastLocation)) { + lastLocation = NSNotFound; + return; + } + lastLocation = location; + [highlightedLocation addObject:@(location)]; + } + if (lastLocation != NSNotFound) { + 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.currentTheme.themeCodeColor range:NSMakeRange(highlightedLocation[i].unsignedIntegerValue, 1)]; + } + [self.searchResults addObject:result]; + } + }; + + [self.properties enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + searchBlock(obj, stop); + }]; + [self.ivarNames enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + searchBlock(obj, stop); + }]; + [self.selectorNames qmui_enumerateNestedArrayWithBlock:searchBlock]; + + [searchController.tableView reloadData]; } @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.h b/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.h index 791b974c..fcc3b75e 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.h @@ -2,7 +2,7 @@ // QDObjectViewController.h // qmuidemo // -// Created by MoLice on 2017/3/24. +// Created by QMUI Team on 2017/3/24. // Copyright © 2017年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.m b/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.m index 70bb47d7..592fcc86 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDObjectViewController.m @@ -2,7 +2,7 @@ // QDObjectViewController.m // qmuidemo // -// Created by MoLice on 2017/3/24. +// Created by QMUI Team on 2017/3/24. // Copyright © 2017年 QMUI Team. All rights reserved. // @@ -23,7 +23,7 @@ - (instancetype)initWithStyle:(UITableViewStyle)style { self.autocompletionClasses = [[NSMutableArray alloc] init]; - dispatch_async(dispatch_get_main_queue(), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ self.allClasses = [self allClassesArray]; }); } @@ -38,7 +38,7 @@ - (void)initSearchController { - (void)viewDidLoad { [super viewDidLoad]; - [self showEmptyViewWithText:@"NSObject (QMUI) 支持列出给定的 Class、Protocol 的方法。本示例允许你查看任意 Class 的实例方法,请通过上方搜索框搜索。" detailText:nil buttonTitle:nil buttonAction:NULL]; + [self showEmptyViewWithText:@"NSObject (QMUI) 支持列出给定的 Class、Protocol 的方法和成员变量。\n本示例允许你搜索任意 Class 的方法和成员变量,请在上方搜索框输入 Class 名。" detailText:nil buttonTitle:nil buttonAction:NULL]; } - (void)showEmptyView { @@ -96,21 +96,20 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger return self.autocompletionClasses.count; } -- (UITableViewCell *)qmui_tableView:(UITableView *)tableView cellWithIdentifier:(NSString *)identifier { - QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (!cell) { - cell = [[QMUITableViewCell alloc] initForTableView:self.tableView withReuseIdentifier:identifier]; + cell = [[QMUITableViewCell alloc] initForTableView:tableView withReuseIdentifier:@"cell"]; cell.textLabel.numberOfLines = 0; } - return cell; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - QMUITableViewCell *cell = [self qmui_tableView:tableView cellWithIdentifier:@"cell"]; 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; @@ -131,7 +130,7 @@ - (void)searchController:(QMUISearchController *)searchController updateResultsF if (searchString.length > 2) { for (NSString *className in self.allClasses) { - if ([className.lowercaseString qmui_includesString:searchString.lowercaseString]) { + if ([className.lowercaseString containsString:searchString.lowercaseString]) { [self.autocompletionClasses addObject:className]; } } @@ -140,11 +139,6 @@ - (void)searchController:(QMUISearchController *)searchController updateResultsF double matchingWeight1 = [self matchingWeightForResult:obj1 withSearchString:searchString]; double matchingWeight2 = [self matchingWeightForResult:obj2 withSearchString:searchString]; NSComparisonResult result = matchingWeight1 == matchingWeight2 ? NSOrderedSame : (matchingWeight1 > matchingWeight2 ? NSOrderedAscending : NSOrderedDescending); - if ([obj1 isEqualToString:@"PLUIView"] && [obj2 isEqualToString:@"UIViewAnimation"]) { - NSLog(@"1, searchString = %@, %@ vs. %@ = %.3f, %.3f", searchString, obj1, obj2, matchingWeight1, matchingWeight2); - } else if ([obj1 isEqualToString:@"UIViewAnimation"] && [obj2 isEqualToString:@"PLUIView"]) { - NSLog(@"2, searchString = %@, %@ vs. %@ = %.3f, %.3f", searchString, obj1, obj2, matchingWeight1, matchingWeight2); - } return result; }]; } diff --git a/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.h b/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.h index 7bec41dc..c871ea78 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.h @@ -2,12 +2,16 @@ // QDOrientationViewController.h // qmuidemo // -// Created by MoLice on 2017/6/23. +// Created by QMUI Team on 2017/6/23. // Copyright © 2017年 QMUI Team. All rights reserved. // #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 dc94e603..db2a2d71 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDOrientationViewController.m @@ -2,13 +2,15 @@ // QDOrientationViewController.m // qmuidemo // -// Created by MoLice on 2017/6/23. +// Created by QMUI Team on 2017/6/23. // Copyright © 2017年 QMUI Team. All rights reserved. // #import "QDOrientationViewController.h" -const NSInteger kIdentifierForDoneCell = 999; +const NSInteger kIdentifierForNextCell = 997; +const NSInteger kIdentifierForCurrentCell = 998; +const NSInteger kIdentifierForCurrentSwitchCell = 999; @interface QDOrientationViewController () @@ -21,23 +23,11 @@ - (instancetype)init { return [self initWithStyle:UITableViewStyleGrouped]; } -- (void)didInitializedWithStyle:(UITableViewStyle)style { - [super didInitializedWithStyle:style]; - self.automaticallyAdjustsScrollViewInsets = NO; -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - CGFloat contentInsetTop = IOS_VERSION >= 11.0 ? -35 : CGRectGetMaxY(self.navigationController.navigationBar.frame) - 35; - self.tableView.contentInset = UIEdgeInsetsSetTop(self.tableView.contentInset, contentInsetTop); - self.tableView.scrollIndicatorInsets = UIEdgeInsetsMake(IOS_VERSION >= 11.0 ? 0 : CGRectGetMaxY(self.navigationController.navigationBar.frame), 0, 0, 0); -} - - (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"; @@ -46,7 +36,7 @@ - (void)initTableView { d.accessoryType = QMUIStaticTableViewCellAccessoryTypeCheckmark; d; }), - ({ + ({ QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; d.identifier = UIInterfaceOrientationMaskLandscapeLeft; d.text = @"UIInterfaceOrientationMaskLandscapeLeft"; @@ -55,7 +45,7 @@ - (void)initTableView { d.accessoryType = QMUIStaticTableViewCellAccessoryTypeCheckmark; d; }), - ({ + ({ QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; d.identifier = UIInterfaceOrientationMaskLandscapeRight; d.text = @"UIInterfaceOrientationMaskLandscapeRight"; @@ -64,7 +54,7 @@ - (void)initTableView { d.accessoryType = QMUIStaticTableViewCellAccessoryTypeCheckmark; d; }), - ({ + ({ QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; d.identifier = UIInterfaceOrientationMaskPortraitUpsideDown; d.text = @"UIInterfaceOrientationMaskPortraitUpsideDown"; @@ -73,43 +63,83 @@ - (void)initTableView { d.accessoryType = QMUIStaticTableViewCellAccessoryTypeCheckmark; d; })], - - // section 1 - @[ - ({ + + // section 1 + @[ + ({ + QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; + d.identifier = kIdentifierForNextCell; + d.text = @"打开符合上述方向的新界面"; + d.accessoryType = QMUIStaticTableViewCellAccessoryTypeDisclosureIndicator; + d.didSelectTarget = self; + d.didSelectAction = @selector(handleNextCellEvent:); + d.cellForRowBlock = ^(UITableView *tableView, __kindof QMUITableViewCell *cell, QMUIStaticTableViewCellData *cellData) { + cell.textLabel.textColor = UIColor.qd_tintColor; + }; + d; + }), + ], + + // section 2 + @[ + ({ QMUIStaticTableViewCellData *d = [[QMUIStaticTableViewCellData alloc] init]; - d.identifier = kIdentifierForDoneCell; - d.text = @"完成方向选择,进入该界面"; + d.identifier = kIdentifierForCurrentCell; + d.text = @"将已选方向设置到当前界面"; d.didSelectTarget = self; - d.didSelectAction = @selector(handleDoneCellEvent:); + 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] 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 { QMUITableViewCell *cell = [tableView.qmui_staticCellDataSource cellForRowAtIndexPath:indexPath]; cell.textLabel.adjustsFontSizeToFitWidth = YES; - - QMUIStaticTableViewCellData *cellData = [tableView.qmui_staticCellDataSource cellDataAtIndexPath:indexPath]; - if (cellData.identifier == kIdentifierForDoneCell) { - cell.textLabel.textAlignment = NSTextAlignmentCenter; - cell.textLabel.textColor = [QDThemeManager sharedInstance].currentTheme.themeTintColor; - } return cell; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { if (section == 0) { - return @"请为下一个界面选择支持的设备方向"; + return @"选择支持的设备方向"; } return nil; } @@ -128,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) { @@ -144,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.h b/qmuidemo/Modules/Demos/UIKit/QDSearchViewController.h index ef8a44de..c0df7dd2 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDSearchViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDSearchViewController.h @@ -2,7 +2,7 @@ // QDSearchViewController.h // qmuidemo // -// Created by MoLice on 16/5/25. +// Created by QMUI Team on 16/5/25. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDSearchViewController.m b/qmuidemo/Modules/Demos/UIKit/QDSearchViewController.m index afb3a4eb..64e1a168 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDSearchViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDSearchViewController.m @@ -2,7 +2,7 @@ // QDSearchViewController.m // qmuidemo // -// Created by MoLice on 16/5/25. +// Created by QMUI Team on 16/5/25. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -19,13 +19,13 @@ @implementation QDRecentSearchView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { - self.backgroundColor = UIColorWhite; + self.backgroundColor = UIColor.qd_backgroundColor; - self.titleLabel = [[QMUILabel alloc] 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]; - self.titleLabel.qmui_borderPosition = QMUIBorderViewPositionBottom; + self.titleLabel.qmui_borderPosition = QMUIViewBorderPositionBottom; [self addSubview:self.titleLabel]; self.floatLayoutView = [[QMUIFloatLayoutView alloc] init]; @@ -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 = UIEdgeInsetsMake(26, 26, 26, 26); + 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)); @@ -84,7 +84,9 @@ - (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; } @@ -94,24 +96,24 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger if (tableView == self.tableView) { return self.keywords.count; } - return self.searchResultsKeywords.count; + return self.searchResultsKeywords.count * 20; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *identifier = @"cell"; QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; if (!cell) { - cell = [[QMUITableViewCell alloc] initForTableView:self.tableView withReuseIdentifier:identifier]; + cell = [[QMUITableViewCell alloc] initForTableView:tableView withReuseIdentifier:identifier]; } 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; } @@ -130,7 +132,7 @@ - (void)searchController:(QMUISearchController *)searchController updateResultsF [self.searchResultsKeywords removeAllObjects]; for (NSString *keyword in self.keywords) { - if ([keyword qmui_includesString:searchString]) { + if ([keyword containsString:searchString]) { [self.searchResultsKeywords addObject:keyword]; } } @@ -144,20 +146,4 @@ - (void)searchController:(QMUISearchController *)searchController updateResultsF } } -- (void)willPresentSearchController:(QMUISearchController *)searchController { - [QMUIHelper renderStatusBarStyleDark]; -} - -- (void)willDismissSearchController:(QMUISearchController *)searchController { - BOOL oldStatusbarLight = NO; - if ([self respondsToSelector:@selector(shouldSetStatusBarStyleLight)]) { - oldStatusbarLight = [self shouldSetStatusBarStyleLight]; - } - if (oldStatusbarLight) { - [QMUIHelper renderStatusBarStyleLight]; - } else { - [QMUIHelper renderStatusBarStyleDark]; - } -} - @end diff --git a/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.h b/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.h index 3f877b54..bb82ca87 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.h @@ -2,7 +2,7 @@ // QDSliderViewController.h // qmuidemo // -// Created by MoLice on 2017/6/1. +// Created by QMUI Team on 2017/6/1. // Copyright © 2017年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.m b/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.m index 774db1c2..643d9db8 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDSliderViewController.m @@ -2,72 +2,120 @@ // QDSliderViewController.m // qmuidemo // -// Created by MoLice on 2017/6/1. +// Created by QMUI Team on 2017/6/1. // Copyright © 2017年 QMUI Team. All rights reserved. // #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] 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(CGRectGetMaxY(self.navigationController.navigationBar.frame) + 32, 24, 24, 24); - - 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/QDTabBarDemoViewController.h b/qmuidemo/Modules/Demos/UIKit/QDTabBarDemoViewController.h new file mode 100644 index 00000000..e0371b2d --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDTabBarDemoViewController.h @@ -0,0 +1,13 @@ +// +// QDTabBarDemoViewController.h +// qmuidemo +// +// Created by QMUI Team on 2016/10/9. +// Copyright © 2016年 QMUI Team. All rights reserved. +// + +#import "QDCommonListViewController.h" + +@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.h b/qmuidemo/Modules/Demos/UIKit/QDTabBarItemViewController.h deleted file mode 100644 index a40bcb6b..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDTabBarItemViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDTabBarItemViewController.h -// qmuidemo -// -// Created by MoLice on 2016/10/9. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QDCommonListViewController.h" - -@interface QDTabBarItemViewController : QDCommonListViewController - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTabBarItemViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTabBarItemViewController.m deleted file mode 100644 index ef8c4ef3..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDTabBarItemViewController.m +++ /dev/null @@ -1,106 +0,0 @@ -// -// QDTabBarItemViewController.m -// qmuidemo -// -// Created by MoLice 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]; - self.tabBar.frame = CGRectMake(0, CGRectGetHeight(self.view.bounds) - CGRectGetHeight(self.tabBar.frame), CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.tabBar.frame)); -} - -- (void)initDataSource { - self.dataSource = @[@"在屏幕底部的 UITabBarItem 上显示未读数", - @"去掉屏幕底部 UITabBarItem 上的未读数", - @"双击 UITabBarItem 可触发双击事件"]; -} - -- (void)didSelectCellWithTitle:(NSString *)title { - - // 利用 [UITabBarItem imageView] 方法获取到某个 UITabBarItem 内的图片容器 - UIImageView *imageViewInTabBarItem = self.tabBar.items.firstObject.qmui_imageView; - - if ([title isEqualToString:@"在屏幕底部的 UITabBarItem 上显示未读数"]) { - - QMUILabel *messageNumberLabel = [self generateMessageNumberLabelWithInteger:8 inView:imageViewInTabBarItem]; - messageNumberLabel.frame = CGRectSetXY(messageNumberLabel.frame, CGRectGetWidth(imageViewInTabBarItem.frame) - 8, -5); - messageNumberLabel.hidden = NO; - - } else if ([title isEqualToString:@"去掉屏幕底部 UITabBarItem 上的未读数"]) { - - QMUILabel *messageNumberLabel = [self messageNumberLabelInView:imageViewInTabBarItem]; - messageNumberLabel.hidden = YES; - - } - - [self.tableView qmui_clearsSelection]; -} - -- (QMUILabel *)generateMessageNumberLabelWithInteger:(NSInteger)integer inView:(UIView *)view { - NSInteger labelTag = 1024; - QMUILabel *numberLabel = [view viewWithTag:labelTag]; - if (!numberLabel) { - numberLabel = [[QMUILabel alloc] initWithFont:UIFontBoldMake(14) textColor:UIColorWhite]; - numberLabel.backgroundColor = UIColorRed; - numberLabel.textAlignment = NSTextAlignmentCenter; - numberLabel.contentEdgeInsets = UIEdgeInsetsMake(2, 5, 2, 5); - numberLabel.clipsToBounds = YES; - numberLabel.tag = labelTag; - [view addSubview:numberLabel]; - } - numberLabel.text = [NSString qmui_stringWithNSInteger:integer]; - [numberLabel sizeToFit]; - if (numberLabel.text.length == 1) { - // 一位数字时,保证宽高相等(因为有些字符可能宽度比较窄) - CGFloat diameter = fmaxf(CGRectGetWidth(numberLabel.bounds), CGRectGetHeight(numberLabel.bounds)); - numberLabel.frame = CGRectMake(CGRectGetMinX(numberLabel.frame), CGRectGetMinY(numberLabel.frame), diameter, diameter); - } - numberLabel.layer.cornerRadius = flat(CGRectGetHeight(numberLabel.bounds) / 2.0); - return numberLabel; -} - -- (QMUILabel *)messageNumberLabelInView:(UIView *)view { - NSInteger labelTag = 1024; - QMUILabel *numberLabel = [view viewWithTag:labelTag]; - return numberLabel; -} - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellAccessoryTypeViewController.h b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellAccessoryTypeViewController.h index ae22f633..b9f5849d 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellAccessoryTypeViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellAccessoryTypeViewController.h @@ -2,7 +2,7 @@ // QDTableViewCellAccessoryTypeViewController.h // qmuidemo // -// Created by MoLice on 2017/6/19. +// Created by QMUI Team on 2017/6/19. // Copyright © 2017年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellAccessoryTypeViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellAccessoryTypeViewController.m index b011c4c0..11da6383 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellAccessoryTypeViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellAccessoryTypeViewController.m @@ -2,7 +2,7 @@ // QDTableViewCellAccessoryTypeViewController.m // qmuidemo // -// Created by MoLice on 2017/6/19. +// Created by QMUI Team on 2017/6/19. // Copyright © 2017年 QMUI Team. All rights reserved. // @@ -15,8 +15,8 @@ @interface QDTableViewCellAccessoryTypeViewController () @implementation QDTableViewCellAccessoryTypeViewController -- (void)didInitialized { - [super didInitialized]; +- (void)didInitialize { + [super didInitialize]; self.dataSource = @[@"UITableViewCellAccessoryNone", @"UITableViewCellAccessoryDisclosureIndicator", @"UITableViewCellAccessoryDetailDisclosureButton", diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellDynamicHeightViewController.h b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellDynamicHeightViewController.h deleted file mode 100644 index ee0a1495..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellDynamicHeightViewController.h +++ /dev/null @@ -1,14 +0,0 @@ -// -// QDTableViewCellDynamicHeightViewController.h -// qmuidemo -// -// Created by zhoonchen on 2016/10/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import -#import "QDCommonTableViewController.h" - -@interface QDTableViewCellDynamicHeightViewController : QDCommonTableViewController - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellDynamicHeightViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellDynamicHeightViewController.m deleted file mode 100644 index a4b48d51..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellDynamicHeightViewController.m +++ /dev/null @@ -1,170 +0,0 @@ -// -// QDTableViewCellDynamicHeightViewController.m -// qmuidemo -// -// Created by zhoonchen on 2016/10/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QDTableViewCellDynamicHeightViewController.h" - -static UIEdgeInsets const kInsets = {15, 16, 15, 16}; -static CGFloat const kAvatarSize = 30; -static CGFloat const kAvatarMarginRight = 12; -static CGFloat const kAvatarMarginBottom = 6; -static CGFloat const kContentMarginBotom = 10; - -@interface QDDynamicTableViewCell : QMUITableViewCell - -@property(nonatomic, strong) UIImageView *avatarImageView; -@property(nonatomic, strong) UILabel *nameLabel; -@property(nonatomic, strong) UILabel *contentLabel; -@property(nonatomic, strong) UILabel *timeLabel; - -- (void)renderWithNameText:(NSString *)nameText contentText:(NSString *)contentText; - -@end - -@implementation QDDynamicTableViewCell - -- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { - if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { - [self initSubviews]; - } - return self; -} - -- (void)initSubviews { - - UIImage *avatarImage = [UIImage qmui_imageWithStrokeColor:[QDCommonUI randomThemeColor] size:CGSizeMake(kAvatarSize, kAvatarSize) lineWidth:3 cornerRadius:6]; - _avatarImageView = [[UIImageView alloc] initWithImage:avatarImage]; - [self.contentView addSubview:self.avatarImageView]; - - _nameLabel = [[UILabel alloc] init]; - self.nameLabel.font = UIFontBoldMake(16); - self.nameLabel.textColor = UIColorGray2; - [self.contentView addSubview:self.nameLabel]; - - _contentLabel = [[UILabel alloc] init]; - self.contentLabel.font = UIFontMake(17); - self.contentLabel.textColor = UIColorGray1; - self.contentLabel.textAlignment = NSTextAlignmentJustified; - self.contentLabel.numberOfLines = 0; - [self.contentView addSubview:self.contentLabel]; - - _timeLabel = [[UILabel alloc] init]; - self.timeLabel.font = UIFontMake(13); - self.timeLabel.textColor = UIColorGray; - [self.contentView addSubview:self.timeLabel]; - -} - -- (void)renderWithNameText:(NSString *)nameText contentText:(NSString *)contentText { - - self.nameLabel.text = nameText; - self.contentLabel.attributedText = [self attributeStringWithString:contentText lineHeight:26]; - self.timeLabel.text = @"昨天 18:24"; - - self.contentLabel.textAlignment = NSTextAlignmentJustified; - - [self setNeedsLayout]; -} - -- (NSAttributedString *)attributeStringWithString:(NSString *)textString lineHeight:(CGFloat)lineHeight { - if (!textString.qmui_trim && textString.qmui_trim.length <= 0) return nil; - NSAttributedString *attriString = [[NSAttributedString alloc] initWithString:textString attributes:@{NSParagraphStyleAttributeName:[NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:lineHeight lineBreakMode:NSLineBreakByTruncatingTail]}]; - return attriString; -} - -- (CGSize)sizeThatFits:(CGSize)size { - CGSize resultSize = CGSizeMake(size.width, 0); - CGFloat contentLabelWidth = size.width - UIEdgeInsetsGetHorizontalValue(kInsets); - - CGFloat resultHeight = UIEdgeInsetsGetHorizontalValue(kInsets) + CGRectGetHeight(self.avatarImageView.bounds) + kAvatarMarginBottom; - - if (self.contentLabel.text.length > 0) { - CGSize contentSize = [self.contentLabel sizeThatFits:CGSizeMake(contentLabelWidth, CGFLOAT_MAX)]; - resultHeight += (contentSize.height + kContentMarginBotom); - } - - if (self.timeLabel.text.length > 0) { - CGSize timeSize = [self.timeLabel sizeThatFits:CGSizeMake(contentLabelWidth, CGFLOAT_MAX)]; - resultHeight += timeSize.height; - } - - resultSize.height = resultHeight; - return resultSize; -} - -- (void)layoutSubviews { - [super layoutSubviews]; - CGFloat contentLabelWidth = CGRectGetWidth(self.contentView.bounds) - UIEdgeInsetsGetHorizontalValue(kInsets); - self.avatarImageView.frame = CGRectSetXY(self.avatarImageView.frame, kInsets.left, kInsets.top); - if (self.nameLabel.text.length > 0) { - CGFloat nameLabelWidth = contentLabelWidth - CGRectGetWidth(self.avatarImageView.bounds) - kAvatarMarginRight; - CGSize nameSize = [self.nameLabel sizeThatFits:CGSizeMake(nameLabelWidth, CGFLOAT_MAX)]; - self.nameLabel.frame = CGRectFlatMake(CGRectGetMaxX(self.avatarImageView.frame) + kAvatarMarginRight, CGRectGetMinY(self.avatarImageView.frame) + (CGRectGetHeight(self.avatarImageView.bounds) - nameSize.height) / 2, nameLabelWidth, nameSize.height); - } - if (self.contentLabel.text.length > 0) { - CGSize contentSize = [self.contentLabel sizeThatFits:CGSizeMake(contentLabelWidth, CGFLOAT_MAX)]; - self.contentLabel.frame = CGRectFlatMake(kInsets.left, CGRectGetMaxY(self.avatarImageView.frame) + kAvatarMarginBottom, contentLabelWidth, contentSize.height); - } - if (self.timeLabel.text.length > 0) { - CGSize timeSize = [self.timeLabel sizeThatFits:CGSizeMake(contentLabelWidth, CGFLOAT_MAX)]; - self.timeLabel.frame = CGRectFlatMake(CGRectGetMinX(self.contentLabel.frame), CGRectGetMaxY(self.contentLabel.frame) + kContentMarginBotom, contentLabelWidth, timeSize.height); - } -} - -@end - -@interface QDTableViewCellDynamicHeightViewController () - -@property(nonatomic, copy) NSArray *names; -@property(nonatomic, copy) NSArray *contents; - -@end - -@implementation QDTableViewCellDynamicHeightViewController - -- (void)viewDidLoad { - [super viewDidLoad]; - self.names = @[@"张三 的想法", @"李四 的想法", @"张三 的想法", @"李四 的想法", @"张三 的想法", @"李四 的想法", @"张三 的想法", @"李四 的想法", @"张三 的想法", @"李四 的想法", @"张三 的想法"]; - self.contents = @[@"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。", @"UIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。", @"丰富的 UI 控件:提供丰富且常用的 UI 控件,使用方便灵活,并且支持自定义控件的样式。", @"高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", @"iOS UI 解决方案:QMUI iOS 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 iOS 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。", @"全局 UI 配置:只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。", @"UIKit 拓展及版本兼容:拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。", @"丰富的 UI 控件:提供丰富且常用的 UI 控件,使用方便灵活,并且支持自定义控件的样式。", @"高效的工具方法及宏:提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。", @"iOS UI 解决方案:QMUI iOS 的设计目的是用于辅助快速搭建一个具备基本设计还原效果的 iOS 项目,同时利用自身提供的丰富控件及兼容处理,让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。"]; -} - -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - return 1; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return MIN(self.names.count, self.contents.count); -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - static NSString *cellIdentifier = @"cell"; - return [self.tableView qmui_heightForCellWithIdentifier:cellIdentifier cacheByIndexPath:indexPath configuration:^(id cell) { - [cell renderWithNameText:[self.names objectAtIndex:indexPath.row] contentText:[self.contents objectAtIndex:indexPath.row]]; - }]; -} - -- (UITableViewCell *)qmui_tableView:(UITableView *)tableView cellWithIdentifier:(NSString *)identifier { - QDDynamicTableViewCell *cell = (QDDynamicTableViewCell *)[tableView dequeueReusableCellWithIdentifier:identifier]; - if (!cell) { - cell = [[QDDynamicTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; - } - cell.separatorInset = UIEdgeInsetsMake(0, 0, 0, 0); - return cell; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - static NSString *cellIdentifier = @"cell"; - QDDynamicTableViewCell *cell = (QDDynamicTableViewCell *)[self qmui_tableView:tableView cellWithIdentifier:cellIdentifier]; - [cell renderWithNameText:[self.names objectAtIndex:indexPath.row] contentText:[self.contents objectAtIndex:indexPath.row]]; - return cell; -} - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - [self.tableView qmui_clearsSelection]; -} - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellInsetsViewController.h b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellInsetsViewController.h index dff27cc8..9e6d5641 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellInsetsViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellInsetsViewController.h @@ -2,7 +2,7 @@ // QDTableViewCellInsetsViewController.h // qmuidemo // -// Created by zhoonchen on 2016/10/11. +// Created by QMUI Team on 2016/10/11. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellInsetsViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellInsetsViewController.m index 24729403..817b1534 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellInsetsViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewCellInsetsViewController.m @@ -2,7 +2,7 @@ // QDTableViewCellInsetsViewController.m // qmuidemo // -// Created by zhoonchen on 2016/10/11. +// Created by QMUI Team on 2016/10/11. // Copyright © 2016年 QMUI Team. All rights reserved. // @@ -33,19 +33,14 @@ - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInte return nil; } -- (UITableViewCell *)qmui_tableView:(UITableView *)tableView cellWithIdentifier:(NSString *)identifier { - QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (!cell) { - cell = [[QMUITableViewCell alloc] initForTableView:self.tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier]; + cell = [[QMUITableViewCell alloc] initForTableView:tableView withStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"]; cell.imageView.image = [UIImage qmui_imageWithShape:QMUIImageShapeOval size:CGSizeMake(16, 16) lineWidth:2 tintColor:[QDCommonUI randomThemeColor]]; cell.textLabel.text = NSStringFromClass([QMUITableViewCell class]); cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; } - return cell; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - QMUITableViewCell *cell = [self qmui_tableView:tableView cellWithIdentifier:@"cell"]; // reset cell.imageEdgeInsets = UIEdgeInsetsZero; 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 acdfa076..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDTableViewCellViewController.h -// qmui -// -// Created by ZhoonChen on 14/11/5. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QDCommonListViewController.h" - -@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 98b77324..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDTableViewCellViewController.m +++ /dev/null @@ -1,43 +0,0 @@ -// -// QDTableViewCellViewController.m -// qmui -// -// Created by ZhoonChen on 14/11/5. -// Copyright (c) 2014年 QMUI Team. All rights reserved. -// - -#import "QDTableViewCellViewController.h" -#import "QDTableViewCellInsetsViewController.h" -#import "QDTableViewCellAccessoryTypeViewController.h" -#import "QDTableViewCellDynamicHeightViewController.h" - -@interface QDTableViewCellViewController () - -@end - -@implementation QDTableViewCellViewController - -- (void)initDataSource { - self.dataSourceWithDetailText = [[QMUIOrderedDictionary alloc] initWithKeysAndObjects: - @"通过 insets 系列属性调整间距", @"", - @"通过配置表修改 accessoryType 的样式", @"", - @"动态高度计算", @"", - nil]; -} - -- (void)didSelectCellWithTitle:(NSString *)title { - [self.tableView qmui_clearsSelection]; - UIViewController *viewController = nil; - NSString *dataString = title; - if ([dataString isEqualToString:@"通过 insets 系列属性调整间距"]) { - viewController = [[QDTableViewCellInsetsViewController alloc] init]; - } else if ([dataString isEqualToString:@"通过配置表修改 accessoryType 的样式"]) { - viewController = [[QDTableViewCellAccessoryTypeViewController alloc] init]; - } else if ([dataString isEqualToString:@"动态高度计算"]) { - viewController = [[QDTableViewCellDynamicHeightViewController 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 new file mode 100644 index 00000000..5ecfbc9b --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.h @@ -0,0 +1,14 @@ +// +// QDTableViewHeaderFooterViewController.h +// qmuidemo +// +// Created by QMUI Team on 2017/11/7. +// Copyright © 2017年 QMUI Team. All rights reserved. +// + +#import "QDStyleSelectableTableViewController.h" + +/// 展示 QMUITableViewHeaderFooterView 以及 UITableView (QMUI) 里与 section header 相关运算的 demo +@interface QDTableViewHeaderFooterViewController : QDStyleSelectableTableViewController + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.m new file mode 100644 index 00000000..4e7786f6 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDTableViewHeaderFooterViewController.m @@ -0,0 +1,212 @@ +// +// QDTableViewHeaderFooterViewController.m +// qmuidemo +// +// Created by QMUI Team on 2017/11/7. +// Copyright © 2017年 QMUI Team. All rights reserved. +// + +#import "QDTableViewHeaderFooterViewController.h" + +@interface QDTableViewInsetDebugPanelView : UIView + +- (void)renderWithTableView:(UITableView *)tableView; +@end + +@interface QDTableViewHeaderFooterViewController () + +@property(nonatomic, strong) QDTableViewInsetDebugPanelView *debugView; +@end + +@implementation QDTableViewHeaderFooterViewController + +- (void)initSubviews { + [super initSubviews]; + self.debugView = [[QDTableViewInsetDebugPanelView alloc] init]; + [self.view addSubview:self.debugView]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + UIEdgeInsets margins = UIEdgeInsetsZero; + CGFloat debugViewWidth = fmin(self.view.qmui_width, [QMUIHelper screenSizeFor55Inch].width) - UIEdgeInsetsGetHorizontalValue(margins); + CGFloat debugViewHeight = 126; + CGFloat debugViewMinX = CGFloatGetCenter(self.view.qmui_width, debugViewWidth); + self.debugView.frame = CGRectMake(debugViewMinX, self.view.qmui_height - margins.bottom - debugViewHeight, debugViewWidth, debugViewHeight); +} + +- (void)handleButtonEvent:(UIView *)view { + // 通过这个方法获取到点击的按钮所处的 sectionHeader,可兼容 sectionHeader 停靠在列表顶部的场景 + NSInteger sectionIndexForView = [self.tableView qmui_indexForSectionHeaderAtView:view]; + if (sectionIndexForView != -1) { + [QMUITips showWithText:[NSString stringWithFormat:@"点击了 section%@ 上的按钮", @(sectionIndexForView)] inView:self.view hideAfterDelay:1.2]; + } else { + [QMUITips showError:@"无法定位被点击的按钮所处的 section" inView:self.view hideAfterDelay:1.2]; + } +} + +// 下面这堆代码都不用看,主要看上面的 handle 方法 + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [self.debugView renderWithTableView:self.tableView];// 进入界面过程中触发的那一次 scrollViewDidScroll: 还不是最终的状态,所以在 didAppear 时主动刷新一遍 +} + +#pragma mark - + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + [self.debugView renderWithTableView:self.tableView]; +} + +#pragma mark - + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 10; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + if (section == 2) return 0; + return 3; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *identifier = @"cell"; + QMUITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; + if (!cell) { + cell = [[QMUITableViewCell alloc] initForTableView:tableView withReuseIdentifier:identifier]; + cell.selectionStyle = UITableViewCellSelectionStyleNone; + } + cell.textLabel.text = [NSString qmui_stringWithNSInteger:indexPath.row]; + [cell updateCellAppearanceWithIndexPath:indexPath]; + return cell; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + return [NSString stringWithFormat:@"Section%@", @(section)]; +} + +- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { + QMUITableViewHeaderFooterView *headerView = (QMUITableViewHeaderFooterView *)[super tableView:tableView viewForHeaderInSection:section]; + QMUIButton *button = (QMUIButton *)headerView.accessoryView; + if (!button) { + button = [QDUIHelper generateLightBorderedButton]; + [button setTitle:@"Button" forState:UIControlStateNormal]; + button.titleLabel.font = UIFontMake(14); + button.contentEdgeInsets = UIEdgeInsetsMake(4, 12, 4, 12); + [button sizeToFit]; + button.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + button.qmui_outsideEdge = UIEdgeInsetsMake(-8, -8, -8, -8); + [button addTarget:self action:@selector(handleButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + headerView.accessoryView = button; + } + return headerView; +} + +@end + +@interface QDTableViewInsetDebugPanelView () + +// 可视范围内的 sectionHeader 列表 +@property(nonatomic, strong) UILabel *visibleHeadersLabel; +@property(nonatomic, strong) UILabel *visibleHeadersValue; + +// 当前 pinned 的那个 section 序号 +@property(nonatomic, strong) UILabel *pinnedHeaderLabel; +@property(nonatomic, strong) UILabel *pinnedHeaderValue; + +// 某个指定的 section 的 pinned 状态 +@property(nonatomic, strong) UILabel *headerPinnedLabel; +@property(nonatomic, strong) UILabel *headerPinnedValue; +@end + +@implementation QDTableViewInsetDebugPanelView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + + self.userInteractionEnabled = NO; + self.backgroundColor = UIColorMakeWithRGBA(0, 0, 0, .7); + + self.visibleHeadersLabel = [self generateTitleLabel]; + self.visibleHeadersLabel.text = @"可视的 sectionHeaders"; + self.visibleHeadersValue = [self generateValueLabel]; + + self.pinnedHeaderLabel = [self generateTitleLabel]; + self.pinnedHeaderLabel.text = @"正在 pinned(悬浮)的 header"; + self.pinnedHeaderValue = [self generateValueLabel]; + + self.headerPinnedLabel = [self generateTitleLabel]; + self.headerPinnedLabel.text = @"section0 和 section1 的 pinned"; + self.headerPinnedValue = [self generateValueLabel]; + } + return self; +} + +- (UILabel *)generateTitleLabel { + UILabel *label = [[UILabel alloc] qmui_initWithFont:UIFontMake(12) textColor:UIColorWhite]; + [label qmui_calculateHeightAfterSetAppearance]; + [self addSubview:label]; + return label; +} + +- (UILabel *)generateValueLabel { + UILabel *label = [[UILabel alloc] qmui_initWithFont:UIFontMake(12) textColor:UIColorWhite]; + label.textAlignment = NSTextAlignmentRight; + [label qmui_calculateHeightAfterSetAppearance]; + [self addSubview:label]; + return label; +} + +- (void)renderWithTableView:(UITableView *)tableView { + self.visibleHeadersValue.text = [tableView.qmui_indexForVisibleSectionHeaders componentsJoinedByString:@", "]; + + NSInteger indexOfPinnedSectionHeader = tableView.qmui_indexOfPinnedSectionHeader; + NSString *pinnedHeaderString = [NSString qmui_stringWithNSInteger:indexOfPinnedSectionHeader]; + self.pinnedHeaderValue.text = pinnedHeaderString; + self.pinnedHeaderValue.textColor = indexOfPinnedSectionHeader == -1 ? UIColorRed : UIColorWhite; + + BOOL isSectionHeader0Pinned = [tableView qmui_isHeaderPinnedForSection:0]; + BOOL isSectionHeader1Pinned = [tableView qmui_isHeaderPinnedForSection:1]; + NSMutableAttributedString *headerPinnedString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"0: %@ | 1: %@", StringFromBOOL(isSectionHeader0Pinned), StringFromBOOL(isSectionHeader1Pinned)] attributes:@{NSFontAttributeName: self.pinnedHeaderValue.font, NSForegroundColorAttributeName: UIColorWhite}]; + + NSRange range0 = isSectionHeader0Pinned ? NSMakeRange(3, 3) : NSMakeRange(3, 2); + NSRange range1 = isSectionHeader1Pinned ? NSMakeRange(headerPinnedString.length - 3, 3) : NSMakeRange(headerPinnedString.length - 2, 2); + [headerPinnedString addAttribute:NSForegroundColorAttributeName value:isSectionHeader0Pinned ? UIColorGreen : UIColorRed range:range0]; + [headerPinnedString addAttribute:NSForegroundColorAttributeName value:isSectionHeader1Pinned ? UIColorGreen : UIColorRed range:range1]; + self.headerPinnedValue.attributedText = headerPinnedString; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + 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]; + + CGFloat contentWidth = self.qmui_width - UIEdgeInsetsGetHorizontalValue(padding); + CGFloat labelHorizontalSpacing = 16; + CGFloat labelVerticalSpacing = 16; + CGFloat minY = padding.top; + + // 左边的 label + CGFloat leftLabelWidth = flat((contentWidth - labelHorizontalSpacing) * 3 / 5); + for (NSInteger i = 0; i < leftLabels.count; i++) { + UILabel *label = leftLabels[i]; + label.frame = CGRectFlatMake(padding.left, minY, leftLabelWidth, label.qmui_height); + minY = label.qmui_bottom + labelVerticalSpacing; + } + + // 右边的 label + minY = padding.top; + CGFloat rightLabelMinX = leftLabels.firstObject.qmui_right + labelHorizontalSpacing; + CGFloat rightLabelWidth = flat(contentWidth - leftLabelWidth - labelHorizontalSpacing); + for (NSInteger i = 0; i < rightLabels.count; i++) { + UILabel *label = rightLabels[i]; + label.frame = CGRectFlatMake(rightLabelMinX, minY, rightLabelWidth, label.qmui_height); + minY = label.qmui_bottom + labelVerticalSpacing; + } + + self.layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:self.layer.cornerRadius].CGPath; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.h b/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.h index 90d7f81f..6e3ff7ba 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.h @@ -2,7 +2,7 @@ // QDTextFieldViewController.h // qmui // -// Created by ZhoonChen on 14-8-6. +// Created by QMUI Team on 14-8-6. // Copyright (c) 2014年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.m index c4622b6a..5687e788 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDTextFieldViewController.m @@ -2,7 +2,7 @@ // QDTextFieldViewController.m // qmui // -// Created by ZhoonChen on 14-8-6. +// Created by QMUI Team on 14-8-6. // Copyright (c) 2014年 QMUI Team. All rights reserved. // @@ -16,47 +16,54 @@ @interface QDTextFieldViewController () @implementation QDTextFieldViewController -- (void)didInitialized { - [super didInitialized]; - // https://github.com/QMUI/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:@"支持自定义 placeholder 颜色,支持调整输入框与文字之间的间距,支持限制最大可输入的文字长度(可试试输入 emoji、从中文输入法候选词输入等)。" attributes:@{NSFontAttributeName: UIFontMake(12), NSForegroundColorAttributeName: UIColorGray6, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:16]}]; + 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(CGRectGetMaxY(self.navigationController.navigationBar.frame) + 16, 16, 16, 16); + 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); - CGFloat tipsLabelHeight = [self.tipsLabel sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)].height; - self.tipsLabel.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.textField.frame) + 8, contentWidth, tipsLabelHeight); + 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.h b/qmuidemo/Modules/Demos/UIKit/QDTextViewController.h index a9b4529d..ade4725c 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTextViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDTextViewController.h @@ -2,7 +2,7 @@ // QDTextViewController.h // qmui // -// Created by ZhoonChen on 14-8-5. +// Created by QMUI Team on 14-8-5. // Copyright (c) 2014年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDTextViewController.m b/qmuidemo/Modules/Demos/UIKit/QDTextViewController.m index 7ddd43bf..a77e5c90 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDTextViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDTextViewController.m @@ -2,7 +2,7 @@ // QDTextViewController.m // qmui // -// Created by ZhoonChen on 14-8-5. +// Created by QMUI Team on 14-8-5. // Copyright (c) 2014年 QMUI Team. All rights reserved. // @@ -12,7 +12,6 @@ @interface QDTextViewController () @property(nonatomic,strong) QMUITextView *textView; @property(nonatomic, assign) CGFloat textViewMinimumHeight; -@property(nonatomic, assign) CGFloat textViewMaximumHeight; @property(nonatomic, strong) UILabel *tipsLabel; @end @@ -21,9 +20,7 @@ @implementation QDTextViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - self.automaticallyAdjustsScrollViewInsets = NO; self.textViewMinimumHeight = 96; - self.textViewMaximumHeight = 200; } return self; } @@ -31,46 +28,57 @@ - (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 的颜色 - self.textView.autoResizable = YES; self.textView.textContainerInset = UIEdgeInsetsMake(10, 7, 10, 7); 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; + // 限制输入框自增高的最大高度 + self.textView.maximumHeight = 200; + self.textView.layer.borderWidth = PixelOne; self.textView.layer.borderColor = UIColorSeparator.CGColor; self.textView.layer.cornerRadius = 4; [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(CGRectGetMaxY(self.navigationController.navigationBar.frame) + 16, 16, 16, 16); + 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)]; - self.textView.frame = CGRectMake(padding.left, padding.top, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding), fminf(self.textViewMaximumHeight, fmaxf(textViewSize.height, self.textViewMinimumHeight))); + self.textView.frame = CGRectMake(padding.left, padding.top, CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding), fmax(textViewSize.height, self.textViewMinimumHeight)); CGFloat tipsLabelHeight = [self.tipsLabel sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)].height; self.tipsLabel.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.textView.frame) + 8, contentWidth, tipsLabelHeight); } +- (BOOL)shouldHideKeyboardWhenTouchInView:(UIView *)view { + // 表示点击空白区域都会降下键盘 + return YES; +} + #pragma mark - +// 实现这个 delegate 方法就可以实现自增高 - (void)textView:(QMUITextView *)textView newHeightAfterTextChanged:(CGFloat)height { - height = fminf(self.textViewMaximumHeight, fmaxf(height, self.textViewMinimumHeight)); + height = fmax(height, self.textViewMinimumHeight); BOOL needsChangeHeight = CGRectGetHeight(textView.frame) != height; if (needsChangeHeight) { [self.view setNeedsLayout]; @@ -79,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/QDToolBarButtonViewController.h b/qmuidemo/Modules/Demos/UIKit/QDToolBarButtonViewController.h index e067bd78..be29c797 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDToolBarButtonViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDToolBarButtonViewController.h @@ -2,7 +2,7 @@ // QDToolBarButtonViewController.h // qmuidemo // -// Created by zhoonchen on 2016/10/13. +// Created by QMUI Team on 2016/10/13. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDToolBarButtonViewController.m b/qmuidemo/Modules/Demos/UIKit/QDToolBarButtonViewController.m index 367aeedc..1c991e88 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDToolBarButtonViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDToolBarButtonViewController.m @@ -2,7 +2,7 @@ // QDToolBarButtonViewController.m // qmuidemo // -// Created by zhoonchen on 2016/10/13. +// Created by QMUI Team on 2016/10/13. // Copyright © 2016年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.h b/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.h index 1bb54362..1f3f38a3 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.h +++ b/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.h @@ -2,7 +2,7 @@ // QDUIKitViewController.h // qmuidemo // -// Created by ZhoonChen on 15/6/2. +// Created by QMUI Team on 15/6/2. // Copyright (c) 2015年 QMUI Team. All rights reserved. // diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.m b/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.m index 42411ddd..fa419a44 100644 --- a/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.m +++ b/qmuidemo/Modules/Demos/UIKit/QDUIKitViewController.m @@ -2,29 +2,42 @@ // QDUIKitViewController.m // qmuidemo // -// Created by ZhoonChen on 15/6/2. +// Created by QMUI Team on 15/6/2. // Copyright (c) 2015年 QMUI Team. All rights reserved. // #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 "QDTableViewCellViewController.h" #import "QDButtonViewController.h" #import "QDAlertController.h" #import "QDSearchViewController.h" #import "QDNavigationListViewController.h" -#import "QDTabBarItemViewController.h" -#import "QDUIViewQMUIViewController.h" -#import "QDCollectionListViewController.h" +#import "QDTabBarDemoViewController.h" #import "QDAboutViewController.h" #import "QDObjectViewController.h" #import "QDFontViewController.h" #import "QDSliderViewController.h" #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 @@ -34,26 +47,34 @@ - (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"), - @"QMUITableViewCell", UIImageMake(@"icon_grid_cell"), - @"QMUICollectionViewLayout", UIImageMake(@"icon_grid_collection"), - @"QMUISearchController", UIImageMake(@"icon_grid_search"), + @"QMUITableView", UIImageMake(@"icon_grid_cell"), @"ViewController Orientation", UIImageMake(@"icon_grid_orientation"), - @"UINavigationController+QMUI", UIImageMake(@"icon_grid_navigation"), - @"UITabBarItem+QMUI", UIImageMake(@"icon_grid_tabBarItem"), + @"QMUINavigationController", UIImageMake(@"icon_grid_navigation"), + @"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)setNavigationItemsIsInEditMode:(BOOL)isInEditMode animated:(BOOL)animated { - [super setNavigationItemsIsInEditMode:isInEditMode animated:animated]; +- (void)didInitialize { + [super didInitialize]; self.title = @"QMUIKit"; - self.navigationItem.rightBarButtonItem = [QMUINavigationButton barButtonItemWithImage:UIImageMake(@"icon_nav_about") position:QMUINavigationButtonPositionRight target:self action:@selector(handleAboutItemEvent)]; +} + +- (void)setupNavigationItems { + [super setupNavigationItems]; + self.navigationItem.rightBarButtonItem = [UIBarButtonItem qmui_itemWithImage:UIImageMake(@"icon_nav_about") target:self action:@selector(handleAboutItemEvent)]; + AddAccessibilityLabel(self.navigationItem.rightBarButtonItem, @"打开关于界面"); } - (void)didSelectCellWithTitle:(NSString *)title { @@ -64,6 +85,9 @@ - (void)didSelectCellWithTitle:(NSString *)title { else if ([title isEqualToString:@"UIImage+QMUI"]) { viewController = [[QDImageViewController alloc] init]; } + else if ([title isEqualToString:@"UIImageView+QMUI"]) { + viewController = [[QDImageViewViewController alloc] init]; + } else if ([title isEqualToString:@"QMUILabel"]) { viewController = [[QDLabelViewController alloc] init]; } @@ -73,20 +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:@"QMUITableViewCell"]) { - viewController = [[QDTableViewCellViewController alloc] init]; - } - else if ([title isEqualToString:@"QMUICollectionViewLayout"]) { - viewController = [[QDCollectionListViewController alloc] init]; + else if ([title isEqualToString:@"QMUITableView"]) { + 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]; @@ -94,21 +178,50 @@ - (void)didSelectCellWithTitle:(NSString *)title { else if ([title isEqualToString:@"ViewController Orientation"]) { viewController = [[QDOrientationViewController alloc] init]; } - else if ([title isEqualToString:@"UINavigationController+QMUI"]) { + 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]; } + 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.h b/qmuidemo/Modules/Demos/UIKit/QDUIViewBorderViewController.h new file mode 100644 index 00000000..7f529348 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDUIViewBorderViewController.h @@ -0,0 +1,13 @@ +// +// QDUIViewBorderViewController.h +// qmuidemo +// +// Created by QMUI Team on 2017/8/8. +// Copyright © 2017年 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +@interface QDUIViewBorderViewController : QDCommonViewController + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewBorderViewController.m b/qmuidemo/Modules/Demos/UIKit/QDUIViewBorderViewController.m new file mode 100644 index 00000000..debe7ff2 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDUIViewBorderViewController.m @@ -0,0 +1,484 @@ +// +// QDUIViewBorderViewController.m +// qmuidemo +// +// Created by QMUI Team on 2017/8/8. +// Copyright © 2017年 QMUI Team. All rights reserved. +// + +#import "QDUIViewBorderViewController.h" + +@interface QDUIViewBorderViewController () + +@property(nonatomic, strong) UIView *targetView; + +@property(nonatomic, strong) UIScrollView *scrollView; + +@property(nonatomic, strong) QMUILabel *locationTitleLabel; +@property(nonatomic, strong) QMUISegmentedControl *locationSegmentedControl; + +@property(nonatomic, strong) QMUILabel *positionTitleLabel; +@property(nonatomic, strong) QMUIButton *positionTopButton; +@property(nonatomic, strong) QMUIButton *positionLeftButton; +@property(nonatomic, strong) QMUIButton *positionBottomButton; +@property(nonatomic, strong) QMUIButton *positionRightButton; + +@property(nonatomic, strong) QMUILabel *maskedCornersTitleLabel; +@property(nonatomic, strong) QMUIButton *maskedCornersMinXMinYButton; +@property(nonatomic, strong) QMUIButton *maskedCornersMaxXMinYButton; +@property(nonatomic, strong) QMUIButton *maskedCornersMinXMaxYButton; +@property(nonatomic, strong) QMUIButton *maskedCornersMaxXMaxYButton; + +@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; + +@property(nonatomic, strong) QMUILabel *colorTitleLabel; +@property(nonatomic, strong) QMUISegmentedControl *colorSegmentedControl; + +@property(nonatomic, strong) QMUILabel *dashPatternTitleLabel; +@property(nonatomic, strong) QMUITextField *dashPatternWidthTextField; +@property(nonatomic, strong) QMUITextField *dashPatternSpacingTextField; + +@property(nonatomic, strong) QMUILabel *dashPhaseTitleLabel; +@property(nonatomic, strong) QMUITextField *dashPhaseTextField; + +@property(nonatomic, strong) QMUIKeyboardManager *keyboardManager; + +@property(nonatomic, strong) UIView *magnifyingView; +@property(nonatomic, strong) UIImageView *magnifyingImageView; +@end + +@implementation QDUIViewBorderViewController + +- (void)initSubviews { + [super initSubviews]; + self.targetView = [[UIView alloc] qmui_initWithSize:CGSizeMake(100, 100)]; + self.targetView.backgroundColor = [UIColor.qd_tintColor colorWithAlphaComponent:.3]; + [self.view addSubview:self.targetView]; + + self.scrollView = [[UIScrollView alloc] init]; + self.scrollView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; + [self.view addSubview:self.scrollView]; + + self.locationTitleLabel = [self generateTitleLabelWithText:NSStringFromSelector(@selector(qmui_borderLocation))]; + self.locationSegmentedControl = [self generateSegmentedControlWithItems:@[@"Inside", @"Center", @"Outside"]]; + + self.positionTitleLabel = [self generateTitleLabelWithText:NSStringFromSelector(@selector(qmui_borderPosition))]; + self.positionTopButton = [self generateSelectableButtonWithTitle:@"Top"]; + self.positionLeftButton = [self generateSelectableButtonWithTitle:@"Left"]; + self.positionBottomButton = [self generateSelectableButtonWithTitle:@"Bottom"]; + self.positionRightButton = [self generateSelectableButtonWithTitle:@"Right"]; + + self.maskedCornersTitleLabel = [self generateTitleLabelWithText:NSStringFromSelector(@selector(qmui_maskedCorners))]; + self.maskedCornersMinXMinYButton = [self generateSelectableButtonWithTitle:@"MinXMinY"]; + self.maskedCornersMaxXMinYButton = [self generateSelectableButtonWithTitle:@"MaxXMinY"]; + self.maskedCornersMinXMaxYButton = [self generateSelectableButtonWithTitle:@"MinXMaxY"]; + self.maskedCornersMaxXMaxYButton = [self generateSelectableButtonWithTitle:@"MaxXMaxY"]; + + 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]; + + self.colorTitleLabel = [self generateTitleLabelWithText:NSStringFromSelector(@selector(qmui_borderColor))]; + self.colorSegmentedControl = [self generateSegmentedControlWithItems:@[@"Translucence", @"Opacity", @"Black"]]; + + self.dashPatternTitleLabel = [self generateTitleLabelWithText:NSStringFromSelector(@selector(qmui_dashPattern))]; + self.dashPatternWidthTextField = [self generateNumericTextField]; + self.dashPatternSpacingTextField = [self generateNumericTextField]; + + self.dashPhaseTitleLabel = [self generateTitleLabelWithText:NSStringFromSelector(@selector(qmui_dashPhase))]; + self.dashPhaseTextField = [self generateNumericTextField]; + self.dashPhaseTextField.placeholder = @"0"; + + self.keyboardManager = [[QMUIKeyboardManager alloc] initWithDelegate:self]; + + // 默认值的设置 + self.locationSegmentedControl.selectedSegmentIndex = 0; + self.positionTopButton.selected = YES; + 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"; + self.dashPhaseTextField.text = @"0"; + [self fireAllEvents]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + self.keyboardManager.delegateEnabled = YES; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + self.keyboardManager.delegateEnabled = NO; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + if (!self.magnifyingView) { + // 放大镜 + UILongPressGestureRecognizer *longGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleGestureRecognizer:)]; + [self.targetView addGestureRecognizer:longGesture]; + + UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGestureRecognizer:)]; + [self.targetView addGestureRecognizer:panGesture]; + + self.magnifyingView = [[UIView alloc] qmui_initWithSize:self.targetView.bounds.size]; + self.magnifyingView.backgroundColor = UIColorWhite; + self.magnifyingView.layer.cornerRadius = CGRectGetHeight(self.magnifyingView.frame) / 2; + self.magnifyingView.layer.borderWidth = 1; + self.magnifyingView.layer.borderColor = UIColorSeparator.CGColor; + self.magnifyingView.clipsToBounds = YES; + self.magnifyingView.hidden = YES; + + self.magnifyingImageView = [[UIImageView alloc] init]; + [self.magnifyingView addSubview:self.magnifyingImageView]; + } + + if (!self.magnifyingView.superview) { + [self.navigationController.view addSubview:self.magnifyingView]; + } +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + [self.magnifyingView removeFromSuperview]; +} + +- (QMUILabel *)generateTitleLabelWithText:(NSString *)text { + QMUILabel *label = [[QMUILabel alloc] qmui_initWithFont:UIFontMake(14) textColor:UIColor.qd_mainTextColor]; + label.text = text; + [label sizeToFit]; + [self.scrollView addSubview:label]; + return label; +} + +- (QMUISegmentedControl *)generateSegmentedControlWithItems:(NSArray *)items { + QMUISegmentedControl *segmentedControl = [[QMUISegmentedControl alloc] initWithItems:items]; + segmentedControl.tintColor = UIColor.qd_tintColor; + segmentedControl.frame = CGRectSetWidth(segmentedControl.frame, 240);// 统一按照最长的来就行啦 + segmentedControl.transform = CGAffineTransformMakeScale(.8, .8); + segmentedControl.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + [self.scrollView addSubview:segmentedControl]; + [segmentedControl addTarget:self action:@selector(handleSegmentedControlEvent:) forControlEvents:UIControlEventValueChanged]; + return segmentedControl; +} + +- (QMUIButton *)generateSelectableButtonWithTitle:(NSString *)title { + QMUIButton *button = [[QMUIButton alloc] init]; + [button setTitle:title forState:UIControlStateNormal]; + UIImage *selectedImage = [TableViewCellCheckmarkImage qmui_imageResizedInLimitedSize:CGSizeMake(13, 13) resizingMode:QMUIImageResizingModeScaleAspectFit]; + [button setImage:selectedImage forState:UIControlStateSelected]; + [button setImage:selectedImage forState:UIControlStateHighlighted | UIControlStateSelected]; + button.imagePosition = QMUIButtonImagePositionRight; + button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill; + button.titleLabel.font = UIFontMake(12); + [button setTitleColor:UIColor.qd_descriptionTextColor forState:UIControlStateNormal]; + button.highlightedBackgroundColor = TableViewCellSelectedBackgroundColor; + button.qmui_automaticallyAdjustTouchHighlightedInScrollView = YES; + [self.scrollView addSubview:button]; + [button addTarget:self action:@selector(handleSelectableButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; + return button; +} + +- (QMUITextField *)generateNumericTextField { + QMUITextField *textField = [[QMUITextField alloc] qmui_initWithSize:CGSizeMake(44, 32)]; + textField.font = UIFontMake(12); + textField.keyboardType = UIKeyboardTypeDecimalPad; + textField.layer.borderWidth = PixelOne; + textField.layer.borderColor = UIColorSeparator.CGColor; + textField.textAlignment = NSTextAlignmentCenter; + textField.textColor = UIColor.qd_tintColor; + [self.scrollView addSubview:textField]; + [textField addTarget:self action:@selector(handleTextFieldChangedEvent:) forControlEvents:UIControlEventEditingChanged]; + return textField; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + if (!IS_IPAD && IS_LANDSCAPE) { + 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; + } else { + self.targetView.qmui_left = self.targetView.qmui_leftWhenCenterInSuperview; + self.targetView.qmui_top = self.qmui_navigationBarMaxYInViewCoordinator + 32; + CGFloat scrollViewMinY = self.targetView.qmui_bottom + 32; + self.scrollView.frame = CGRectMake(0, scrollViewMinY, self.view.qmui_width, CGRectGetHeight(self.view.bounds) - scrollViewMinY); + self.scrollView.qmui_borderPosition = QMUIViewBorderPositionTop; + } + + CGFloat marginLeft = 16 + self.scrollView.safeAreaInsets.left; + CGFloat marginRight = 16 + self.scrollView.safeAreaInsets.right; + __block CGFloat maxY = 0; + CGFloat defaultLineHeight = 44; + + self.locationTitleLabel.qmui_left = marginLeft; + self.locationTitleLabel.qmui_top = CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.locationTitleLabel.frame)); + self.locationSegmentedControl.center = CGPointMake(CGRectGetWidth(self.scrollView.bounds) - marginRight - CGRectGetWidth(self.locationSegmentedControl.frame) / 2.0, self.locationTitleLabel.center.y); + maxY = defaultLineHeight; + + self.positionTitleLabel.qmui_left = marginLeft; + self.positionTitleLabel.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.positionTitleLabel.frame)); + maxY += defaultLineHeight; + [@[self.positionTopButton, self.positionLeftButton, self.positionBottomButton, self.positionRightButton] enumerateObjectsUsingBlock:^(QMUIButton *obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj.contentEdgeInsets = UIEdgeInsetsMake(0, marginLeft + 18, 0, marginRight); + obj.qmui_top = maxY; + obj.qmui_width = CGRectGetWidth(self.scrollView.bounds); + obj.qmui_height = 32; + maxY = obj.qmui_bottom; + }]; + + self.widthTitleLabel.qmui_left = marginLeft; + self.widthTitleLabel.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.widthTitleLabel.frame)); + self.widthTextField.qmui_right = CGRectGetWidth(self.scrollView.bounds) - marginRight; + 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); + maxY += defaultLineHeight; + + self.cornerRadiusTitleLabel.qmui_left = marginLeft; + self.cornerRadiusTitleLabel.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.cornerRadiusTitleLabel.frame)); + self.cornerRadiusTextField.qmui_right = CGRectGetWidth(self.scrollView.bounds) - marginRight; + self.cornerRadiusTextField.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.cornerRadiusTextField.frame)); + maxY += defaultLineHeight; + + self.maskedCornersTitleLabel.qmui_left = marginLeft; + self.maskedCornersTitleLabel.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.maskedCornersTitleLabel.frame)); + maxY += defaultLineHeight; + [@[self.maskedCornersMinXMinYButton, self.maskedCornersMaxXMinYButton, self.maskedCornersMinXMaxYButton, self.maskedCornersMaxXMaxYButton] enumerateObjectsUsingBlock:^(QMUIButton *obj, NSUInteger idx, BOOL * _Nonnull stop) { + obj.contentEdgeInsets = UIEdgeInsetsMake(0, marginLeft + 18, 0, marginRight); + obj.qmui_top = maxY; + obj.qmui_width = CGRectGetWidth(self.scrollView.bounds); + obj.qmui_height = 32; + maxY = obj.qmui_bottom; + }]; + + self.dashPatternTitleLabel.qmui_left = marginLeft; + self.dashPatternTitleLabel.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.dashPatternTitleLabel.frame)); + self.dashPatternSpacingTextField.qmui_right = CGRectGetWidth(self.scrollView.bounds) - marginRight; + self.dashPatternSpacingTextField.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.dashPatternSpacingTextField.frame)); + self.dashPatternWidthTextField.qmui_right = self.dashPatternSpacingTextField.qmui_left - 8; + self.dashPatternWidthTextField.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.dashPatternWidthTextField.frame)); + maxY += defaultLineHeight; + + self.dashPhaseTitleLabel.qmui_left = marginLeft; + self.dashPhaseTitleLabel.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.dashPhaseTitleLabel.frame)); + self.dashPhaseTextField.qmui_right = CGRectGetWidth(self.scrollView.bounds) - marginRight; + self.dashPhaseTextField.qmui_top = maxY + CGFloatGetCenter(defaultLineHeight, CGRectGetHeight(self.dashPhaseTextField.frame)); + maxY += defaultLineHeight; + + self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.scrollView.bounds), maxY); +} + +- (void)handleSegmentedControlEvent:(QMUISegmentedControl *)segmentedControl { + if (segmentedControl == self.locationSegmentedControl) { + self.targetView.qmui_borderLocation = segmentedControl.selectedSegmentIndex; + } else if (segmentedControl == self.colorSegmentedControl) { + UIColor *borderColor = nil; + switch (segmentedControl.selectedSegmentIndex) { + case 0: + borderColor = [UIColor.qd_tintColor colorWithAlphaComponent:.5]; + break; + case 1: + borderColor = UIColor.qd_tintColor; + break; + case 2: + borderColor = [UIColor blackColor]; + default: + break; + } + self.targetView.qmui_borderColor = borderColor; + } +} + +- (void)handleSelectableButtonEvent:(QMUIButton *)button { + button.selected = !button.selected; + if (button == self.positionTopButton || button == self.positionLeftButton || button == self.positionBottomButton || button == self.positionRightButton) { + [self updateBorderPosition]; + } else { + [self updateMaskedCorners]; + } +} + +- (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) { + self.targetView.layer.cornerRadius = [textField.text doubleValue]; + } else { + CGFloat width = [self.dashPatternWidthTextField.text doubleValue]; + CGFloat spacing = [self.dashPatternSpacingTextField.text doubleValue]; + if (width > 0 || spacing > 0) { + self.targetView.qmui_dashPattern = @[@(width), @(spacing)]; + } else { + self.targetView.qmui_dashPattern = nil; + } + } +} + +- (void)updateBorderPosition { + QMUIViewBorderPosition position = QMUIViewBorderPositionNone; + if (self.positionTopButton.selected) { + position |= QMUIViewBorderPositionTop; + } + if (self.positionLeftButton.selected) { + position |= QMUIViewBorderPositionLeft; + } + if (self.positionBottomButton.selected) { + position |= QMUIViewBorderPositionBottom; + } + if (self.positionRightButton.selected) { + position |= QMUIViewBorderPositionRight; + } + self.targetView.qmui_borderPosition = position; +} + +- (void)updateMaskedCorners { + QMUICornerMask cornerMask = 0; + if (self.maskedCornersMinXMinYButton.isSelected) { + cornerMask |= QMUILayerMinXMinYCorner; + } + if (self.maskedCornersMaxXMinYButton.isSelected) { + cornerMask |= QMUILayerMaxXMinYCorner; + } + if (self.maskedCornersMinXMaxYButton.isSelected) { + cornerMask |= QMUILayerMinXMaxYCorner; + } + if (self.maskedCornersMaxXMaxYButton.isSelected) { + cornerMask |= QMUILayerMaxXMaxYCorner; + } + if (cornerMask == 0) { + // 默认值 + cornerMask = QMUILayerAllCorner; + } + BeginIgnoreDeprecatedWarning + self.targetView.layer.qmui_maskedCorners = cornerMask; + EndIgnoreDeprecatedWarning +} + +- (void)fireAllEvents { + [@[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.insetsTextField, self.dashPatternWidthTextField, self.dashPatternSpacingTextField, self.dashPhaseTextField] enumerateObjectsUsingBlock:^(UIControl *obj, NSUInteger idx, BOOL * _Nonnull stop) { + [obj sendActionsForControlEvents:UIControlEventEditingChanged]; + }]; + [self updateBorderPosition]; +} + +- (void)handleGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer { + + CGFloat offset = self.targetView.qmui_borderWidth + 5; + CGFloat scale = 8; + + if (gestureRecognizer.state == UIGestureRecognizerStateEnded || gestureRecognizer.state == UIGestureRecognizerStateFailed || gestureRecognizer.state == UIGestureRecognizerStateCancelled) { + self.magnifyingView.hidden = YES; + return; + } + + if (gestureRecognizer.state == UIGestureRecognizerStateBegan) { + + CGSize size = self.targetView.frame.size; + size.width += offset * 2; + size.height += offset * 2; + + UIImage *snapshotImage = [UIImage qmui_imageWithSize:size opaque:NO scale:ScreenScale * scale actions:^(CGContextRef contextRef) { + CGContextTranslateCTM(contextRef, offset, offset); + // 当你使用了 qmui_maskedCorners 并且只指定某几个角为圆角时,用以下这种方式绘制出来的图片会让直角也绘制为圆角,这是 iOS 11 及以上的系统 bug,暂不处理 + [self.targetView.layer renderInContext:contextRef]; + }]; + + self.magnifyingImageView.image = snapshotImage; + [self.magnifyingImageView sizeToFit]; + self.magnifyingImageView.transform = CGAffineTransformMakeScale(scale, scale); + + self.magnifyingView.hidden = NO; + } + + CGPoint locationInView = [gestureRecognizer locationInView:self.magnifyingView.superview]; + locationInView.x = MIN(MAX(locationInView.x, CGRectGetMinX(self.targetView.frame) - self.targetView.qmui_borderWidth), CGRectGetMaxX(self.targetView.frame) + self.targetView.qmui_borderWidth); + locationInView.y = MIN(MAX(locationInView.y, CGRectGetMinY(self.targetView.frame) - self.targetView.qmui_borderWidth), CGRectGetMaxY(self.targetView.frame) + self.targetView.qmui_borderWidth); + self.magnifyingView.center = locationInView; + + CGPoint locationInTarget = [self.targetView convertPoint:locationInView fromView:self.magnifyingView.superview]; + self.magnifyingImageView.center = CGPointMake(CGRectGetWidth(self.magnifyingView.bounds) / 2 + CGRectGetWidth(self.magnifyingImageView.frame) / 2 - offset * scale - locationInTarget.x * scale, CGRectGetHeight(self.magnifyingView.bounds) / 2 + CGRectGetHeight(self.magnifyingImageView.frame) / 2 - offset * scale - locationInTarget.y * scale); + + // 避免手指挡住放大镜 + CGFloat avoidFingerX = -60; + CGFloat avoidFingerY = -100; + CGFloat magnifyingViewMinX = CGRectGetMinX(self.magnifyingView.frame); + CGFloat magnifyingViewMinY = CGRectGetMinY(self.magnifyingView.frame); + 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; + } + } else { + magnifyingViewMinY += avoidFingerY; + } + self.magnifyingView.frame = CGRectSetXY(self.magnifyingView.frame, magnifyingViewMinX, magnifyingViewMinY); +} + +#pragma mark - + +- (void)keyboardWillChangeFrameWithUserInfo:(QMUIKeyboardUserInfo *)keyboardUserInfo { + QMUITextField *textField = (QMUITextField *)keyboardUserInfo.targetResponder; + CGRect textFieldRect = [self.view convertRect:textField.frame fromView:textField.superview]; + 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.safeAreaInsets.bottom : 0)); + if (CGRectGetMaxY(textFieldRect) < CGRectGetHeight(self.view.bounds) - keyboardHeight) { + return; + } + + CGFloat scrollDistance = CGRectGetMaxY(textFieldRect) - (CGRectGetHeight(self.view.bounds) - keyboardHeight); + [self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, self.scrollView.contentOffset.y + scrollDistance)]; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewDebugViewController.h b/qmuidemo/Modules/Demos/UIKit/QDUIViewDebugViewController.h new file mode 100644 index 00000000..620c8ef3 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDUIViewDebugViewController.h @@ -0,0 +1,13 @@ +// +// QDUIViewDebugViewController.h +// qmuidemo +// +// Created by QMUI Team on 2017/8/8. +// Copyright © 2017年 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +@interface QDUIViewDebugViewController : QDCommonViewController + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewDebugViewController.m b/qmuidemo/Modules/Demos/UIKit/QDUIViewDebugViewController.m new file mode 100644 index 00000000..21486485 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDUIViewDebugViewController.m @@ -0,0 +1,59 @@ +// +// QDUIViewDebugViewController.m +// qmuidemo +// +// Created by QMUI Team on 2017/8/8. +// Copyright © 2017年 QMUI Team. All rights reserved. +// + +#import "QDUIViewDebugViewController.h" + +@interface QDUIViewDebugViewController () + +@property(nonatomic, strong) UILabel *descriptionLabel; +@property(nonatomic, strong) UIView *parentView; +@property(nonatomic, strong) UIView *subview1; +@property(nonatomic, strong) UIView *subview2; +@end + +@implementation QDUIViewDebugViewController + +- (void)initSubviews { + [super initSubviews]; + + 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]; + }]; + + self.descriptionLabel = [[UILabel alloc] init]; + self.descriptionLabel.attributedText = attributedString; + self.descriptionLabel.numberOfLines = 0; + [self.view addSubview:self.descriptionLabel]; + + self.parentView = [[UIView alloc] init]; + self.parentView.qmui_shouldShowDebugColor = YES;// 打开 debug 背景色 + self.parentView.qmui_needsDifferentDebugColor = YES;// 让背景颜色随机 + [self.view addSubview:self.parentView]; + + self.subview1 = [[UIView alloc] qmui_initWithSize:CGSizeMake(50, 50)]; + [self.parentView addSubview:self.subview1]; + + self.subview2 = [[UIView alloc] qmui_initWithSize:CGSizeMake(160, 90)]; + [self.parentView addSubview:self.subview2]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + 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; + self.subview1.qmui_top = 24; + self.subview2.qmui_left = self.subview1.qmui_left; + self.subview2.qmui_top = self.subview1.qmui_bottom + 24; + self.parentView.frame = CGRectMake(padding.left, CGRectGetMaxY(self.descriptionLabel.frame) + 24, CGRectGetWidth(self.descriptionLabel.frame), self.subview2.qmui_bottom + 24); +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewLayoutViewController.h b/qmuidemo/Modules/Demos/UIKit/QDUIViewLayoutViewController.h new file mode 100644 index 00000000..2559b237 --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDUIViewLayoutViewController.h @@ -0,0 +1,13 @@ +// +// QDUIViewLayoutViewController.h +// qmuidemo +// +// Created by QMUI Team on 2017/8/9. +// Copyright © 2017年 QMUI Team. All rights reserved. +// + +#import "QDCommonViewController.h" + +@interface QDUIViewLayoutViewController : QDCommonViewController + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewLayoutViewController.m b/qmuidemo/Modules/Demos/UIKit/QDUIViewLayoutViewController.m new file mode 100644 index 00000000..e8e6201e --- /dev/null +++ b/qmuidemo/Modules/Demos/UIKit/QDUIViewLayoutViewController.m @@ -0,0 +1,58 @@ +// +// QDUIViewLayoutViewController.m +// qmuidemo +// +// Created by QMUI Team on 2017/8/9. +// Copyright © 2017年 QMUI Team. All rights reserved. +// + +#import "QDUIViewLayoutViewController.h" + +@interface QDUIViewLayoutViewController () + +@property(nonatomic, strong) UIView *view1; +@property(nonatomic, strong) UIView *view2; +@property(nonatomic, strong) UIView *view3; +@end + +@implementation QDUIViewLayoutViewController + +- (void)initSubviews { + [super initSubviews]; + self.view1 = [[UIView alloc] init]; + self.view1.backgroundColor = [QDCommonUI randomThemeColor]; + [self.view addSubview:self.view1]; + + self.view2 = [[UIView alloc] init]; + self.view2.backgroundColor = [QDCommonUI randomThemeColor]; + [self.view addSubview:self.view2]; + + self.view3 = [[UIView alloc] init]; + self.view3.backgroundColor = [QDCommonUI randomThemeColor]; + [self.view addSubview:self.view3]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + 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.view1.qmui_left = padding.left; + self.view1.qmui_top = padding.top; + self.view1.qmui_width = contentWidth; + self.view1.qmui_height = 40; + + self.view2.qmui_left = self.view1.qmui_left; + self.view2.qmui_top = self.view1.qmui_bottom + 24; + self.view2.qmui_width = self.view1.qmui_width / 2; + self.view2.qmui_height = 40; + + self.view3.qmui_width = self.view1.qmui_width / 2; + self.view3.qmui_height = 40; + self.view3.qmui_top = self.view2.qmui_bottom + 24; + self.view3.qmui_right = self.view1.qmui_right; +} + +@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.h b/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.h deleted file mode 100644 index 4e556177..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// QDUIViewQMUIViewController.h -// qmuidemo -// -// Created by zhoonchen on 2016/10/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QDCommonViewController.h" - -@interface QDUIViewQMUIViewController : QDCommonViewController - -@end diff --git a/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.m b/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.m deleted file mode 100644 index 9111d5ac..00000000 --- a/qmuidemo/Modules/Demos/UIKit/QDUIViewQMUIViewController.m +++ /dev/null @@ -1,101 +0,0 @@ -// -// QDUIViewQMUIViewController.m -// qmuidemo -// -// Created by zhoonchen on 2016/10/11. -// Copyright © 2016年 QMUI Team. All rights reserved. -// - -#import "QDUIViewQMUIViewController.h" - -@interface QDUIViewQMUIViewController () - -@property(nonatomic, strong) UIScrollView *contentScrollView; - -@property(nonatomic, strong) UILabel *descriptionLabel1; -@property(nonatomic, strong) UILabel *descriptionLabel2; - -@property(nonatomic, strong) UIView *contentView; -@property(nonatomic, strong) UILabel *contentLabel1; -@property(nonatomic, strong) UILabel *contentLabel2; -@property(nonatomic, strong) UILabel *contentLabel3; - -@end - -@implementation QDUIViewQMUIViewController - -- (void)initSubviews { - [super initSubviews]; - - _contentScrollView = [[UIScrollView alloc] init]; - self.contentScrollView.showsVerticalScrollIndicator = NO; - self.contentScrollView.showsHorizontalScrollIndicator = NO; - [self.view addSubview:self.contentScrollView]; - - NSMutableAttributedString *describeAttributedString = [[NSMutableAttributedString alloc] initWithString:@"通过 qmui_borderPosition、qmui_borderWidth 和 qmui_borderColor 给任意的 UIView 加边框" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorGray1, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:22]}]; - NSDictionary *codeAttributes = CodeAttributes(16); - [describeAttributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { - [describeAttributedString addAttributes:codeAttributes range:codeRange]; - }]; - - _descriptionLabel1 = [[UILabel alloc] init]; - self.descriptionLabel1.attributedText = describeAttributedString; - self.descriptionLabel1.numberOfLines = 0; - self.descriptionLabel1.qmui_borderPosition = QMUIBorderViewPositionTop | QMUIBorderViewPositionBottom; - [self.contentScrollView addSubview:self.descriptionLabel1]; - - describeAttributedString = [[NSMutableAttributedString alloc] initWithString:@"通过 qmui_shouldShowDebugColor 让 UIView 以及其所有的 subviews 都加上一个背景色,方便查看其布局情况" attributes:@{NSFontAttributeName: UIFontMake(16), NSForegroundColorAttributeName: UIColorGray1, NSParagraphStyleAttributeName: [NSMutableParagraphStyle qmui_paragraphStyleWithLineHeight:22]}]; - codeAttributes = CodeAttributes(16); - [describeAttributedString.string enumerateCodeStringUsingBlock:^(NSString *codeString, NSRange codeRange) { - [describeAttributedString addAttributes:codeAttributes range:codeRange]; - }]; - - _descriptionLabel2 = [[UILabel alloc] init]; - self.descriptionLabel2.attributedText = describeAttributedString; - self.descriptionLabel2.numberOfLines = 0; - [self.contentScrollView addSubview:self.descriptionLabel2]; - - _contentView = [[UIView alloc] init]; - self.contentView.backgroundColor = UIColorGrayLighten; - self.contentView.qmui_shouldShowDebugColor = YES; - self.contentView.qmui_needsDifferentDebugColor = YES; - [self.contentScrollView addSubview:self.contentView]; - - _contentLabel1 = [[UILabel alloc] initWithFont:UIFontMake(16) textColor:UIColorWhite]; - [self.contentView addSubview:self.contentLabel1]; - - _contentLabel2 = [[UILabel alloc] initWithFont:UIFontMake(16) textColor:UIColorWhite]; - [self.contentView addSubview:self.contentLabel2]; - - _contentLabel3 = [[UILabel alloc] initWithFont:UIFontMake(16) textColor:UIColorWhite]; - [self.contentView addSubview:self.contentLabel3]; - - CGFloat labelMinY = 16; - for (NSInteger i = 0; i < self.contentView.subviews.count; i ++) { - UILabel *label = (UILabel *)self.contentView.subviews[i]; - label.text = [NSString stringWithFormat:@"subview%@", @(i + 1)]; - [label sizeToFit]; - label.frame = CGRectSetXY(label.frame, 16 * (i + 1), labelMinY); - labelMinY = CGRectGetMaxY(label.frame) + 16; - } -} - -- (void)viewDidLayoutSubviews { - [super viewDidLayoutSubviews]; - - UIEdgeInsets padding = UIEdgeInsetsMake(24, 24, 24, 24); - CGFloat contentWidth = CGRectGetWidth(self.view.bounds) - UIEdgeInsetsGetHorizontalValue(padding); - - CGSize descriptionLabel1Size = [self.descriptionLabel1 sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)]; - self.descriptionLabel1.frame = CGRectFlatMake(padding.left, padding.top, contentWidth, descriptionLabel1Size.height + 20); - - CGSize descriptionLabel2Size = [self.descriptionLabel2 sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)]; - self.descriptionLabel2.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.descriptionLabel1.frame) + 24, contentWidth, descriptionLabel2Size.height); - - self.contentView.frame = CGRectFlatMake(padding.left, CGRectGetMaxY(self.descriptionLabel2.frame) + 24, contentWidth, 120); - - self.contentScrollView.frame = self.view.bounds; - self.contentScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.contentScrollView.bounds), CGRectGetMaxY(self.contentView.frame) + 24); -} - -@end diff --git a/qmuidemo/PrefixHeader.pch b/qmuidemo/PrefixHeader.pch index 5105fe41..5edb56e0 100644 --- a/qmuidemo/PrefixHeader.pch +++ b/qmuidemo/PrefixHeader.pch @@ -2,7 +2,7 @@ // PrefixHeader.pch // qmuidemo // -// Created by ZhoonChen on 15/4/13. +// Created by QMUI Team on 15/4/13. // Copyright (c) 2015年 QMUI Team. All rights reserved. // @@ -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/animatedImage.gif b/qmuidemo/Resources/images/animatedImage.gif new file mode 100644 index 00000000..ea4ca3d9 Binary files /dev/null and b/qmuidemo/Resources/images/animatedImage.gif differ 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 new file mode 100644 index 00000000..f97745b5 --- /dev/null +++ b/qmuidemo/en.lproj/Info.plist @@ -0,0 +1,73 @@ + + + + + CFBundleDevelopmentRegion + en_US + CFBundleDisplayName + $(BUNDLE_DISPLAY_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 2.7.3 + CFBundleSignature + ???? + CFBundleVersion + 17 + LSRequiresIPhoneOS + + NSCameraUsageDescription + $(PRODUCT_NAME) wants to use your camera + NSMicrophoneUsageDescription + $(PRODUCT_NAME) wants to use your microphone + NSPhotoLibraryAddUsageDescription + $(PRODUCT_NAME) wants to add photos to your photo library + NSPhotoLibraryUsageDescription + $(PRODUCT_NAME) wants to use your photo library + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneDelegateClassName + AppDelegate + UISceneConfigurationName + Default Configuration + + + + + + diff --git a/qmuidemo/en.lproj/LaunchScreen.strings b/qmuidemo/en.lproj/LaunchScreen.strings new file mode 100644 index 00000000..6940ef37 --- /dev/null +++ b/qmuidemo/en.lproj/LaunchScreen.strings @@ -0,0 +1,3 @@ + +/* Class = "UILabel"; text = "© 2018 QMUI Team All Rights Reserved."; ObjectID = "v3q-1s-6u4"; */ +"v3q-1s-6u4.text" = "© 2024 QMUI Team All Rights Reserved."; diff --git a/qmuidemo/en.lproj/Localizable.strings b/qmuidemo/en.lproj/Localizable.strings new file mode 100644 index 00000000..b736dcbd --- /dev/null +++ b/qmuidemo/en.lproj/Localizable.strings @@ -0,0 +1,11 @@ +/* + Localizable.strings + qmuidemo + + Created by QMUI Team on 07/26/2018. + Copyright © 2018 QMUI Team. All rights reserved. +*/ +"QMUIButton_Normal_Button_Title" = "Button with highlighted background color"; +"QMUIButton_Bordered_Button_Title" = "Button with highlighted border"; +"QMUIButton_Image_Position_Button_Title_1" = "Text below image"; +"QMUIButton_Image_Position_Button_Title_2" = "Text above image"; diff --git a/qmuidemo/main.m b/qmuidemo/main.m index 748c23cd..3621d763 100644 --- a/qmuidemo/main.m +++ b/qmuidemo/main.m @@ -2,7 +2,7 @@ // main.m // qmuidemo // -// Created by ZhoonChen on 15/4/13. +// Created by QMUI Team on 15/4/13. // Copyright (c) 2015年 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 new file mode 100644 index 00000000..21c8d138 --- /dev/null +++ b/qmuidemo/zh-Hans.lproj/Info.plist @@ -0,0 +1,73 @@ + + + + + CFBundleDevelopmentRegion + en_US + CFBundleDisplayName + $(BUNDLE_DISPLAY_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 2.7.3 + CFBundleSignature + ???? + CFBundleVersion + 17 + LSRequiresIPhoneOS + + NSCameraUsageDescription + $(PRODUCT_NAME) 想要使用摄像头 + NSMicrophoneUsageDescription + $(PRODUCT_NAME) 想要使用麦克风 + NSPhotoLibraryAddUsageDescription + $(PRODUCT_NAME) 想要访问照片库 + NSPhotoLibraryUsageDescription + $(PRODUCT_NAME) 想要访问照片库 + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + 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 new file mode 100644 index 00000000..6940ef37 --- /dev/null +++ b/qmuidemo/zh-Hans.lproj/LaunchScreen.strings @@ -0,0 +1,3 @@ + +/* Class = "UILabel"; text = "© 2018 QMUI Team All Rights Reserved."; ObjectID = "v3q-1s-6u4"; */ +"v3q-1s-6u4.text" = "© 2024 QMUI Team All Rights Reserved."; diff --git a/qmuidemo/zh-Hans.lproj/Localizable.strings b/qmuidemo/zh-Hans.lproj/Localizable.strings new file mode 100644 index 00000000..a26036c1 --- /dev/null +++ b/qmuidemo/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,11 @@ +/* + Localizable.strings + qmuidemo + + Created by QMUI Team on 07/26/2018. + Copyright © 2018 QMUI Team. All rights reserved. +*/ +"QMUIButton_Normal_Button_Title" = "按钮,支持高亮背景色"; +"QMUIButton_Bordered_Button_Title" = "边框支持高亮的按钮"; +"QMUIButton_Image_Position_Button_Title_1" = "图片在上方的按钮"; +"QMUIButton_Image_Position_Button_Title_2" = "图片在下方的按钮";