Repository: Tencent/QMUI_iOS Branch: master Commit: 4dca2347dcb5 Files: 386 Total size: 3.0 MB Directory structure: gitextract_c6ylhudb/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── ----.md │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.TXT ├── QMUIConfigurationTemplate/ │ ├── QMUIConfigurationTemplate.h │ └── QMUIConfigurationTemplate.m ├── QMUIKit/ │ ├── Info.plist │ ├── PrivacyInfo.xcprivacy │ ├── QMUIComponents/ │ │ ├── AssetLibrary/ │ │ │ ├── QMUIAsset.h │ │ │ ├── QMUIAsset.m │ │ │ ├── QMUIAssetsGroup.h │ │ │ ├── QMUIAssetsGroup.m │ │ │ ├── QMUIAssetsManager.h │ │ │ └── QMUIAssetsManager.m │ │ ├── CAAnimation+QMUI.h │ │ ├── CAAnimation+QMUI.m │ │ ├── CALayer+QMUIViewAnimation.h │ │ ├── CALayer+QMUIViewAnimation.m │ │ ├── ImagePickerLibrary/ │ │ │ ├── QMUIAlbumViewController.h │ │ │ ├── QMUIAlbumViewController.m │ │ │ ├── QMUIImagePickerCollectionViewCell.h │ │ │ ├── QMUIImagePickerCollectionViewCell.m │ │ │ ├── QMUIImagePickerHelper.h │ │ │ ├── QMUIImagePickerHelper.m │ │ │ ├── QMUIImagePickerPreviewViewController.h │ │ │ ├── QMUIImagePickerPreviewViewController.m │ │ │ ├── QMUIImagePickerViewController.h │ │ │ └── QMUIImagePickerViewController.m │ │ ├── NavigationBarTransition/ │ │ │ ├── UINavigationBar+Transition.h │ │ │ ├── UINavigationBar+Transition.m │ │ │ ├── UINavigationController+NavigationBarTransition.h │ │ │ └── UINavigationController+NavigationBarTransition.m │ │ ├── QMUIAlertController.h │ │ ├── QMUIAlertController.m │ │ ├── QMUIAnimation/ │ │ │ ├── QMUIAnimationHelper.h │ │ │ ├── QMUIAnimationHelper.m │ │ │ ├── QMUIDisplayLinkAnimation.h │ │ │ ├── QMUIDisplayLinkAnimation.m │ │ │ └── QMUIEasings.h │ │ ├── QMUIAppearance.h │ │ ├── QMUIAppearance.m │ │ ├── QMUIBadge/ │ │ │ ├── QMUIBadgeLabel.h │ │ │ ├── QMUIBadgeLabel.m │ │ │ ├── QMUIBadgeProtocol.h │ │ │ ├── UIBarItem+QMUIBadge.h │ │ │ ├── UIBarItem+QMUIBadge.m │ │ │ ├── UIView+QMUIBadge.h │ │ │ └── UIView+QMUIBadge.m │ │ ├── QMUIButton/ │ │ │ ├── QMUIButton.h │ │ │ ├── QMUIButton.m │ │ │ ├── QMUINavigationButton.h │ │ │ ├── QMUINavigationButton.m │ │ │ ├── QMUIToolbarButton.h │ │ │ └── QMUIToolbarButton.m │ │ ├── QMUICellHeightCache.h │ │ ├── QMUICellHeightCache.m │ │ ├── QMUICellHeightKeyCache/ │ │ │ ├── QMUICellHeightKeyCache.h │ │ │ ├── QMUICellHeightKeyCache.m │ │ │ ├── UITableView+QMUICellHeightKeyCache.h │ │ │ └── UITableView+QMUICellHeightKeyCache.m │ │ ├── QMUICellSizeKeyCache/ │ │ │ ├── QMUICellSizeKeyCache.h │ │ │ ├── QMUICellSizeKeyCache.m │ │ │ ├── UICollectionView+QMUICellSizeKeyCache.h │ │ │ └── UICollectionView+QMUICellSizeKeyCache.m │ │ ├── QMUICheckbox.h │ │ ├── QMUICheckbox.m │ │ ├── QMUICollectionViewPagingLayout.h │ │ ├── QMUICollectionViewPagingLayout.m │ │ ├── QMUIConsole/ │ │ │ ├── QMUIConsole.h │ │ │ ├── QMUIConsole.m │ │ │ ├── QMUIConsoleToolbar.h │ │ │ ├── QMUIConsoleToolbar.m │ │ │ ├── QMUIConsoleViewController.h │ │ │ ├── QMUIConsoleViewController.m │ │ │ ├── QMUILog+QMUIConsole.h │ │ │ └── QMUILog+QMUIConsole.m │ │ ├── QMUIDialogViewController.h │ │ ├── QMUIDialogViewController.m │ │ ├── QMUIEmotionInputManager.h │ │ ├── QMUIEmotionInputManager.m │ │ ├── QMUIEmotionView.h │ │ ├── QMUIEmotionView.m │ │ ├── QMUIEmptyView.h │ │ ├── QMUIEmptyView.m │ │ ├── QMUIFloatLayoutView.h │ │ ├── QMUIFloatLayoutView.m │ │ ├── QMUIGridView.h │ │ ├── QMUIGridView.m │ │ ├── QMUIImagePreviewView/ │ │ │ ├── QMUIImagePreviewView.h │ │ │ ├── QMUIImagePreviewView.m │ │ │ ├── QMUIImagePreviewViewController.h │ │ │ ├── QMUIImagePreviewViewController.m │ │ │ ├── QMUIImagePreviewViewTransitionAnimator.h │ │ │ └── QMUIImagePreviewViewTransitionAnimator.m │ │ ├── QMUIKeyboardManager.h │ │ ├── QMUIKeyboardManager.m │ │ ├── QMUILabel.h │ │ ├── QMUILabel.m │ │ ├── QMUILayouter/ │ │ │ ├── QMUILayouter.h │ │ │ ├── QMUILayouterItem.h │ │ │ ├── QMUILayouterItem.m │ │ │ ├── QMUILayouterLinearHorizontal.h │ │ │ ├── QMUILayouterLinearHorizontal.m │ │ │ ├── QMUILayouterLinearVertical.h │ │ │ └── QMUILayouterLinearVertical.m │ │ ├── QMUILog/ │ │ │ ├── QMUILog.h │ │ │ ├── QMUILogItem.h │ │ │ ├── QMUILogItem.m │ │ │ ├── QMUILogNameManager.h │ │ │ ├── QMUILogNameManager.m │ │ │ ├── QMUILogger.h │ │ │ └── QMUILogger.m │ │ ├── QMUILogManagerViewController.h │ │ ├── QMUILogManagerViewController.m │ │ ├── QMUILogger+QMUIConfigurationTemplate.h │ │ ├── QMUILogger+QMUIConfigurationTemplate.m │ │ ├── QMUIMarqueeLabel.h │ │ ├── QMUIMarqueeLabel.m │ │ ├── QMUIModalPresentationViewController.h │ │ ├── QMUIModalPresentationViewController.m │ │ ├── QMUIMoreOperationController.h │ │ ├── QMUIMoreOperationController.m │ │ ├── QMUIMultipleDelegates/ │ │ │ ├── NSObject+QMUIMultipleDelegates.h │ │ │ ├── NSObject+QMUIMultipleDelegates.m │ │ │ ├── QMUIMultipleDelegates.h │ │ │ └── QMUIMultipleDelegates.m │ │ ├── QMUINavigationTitleView.h │ │ ├── QMUINavigationTitleView.m │ │ ├── QMUIOrderedDictionary.h │ │ ├── QMUIOrderedDictionary.m │ │ ├── QMUIPieProgressView.h │ │ ├── QMUIPieProgressView.m │ │ ├── QMUIPopupContainerView.h │ │ ├── QMUIPopupContainerView.m │ │ ├── QMUIPopupMenuView/ │ │ │ ├── QMUIPopupMenuItem.h │ │ │ ├── QMUIPopupMenuItem.m │ │ │ ├── QMUIPopupMenuItemView.h │ │ │ ├── QMUIPopupMenuItemView.m │ │ │ ├── QMUIPopupMenuItemViewProtocol.h │ │ │ ├── QMUIPopupMenuView.h │ │ │ └── QMUIPopupMenuView.m │ │ ├── QMUIScrollAnimator/ │ │ │ ├── QMUINavigationBarScrollingAnimator.h │ │ │ ├── QMUINavigationBarScrollingAnimator.m │ │ │ ├── QMUINavigationBarScrollingSnapAnimator.h │ │ │ ├── QMUINavigationBarScrollingSnapAnimator.m │ │ │ ├── QMUIScrollAnimator.h │ │ │ └── QMUIScrollAnimator.m │ │ ├── QMUISearchBar.h │ │ ├── QMUISearchBar.m │ │ ├── QMUISearchController.h │ │ ├── QMUISearchController.m │ │ ├── QMUISegmentedControl.h │ │ ├── QMUISegmentedControl.m │ │ ├── QMUISheetPresentation/ │ │ │ ├── QMUISheetPresentationNavigationBar.h │ │ │ ├── QMUISheetPresentationNavigationBar.m │ │ │ ├── QMUISheetPresentationSupports.h │ │ │ └── QMUISheetPresentationSupports.m │ │ ├── QMUITableView.h │ │ ├── QMUITableView.m │ │ ├── QMUITableViewCell.h │ │ ├── QMUITableViewCell.m │ │ ├── QMUITableViewHeaderFooterView.h │ │ ├── QMUITableViewHeaderFooterView.m │ │ ├── QMUITableViewProtocols.h │ │ ├── QMUITestView.h │ │ ├── QMUITestView.m │ │ ├── QMUITextField.h │ │ ├── QMUITextField.m │ │ ├── QMUITextView.h │ │ ├── QMUITextView.m │ │ ├── QMUITheme/ │ │ │ ├── QMUITheme.h │ │ │ ├── QMUIThemeManager.h │ │ │ ├── QMUIThemeManager.m │ │ │ ├── QMUIThemeManagerCenter.h │ │ │ ├── QMUIThemeManagerCenter.m │ │ │ ├── QMUIThemePrivate.h │ │ │ ├── QMUIThemePrivate.m │ │ │ ├── UIColor+QMUITheme.h │ │ │ ├── UIColor+QMUITheme.m │ │ │ ├── UIImage+QMUITheme.h │ │ │ ├── UIImage+QMUITheme.m │ │ │ ├── UIView+QMUITheme.h │ │ │ ├── UIView+QMUITheme.m │ │ │ ├── UIViewController+QMUITheme.h │ │ │ ├── UIViewController+QMUITheme.m │ │ │ ├── UIVisualEffect+QMUITheme.h │ │ │ └── UIVisualEffect+QMUITheme.m │ │ ├── QMUITips.h │ │ ├── QMUITips.m │ │ ├── QMUIWeakObjectContainer.h │ │ ├── QMUIWeakObjectContainer.m │ │ ├── QMUIWindowSizeMonitor.h │ │ ├── QMUIWindowSizeMonitor.m │ │ ├── QMUIZoomImageView.h │ │ ├── QMUIZoomImageView.m │ │ ├── StaticTableView/ │ │ │ ├── QMUIStaticTableViewCellData.h │ │ │ ├── QMUIStaticTableViewCellData.m │ │ │ ├── QMUIStaticTableViewCellDataSource.h │ │ │ ├── QMUIStaticTableViewCellDataSource.m │ │ │ ├── UITableView+QMUIStaticCell.h │ │ │ └── UITableView+QMUIStaticCell.m │ │ └── ToastView/ │ │ ├── QMUIToastAnimator.h │ │ ├── QMUIToastAnimator.m │ │ ├── QMUIToastBackgroundView.h │ │ ├── QMUIToastBackgroundView.m │ │ ├── QMUIToastContentView.h │ │ ├── QMUIToastContentView.m │ │ ├── QMUIToastView.h │ │ └── QMUIToastView.m │ ├── QMUICore/ │ │ ├── QMUICommonDefines.h │ │ ├── QMUIConfiguration.h │ │ ├── QMUIConfiguration.m │ │ ├── QMUIConfigurationMacros.h │ │ ├── QMUICore.h │ │ ├── QMUIHelper.h │ │ ├── QMUIHelper.m │ │ ├── QMUILab.h │ │ ├── QMUIRuntime.h │ │ └── QMUIRuntime.m │ ├── QMUIKit.h │ ├── QMUIMainFrame/ │ │ ├── QMUICommonTableViewController.h │ │ ├── QMUICommonTableViewController.m │ │ ├── QMUICommonViewController.h │ │ ├── QMUICommonViewController.m │ │ ├── QMUINavigationController.h │ │ ├── QMUINavigationController.m │ │ ├── QMUITabBarViewController.h │ │ └── QMUITabBarViewController.m │ ├── QMUIResources/ │ │ └── Images.xcassets/ │ │ ├── Contents.json │ │ ├── QMUI_checkbox16.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_checkbox16_checked.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_checkbox16_disabled.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_checkbox16_indeterminate.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_console_clear.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_console_filter.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_console_filter_selected.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_console_logo.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_emotion_delete.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_hiddenAlbum.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_icloud_download_fault.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_pickerImage_checkbox.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_pickerImage_checkbox_checked.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_pickerImage_favorite.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_pickerImage_video_mark.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_previewImage_checkbox.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_previewImage_checkbox_checked.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_tips_done.imageset/ │ │ │ └── Contents.json │ │ ├── QMUI_tips_error.imageset/ │ │ │ └── Contents.json │ │ └── QMUI_tips_info.imageset/ │ │ └── Contents.json │ └── UIKitExtensions/ │ ├── CALayer+QMUI.h │ ├── CALayer+QMUI.m │ ├── NSArray+QMUI.h │ ├── NSArray+QMUI.m │ ├── NSAttributedString+QMUI.h │ ├── NSAttributedString+QMUI.m │ ├── NSCharacterSet+QMUI.h │ ├── NSCharacterSet+QMUI.m │ ├── NSDictionary+QMUI.h │ ├── NSDictionary+QMUI.m │ ├── NSMethodSignature+QMUI.h │ ├── NSMethodSignature+QMUI.m │ ├── NSNumber+QMUI.h │ ├── NSNumber+QMUI.m │ ├── NSObject+QMUI.h │ ├── NSObject+QMUI.m │ ├── NSParagraphStyle+QMUI.h │ ├── NSParagraphStyle+QMUI.m │ ├── NSPointerArray+QMUI.h │ ├── NSPointerArray+QMUI.m │ ├── NSRegularExpression+QMUI.h │ ├── NSRegularExpression+QMUI.m │ ├── NSShadow+QMUI.h │ ├── NSShadow+QMUI.m │ ├── NSString+QMUI.h │ ├── NSString+QMUI.m │ ├── NSURL+QMUI.h │ ├── NSURL+QMUI.m │ ├── QMUIBarProtocol/ │ │ ├── QMUIBarProtocol.h │ │ ├── QMUIBarProtocolPrivate.h │ │ ├── QMUIBarProtocolPrivate.m │ │ ├── UINavigationBar+QMUIBarProtocol.h │ │ ├── UINavigationBar+QMUIBarProtocol.m │ │ ├── UITabBar+QMUIBarProtocol.h │ │ └── UITabBar+QMUIBarProtocol.m │ ├── QMUIStringPrivate.h │ ├── QMUIStringPrivate.m │ ├── UIActivityIndicatorView+QMUI.h │ ├── UIActivityIndicatorView+QMUI.m │ ├── UIApplication+QMUI.h │ ├── UIApplication+QMUI.m │ ├── UIBarItem+QMUI.h │ ├── UIBarItem+QMUI.m │ ├── UIBezierPath+QMUI.h │ ├── UIBezierPath+QMUI.m │ ├── UIBlurEffect+QMUI.h │ ├── UIBlurEffect+QMUI.m │ ├── UIButton+QMUI.h │ ├── UIButton+QMUI.m │ ├── UICollectionView+QMUI.h │ ├── UICollectionView+QMUI.m │ ├── UICollectionViewCell+QMUI.h │ ├── UICollectionViewCell+QMUI.m │ ├── UIColor+QMUI.h │ ├── UIColor+QMUI.m │ ├── UIControl+QMUI.h │ ├── UIControl+QMUI.m │ ├── UIFont+QMUI.h │ ├── UIFont+QMUI.m │ ├── UIGestureRecognizer+QMUI.h │ ├── UIGestureRecognizer+QMUI.m │ ├── UIImage+QMUI.h │ ├── UIImage+QMUI.m │ ├── UIImageView+QMUI.h │ ├── UIImageView+QMUI.m │ ├── UIInterface+QMUI.h │ ├── UIInterface+QMUI.m │ ├── UILabel+QMUI.h │ ├── UILabel+QMUI.m │ ├── UIMenuController+QMUI.h │ ├── UIMenuController+QMUI.m │ ├── UINavigationBar+QMUI.h │ ├── UINavigationBar+QMUI.m │ ├── UINavigationController+QMUI.h │ ├── UINavigationController+QMUI.m │ ├── UINavigationItem+QMUI.h │ ├── UINavigationItem+QMUI.m │ ├── UIScrollView+QMUI.h │ ├── UIScrollView+QMUI.m │ ├── UISearchBar+QMUI.h │ ├── UISearchBar+QMUI.m │ ├── UISearchController+QMUI.h │ ├── UISearchController+QMUI.m │ ├── UISlider+QMUI.h │ ├── UISlider+QMUI.m │ ├── UISwitch+QMUI.h │ ├── UISwitch+QMUI.m │ ├── UITabBar+QMUI.h │ ├── UITabBar+QMUI.m │ ├── UITabBarItem+QMUI.h │ ├── UITabBarItem+QMUI.m │ ├── UITableView+QMUI.h │ ├── UITableView+QMUI.m │ ├── UITableViewCell+QMUI.h │ ├── UITableViewCell+QMUI.m │ ├── UITableViewHeaderFooterView+QMUI.h │ ├── UITableViewHeaderFooterView+QMUI.m │ ├── UITextField+QMUI.h │ ├── UITextField+QMUI.m │ ├── UITextInputTraits+QMUI.h │ ├── UITextInputTraits+QMUI.m │ ├── UITextView+QMUI.h │ ├── UITextView+QMUI.m │ ├── UIToolbar+QMUI.h │ ├── UIToolbar+QMUI.m │ ├── UITraitCollection+QMUI.h │ ├── UITraitCollection+QMUI.m │ ├── UIView+QMUI.h │ ├── UIView+QMUI.m │ ├── UIView+QMUIBorder.h │ ├── UIView+QMUIBorder.m │ ├── UIViewController+QMUI.h │ ├── UIViewController+QMUI.m │ ├── UIVisualEffectView+QMUI.h │ ├── UIVisualEffectView+QMUI.m │ ├── UIWindow+QMUI.h │ └── UIWindow+QMUI.m ├── QMUIKit.podspec ├── QMUIKitTests/ │ ├── Components/ │ │ └── QMUIThemeTests.m │ ├── Core/ │ │ └── QMUICommonDefinesTests.m │ ├── Info.plist │ └── UIKitExtensions/ │ ├── NSObjectTests.m │ ├── NSStringTests.m │ ├── UIButtonTests.m │ └── UIColorTests.m ├── README.md ├── add_license.py ├── new_license_content.txt ├── old_license_content.txt ├── qmui.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ ├── QMUIKit.xcscheme │ └── QMUIKitTests.xcscheme └── umbrellaHeaderFileCreator.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/----.md ================================================ --- name: 使用方式 about: 咨询 QMUI 某些功能的用法 title: '' labels: help wanted assignees: '' --- QMUI 已经提供了**详尽的注释文档**,以及**完整的示例项目 [QMUI Demo](https://github.com/QMUI/QMUI_iOS_Demo)**,当你遇到某些功能不知道怎么使用,或者想知道 QMUI 是否有提供某些功能时,请先查看注释文档或者 Demo,找不到了再提 issue。若提的 issue 已有明确注释或示例的,**可能会被直接关闭或得不到及时的回复**,请知悉。 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug about: QMUIKit 框架本身的 bug(请注意区分非 QMUIKit 代码引发的问题) title: '' labels: '' assignees: '' --- **Bug 表现** 问题的具体描述 **截图** Bug 现场的界面截图,或者 Xcode 控制台的错误信息截图,有问题的代码截图 **如何重现** 1. ... 2. ... **预期的表现** 正常情况下,应该是什么表现 **其他信息** - 设备: [例如模拟器、iPhone、iPad] - iOS 版本: [iOS 14.x] - Xcode 版本: [Xcode 12.x] - QMUI 版本: [4.x.x] ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: 意见与建议 about: 功能、接口设计等的相关建议 title: '' labels: suggest assignees: '' --- **现存问题或期望目标** 对于功能的建议,请说明具体的场景,现在的代码为什么无法实现需求。 对于代码设计方面的建议,请说明目前的问题所在。 ================================================ FILE: .gitignore ================================================ .DS_Store xcuserdata/ ================================================ FILE: CONTRIBUTING.md ================================================ [腾讯开源激励计划](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. 做充分的测试。 ================================================ FILE: LICENSE.TXT ================================================ Tencent is pleased 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. ================================================ FILE: QMUIConfigurationTemplate/QMUIConfigurationTemplate.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/3/29. // #import #import /** * QMUIConfigurationTemplate 是一份配置表,用于配合 QMUIConfiguration 来管理整个 App 的全局样式,使用方式: * 在 QMUI 项目代码的文件夹里找到 QMUIConfigurationTemplate 目录,把里面所有文件复制到自己项目里,保证能被编译到即可,不需要在某些地方 import,也不需要手动运行。 * * @warning 更新 QMUIKit 的版本时,请留意 Release Log 里是否有提醒更新配置表,请尽量保持自己项目里的配置表与 QMUIKit 里的配置表一致,避免遗漏新的属性。 * @warning 配置表的 class 名必须以 QMUIConfigurationTemplate 开头,并且实现 ,因为这两者是 QMUI 识别该 NSObject 是否为一份配置表的条件。 * @warning QMUI 2.3.0 之后,配置表改为自动运行,不需要再在某个地方手动运行了。 */ @interface QMUIConfigurationTemplate : NSObject @end ================================================ FILE: QMUIConfigurationTemplate/QMUIConfigurationTemplate.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/3/29. // #import "QMUIConfigurationTemplate.h" #import @implementation QMUIConfigurationTemplate #pragma mark - - (void)applyConfigurationTemplate { // === 修改配置值 === // #pragma mark - Global Color 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 : 浅一点的灰色 QMUICMI.redColor = UIColorMake(250, 58, 58); // UIColorRed : 红色 QMUICMI.greenColor = UIColorMake(159, 214, 97); // UIColorGreen : 绿色 QMUICMI.blueColor = UIColorMake(49, 189, 243); // UIColorBlue : 蓝色 QMUICMI.yellowColor = UIColorMake(255, 207, 71); // UIColorYellow : 黄色 QMUICMI.linkColor = UIColorMake(56, 116, 171); // UIColorLink : 文字链接颜色 QMUICMI.disabledColor = UIColorGray; // UIColorDisabled : 全局 disabled 的颜色,一般用于 UIControl 等控件 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 (QMUIBorder) 分隔线颜色 QMUICMI.separatorDashedColor = UIColorMake(17, 17, 17); // UIColorSeparatorDashed : 全局默认的虚线分隔线的颜色,默认 QMUIKit 暂时没用到 QMUICMI.placeholderColor = UIColorMake(196, 200, 208); // 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 = NO; // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上 #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 = UIColorBlue; // ButtonTintColor : QMUIButton 默认的 tintColor,不影响系统的 UIButton #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 : UINavigationBar 里 UIBarButtonItem 以及 QMUINavigationButtonTypeNormal 的字体 QMUICMI.navBarButtonFontBold = nil; // NavBarButtonFontBold : iOS 15 及以后用于设置 UINavigationBar 里 Done 类型的 UIBarButtonItem 以及 QMUINavigationButtonTypeBold 的字体,iOS 14 及以前只对后者生效 QMUICMI.navBarBackgroundImage = nil; // NavBarBackgroundImage : UINavigationBar 的背景图 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.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.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: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 的背景图 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.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 : 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 : 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.searchBarTextFieldBackgroundImage = nil; // SearchBarTextFieldBackgroundImage : QMUISearchBar 里的文本框的背景图,图片高度会决定输入框的高度 QMUICMI.searchBarTextFieldBorderColor = nil; // SearchBarTextFieldBorderColor : QMUISearchBar 里的文本框的边框颜色 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 里的文本框输入文字时右边的清空按钮的图片 #pragma mark - Plain TableView QMUICMI.tableViewEstimatedHeightEnabled = YES; // TableViewEstimatedHeightEnabled : 是否要开启全局 UITableView 的 estimatedRow(Section/Footer)Height QMUICMI.tableViewBackgroundColor = nil; // TableViewBackgroundColor : Plain 类型的 QMUITableView 的背景色颜色 QMUICMI.tableSectionIndexColor = nil; // TableSectionIndexColor : 列表右边的字母索引条的文字颜色 QMUICMI.tableSectionIndexBackgroundColor = nil; // TableSectionIndexBackgroundColor : 列表右边的字母索引条的背景色 QMUICMI.tableSectionIndexTrackingBackgroundColor = nil; // TableSectionIndexTrackingBackgroundColor : 列表右边的字母索引条在选中时的背景色 QMUICMI.tableViewSeparatorColor = UIColorSeparator; // TableViewSeparatorColor : 列表的分隔线颜色 QMUICMI.tableViewCellNormalHeight = UITableViewAutomaticDimension; // TableViewCellNormalHeight : QMUITableView 的默认 cell 高度 QMUICMI.tableViewCellTitleLabelColor = nil; // TableViewCellTitleLabelColor : QMUITableViewCell 的 textLabel 的文字颜色 QMUICMI.tableViewCellDetailLabelColor = nil; // TableViewCellDetailLabelColor : QMUITableViewCell 的 detailTextLabel 的文字颜色 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 时的箭头的图片 QMUICMI.tableViewCellCheckmarkImage = nil; // TableViewCellCheckmarkImage : QMUITableViewCell 当 accessoryType 为 UITableViewCellAccessoryCheckmark 时的打钩的图片 QMUICMI.tableViewCellDetailButtonImage = nil; // 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 = UIColorGrayDarken; // TableViewSectionHeaderTextColor : Plain 类型的 QMUITableView sectionHeader 里的文字颜色 QMUICMI.tableViewSectionFooterTextColor = UIColorGray; // TableViewSectionFooterTextColor : Plain 类型的 QMUITableView sectionFooter 里的文字颜色 QMUICMI.tableViewSectionHeaderAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewSectionHeaderAccessoryMargins : Plain 类型的 QMUITableView sectionHeader accessoryView 的间距 QMUICMI.tableViewSectionFooterAccessoryMargins = UIEdgeInsetsMake(0, 15, 0, 0); // TableViewSectionFooterAccessoryMargins : Plain 类型的 QMUITableView sectionFooter accessoryView 的间距 QMUICMI.tableViewSectionHeaderContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionHeaderContentInset : Plain 类型的 QMUITableView sectionHeader 里的内容的 padding QMUICMI.tableViewSectionFooterContentInset = UIEdgeInsetsMake(4, 15, 4, 15); // TableViewSectionFooterContentInset : Plain 类型的 QMUITableView sectionFooter 里的内容的 padding 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.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.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 = 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 ================================================ FILE: QMUIKit/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: QMUIKit/PrivacyInfo.xcprivacy ================================================ NSPrivacyAccessedAPITypes NSPrivacyAccessedAPIType NSPrivacyAccessedAPICategoryUserDefaults NSPrivacyAccessedAPITypeReasons CA92.1 NSPrivacyCollectedDataTypes NSPrivacyTrackingDomains NSPrivacyTracking ================================================ FILE: QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/AssetLibrary/QMUIAsset.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/6/30. // #import #import #import #import #import #import @class QMUIAsset; /// 相册展示内容的类型 typedef NS_ENUM(NSUInteger, QMUIAlbumContentType) { QMUIAlbumContentTypeAll, // 展示所有资源 QMUIAlbumContentTypeOnlyPhoto, // 只展示照片 QMUIAlbumContentTypeOnlyVideo, // 只展示视频 QMUIAlbumContentTypeOnlyAudio // 只展示音频 }; /// 相册展示内容按日期排序的方式 typedef NS_ENUM(NSUInteger, QMUIAlbumSortType) { QMUIAlbumSortTypePositive, // 日期最新的内容排在后面 QMUIAlbumSortTypeReverse // 日期最新的内容排在前面 }; @interface QMUIAssetsGroup : NSObject - (instancetype)initWithPHCollection:(PHAssetCollection *)phAssetCollection; - (instancetype)initWithPHCollection:(PHAssetCollection *)phAssetCollection fetchAssetsOptions:(PHFetchOptions *)pHFetchOptions; /// 仅能通过 initWithPHCollection 和 initWithPHCollection:fetchAssetsOption 方法修改 phAssetCollection 的值 @property(nonatomic, strong, readonly) PHAssetCollection *phAssetCollection; /// 仅能通过 initWithPHCollection 和 initWithPHCollection:fetchAssetsOption 方法修改 phAssetCollection 后,产生一个对应的 PHAssetsFetchResults 保存到 phFetchResult 中 @property(nonatomic, strong, readonly) PHFetchResult *phFetchResult; /// 相册的名称 - (NSString *)name; /// 相册内的资源数量,包括视频、图片、音频(如果支持)这些类型的所有资源 - (NSInteger)numberOfAssets; /** * 相册的缩略图,即系统接口中的相册海报(Poster Image) * * @return 相册的缩略图 */ - (UIImage *)posterImageWithSize:(CGSize)size; /** * 枚举相册内所有的资源 * * @param albumSortType 相册内资源的排序方式,可以选择日期最新的排在最前面,也可以选择日期最新的排在最后面 * @param enumerationBlock 枚举相册内资源时调用的 block,参数 result 表示每次枚举时对应的资源。 * 枚举所有资源结束后,enumerationBlock 会被再调用一次,这时 result 的值为 nil。 * 可以以此作为判断枚举结束的标记 */ - (void)enumerateAssetsWithOptions:(QMUIAlbumSortType)albumSortType usingBlock:(void (^)(QMUIAsset *resultAsset))enumerationBlock; /** * 枚举相册内所有的资源,相册内资源按日期最新的排在最后面 * * @param enumerationBlock 枚举相册内资源时调用的 block,参数 result 表示每次枚举时对应的资源。 * 枚举所有资源结束后,enumerationBlock 会被再调用一次,这时 result 的值为 nil。 * 可以以此作为判断枚举结束的标记 */ - (void)enumerateAssetsUsingBlock:(void (^)(QMUIAsset *result))enumerationBlock; @end ================================================ FILE: QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsGroup.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/AssetLibrary/QMUIAssetsManager.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/CAAnimation+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/CAAnimation+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/CALayer+QMUIViewAnimation.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIAlbumViewController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerCollectionViewCell.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerHelper.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerPreviewViewController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIAlertController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIAlertController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIAnimation/QMUIAnimationHelper.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIAnimation/QMUIDisplayLinkAnimation.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIAnimation/QMUIEasings.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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; } ================================================ FILE: QMUIKit/QMUIComponents/QMUIAppearance.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIAppearance.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.h ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeLabel.m ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeProtocol.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIButton/QMUIToolbarButton.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICellHeightCache.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICellHeightCache.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICellHeightKeyCache/QMUICellHeightKeyCache.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICellHeightKeyCache/UITableView+QMUICellHeightKeyCache.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICellSizeKeyCache/QMUICellSizeKeyCache.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICellSizeKeyCache/UICollectionView+QMUICellSizeKeyCache.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICheckbox.h ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICheckbox.m ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIConsole/QMUIConsole.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleToolbar.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIConsole/QMUILog+QMUIConsole.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIDialogViewController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIDialogViewController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIEmotionInputManager.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIEmotionInputManager.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIEmotionView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIEmotionView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIEmptyView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 2016/10/9. // #import @class QMUIButton; @protocol QMUIEmptyViewLoadingViewProtocol @optional - (void)startAnimating; // 当调用 setLoadingViewHidden:NO 时,系统将自动调用此处的 startAnimating @end /** * 通用的空界面控件,支持显示 loading、标题和副标题提示语、占位图片,QMUICommonViewController 内已集成一个 emptyView,无需额外添加。 */ @interface QMUIEmptyView : UIView // 布局顺序从上到下依次为:imageView, loadingView, textLabel, detailTextLabel, actionButton @property(nonatomic, strong) UIView *loadingView; // 此控件通过设置 loadingView.hidden 来控制 loadinView 的显示和隐藏,因此请确保你的loadingView 没有类似于 hidesWhenStopped = YES 之类会使 view.hidden 失效的属性 @property(nonatomic, strong, readonly) UIImageView *imageView; @property(nonatomic, strong, readonly) UILabel *textLabel; @property(nonatomic, strong, readonly) UILabel *detailTextLabel; @property(nonatomic, strong, readonly) QMUIButton *actionButton; // 可通过调整这些insets来控制间距 @property(nonatomic, assign) UIEdgeInsets imageViewInsets UI_APPEARANCE_SELECTOR; // 默认为(0, 0, 36, 0) @property(nonatomic, assign) UIEdgeInsets loadingViewInsets UI_APPEARANCE_SELECTOR; // 默认为(0, 0, 36, 0) @property(nonatomic, assign) UIEdgeInsets textLabelInsets UI_APPEARANCE_SELECTOR; // 默认为(0, 0, 10, 0) @property(nonatomic, assign) UIEdgeInsets detailTextLabelInsets UI_APPEARANCE_SELECTOR; // 默认为(0, 0, 10, 0) @property(nonatomic, assign) UIEdgeInsets actionButtonInsets UI_APPEARANCE_SELECTOR; // 默认为(0, 0, 0, 0) @property(nonatomic, assign) CGFloat verticalOffset UI_APPEARANCE_SELECTOR; // 如果不想要内容整体垂直居中,则可通过调整此属性来进行垂直偏移。默认为-30,即内容比中间略微偏上 // 字体 @property(nonatomic, strong) UIFont *textLabelFont UI_APPEARANCE_SELECTOR; // 默认为15pt系统字体 @property(nonatomic, strong) UIFont *detailTextLabelFont UI_APPEARANCE_SELECTOR; // 默认为14pt系统字体 @property(nonatomic, strong) UIFont *actionButtonFont UI_APPEARANCE_SELECTOR; // 默认为15pt系统字体 // 颜色 @property(nonatomic, strong) UIColor *textLabelTextColor UI_APPEARANCE_SELECTOR; // 默认为(93, 100, 110) @property(nonatomic, strong) UIColor *detailTextLabelTextColor UI_APPEARANCE_SELECTOR; // 默认为(133, 140, 150) @property(nonatomic, strong) UIColor *actionButtonTitleColor UI_APPEARANCE_SELECTOR; // 默认为 ButtonTintColor // 显示或隐藏loading图标 - (void)setLoadingViewHidden:(BOOL)hidden; /** * 设置要显示的图片 * @param image 要显示的图片,为nil则不显示 */ - (void)setImage:(UIImage *)image; /** * 设置提示语 * @param text 提示语文本,若为nil则隐藏textLabel */ - (void)setTextLabelText:(NSString *)text; /** * 设置详细提示语的文本 * @param text 详细提示语文本,若为nil则隐藏detailTextLabel */ - (void)setDetailTextLabelText:(NSString *)text; /** * 设置操作按钮的文本 * @param title 操作按钮的文本,若为nil则隐藏actionButton */ - (void)setActionButtonTitle:(NSString *)title; /** * 如果要继承QMUIEmptyView并添加新的子 view,则必须: * 1. 像其它自带 view 一样添加到 contentView 上 * 2. 重写sizeThatContentViewFits */ @property(nonatomic, strong, readonly) UIView *contentView; - (CGSize)sizeThatContentViewFits; // 返回一个恰好容纳所有子 view 的大小 @end ================================================ FILE: QMUIKit/QMUIComponents/QMUIEmptyView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIFloatLayoutView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIFloatLayoutView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIGridView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIGridView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/1/30. // #import "QMUIGridView.h" #import "QMUICore.h" #import "CALayer+QMUI.h" @interface QMUIGridView () @property(nonatomic, strong) CAShapeLayer *separatorLayer; @end @implementation QMUIGridView - (instancetype)initWithFrame:(CGRect)frame column:(NSInteger)column rowHeight:(CGFloat)rowHeight { if (self = [super initWithFrame:frame]) { [self didInitialize]; self.columnCount = column; self.rowHeight = rowHeight; } return self; } - (instancetype)initWithColumn:(NSInteger)column rowHeight:(CGFloat)rowHeight { return [self initWithFrame:CGRectZero column:column rowHeight:rowHeight]; } - (instancetype)initWithFrame:(CGRect)frame { return [self initWithFrame:frame column:0 rowHeight:0]; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { [self didInitialize]; } return self; } - (void)didInitialize { self.separatorLayer = [CAShapeLayer layer]; [self.separatorLayer qmui_removeDefaultAnimations]; self.separatorLayer.hidden = YES; [self.layer addSublayer:self.separatorLayer]; self.separatorColor = UIColorSeparator; } - (void)setSeparatorWidth:(CGFloat)separatorWidth { _separatorWidth = separatorWidth; self.separatorLayer.lineWidth = _separatorWidth; self.separatorLayer.hidden = _separatorWidth <= 0; } - (void)setSeparatorColor:(UIColor *)separatorColor { _separatorColor = separatorColor; self.separatorLayer.strokeColor = _separatorColor.CGColor; } // 返回最接近平均列宽的值,保证其为整数,因此所有columnWidth加起来可能比总宽度要小 - (CGFloat)stretchColumnWidth { return floor((CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(self.padding) - self.separatorWidth * (self.columnCount - 1)) / self.columnCount); } - (NSInteger)rowCount { NSInteger subviewCount = self.subviews.count; return subviewCount / self.columnCount + (subviewCount % self.columnCount > 0 ? 1 : 0); } - (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; } - (void)layoutSubviews { [super layoutSubviews]; NSInteger subviewCount = self.subviews.count; if (subviewCount == 0) return; CGSize size = self.bounds.size; if (CGSizeIsEmpty(size)) return; CGFloat columnWidth = [self stretchColumnWidth]; CGFloat rowHeight = self.rowHeight; NSInteger rowCount = [self rowCount]; BOOL shouldShowSeparator = self.separatorWidth > 0; CGFloat lineOffset = shouldShowSeparator ? self.separatorWidth / 2.0 : 0; UIBezierPath *separatorPath = shouldShowSeparator ? [UIBezierPath bezierPath] : nil; for (NSInteger row = 0; row < rowCount; row++) { for (NSInteger column = 0; column < self.columnCount; column++) { NSInteger index = row * self.columnCount + column; if (index < subviewCount) { BOOL isLastColumn = column == self.columnCount - 1; BOOL isLastRow = row == rowCount - 1; UIView *subview = self.subviews[index]; 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 - UIEdgeInsetsGetHorizontalValue(self.padding) - columnWidth * (self.columnCount - 1) - self.separatorWidth * (self.columnCount - 1); } if (isLastRow) { // 最后一行的item要占满剩余空间,避免一些计算偏差 subviewFrame.size.height = size.height - UIEdgeInsetsGetVerticalValue(self.padding) - rowHeight * (rowCount - 1) - self.separatorWidth * (rowCount - 1); } subview.frame = subviewFrame; [subview setNeedsLayout]; if (shouldShowSeparator) { // 每个 item 都画右边和下边这两条分隔线 CGPoint rightTopPoint = CGPointMake(CGRectGetMaxX(subviewFrame) + lineOffset, CGRectGetMinY(subviewFrame)); CGPoint rightBottomPoint = CGPointMake(rightTopPoint.x - (isLastColumn ? lineOffset : 0), CGRectGetMaxY(subviewFrame) + (!isLastRow ? lineOffset : 0)); CGPoint leftBottomPoint = CGPointMake(CGRectGetMinX(subviewFrame), rightBottomPoint.y); if (!isLastColumn) { [separatorPath moveToPoint:rightTopPoint]; [separatorPath addLineToPoint:rightBottomPoint]; } if (!isLastRow) { [separatorPath moveToPoint:rightBottomPoint]; [separatorPath addLineToPoint:leftBottomPoint]; } } } } } if (shouldShowSeparator) { self.separatorLayer.path = separatorPath.CGPath; } } @end ================================================ FILE: QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 2016/11/30. // #import #import "QMUIZoomImageView.h" @class QMUIImagePreviewView; @class QMUICollectionViewPagingLayout; typedef NS_ENUM (NSUInteger, QMUIImagePreviewMediaType) { QMUIImagePreviewMediaTypeImage, QMUIImagePreviewMediaTypeLivePhoto, QMUIImagePreviewMediaTypeVideo, QMUIImagePreviewMediaTypeOthers }; @protocol QMUIImagePreviewViewDelegate @required - (NSUInteger)numberOfImagesInImagePreviewView:(QMUIImagePreviewView *)imagePreviewView; - (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView renderZoomImageView:(QMUIZoomImageView *)zoomImageView atIndex:(NSUInteger)index; @optional // 返回要展示的媒体资源的类型(图片、live photo、视频),如果不实现此方法,则 QMUIImagePreviewView 将无法选择最合适的 cell 来复用从而略微增大系统开销 - (QMUIImagePreviewMediaType)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView assetTypeAtIndex:(NSUInteger)index; /** * 当左右的滚动停止时会触发这个方法 * @param imagePreviewView 当前预览的 QMUIImagePreviewView * @param index 当前滚动到的图片所在的索引 */ - (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView didScrollToIndex:(NSUInteger)index; /** * 在滚动过程中,如果某一张图片的边缘(左/右)经过预览控件的中心点时,就会触发这个方法 * @param imagePreviewView 当前预览的 QMUIImagePreviewView * @param index 当前滚动到的图片所在的索引 */ - (void)imagePreviewView:(QMUIImagePreviewView *)imagePreviewView willScrollHalfToIndex:(NSUInteger)index; @end /** * 查看图片的控件,支持横向滚动、放大缩小、loading 及错误语展示,内部使用 UICollectionView 实现横向滚动及 cell 复用,因此与其他普通的 UICollectionView 一样,也可使用 reloadData、collectionViewLayout 等常用方法。 * * 使用方式: * * 1. 使用 initWithFrame: 或 init 方法初始化。 * 2. 设置 delegate。 * 3. 在 delegate 的 numberOfImagesInImagePreviewView: 方法里返回图片总数。 * 4. 在 delegate 的 imagePreviewView:renderZoomImageView:atIndex: 方法里为 zoomImageView.image 设置图片,如果需要,也可调用 [zoomImageView showLoading] 等方法来显示 loading。 * 5. 由于 QMUIImagePreviewViewDelegate 继承自 QMUIZoomImageViewDelegate,所以若需要响应单击、双击、长按事件,请实现 QMUIZoomImageViewDelegate 里的对应方法。 * 6. 若需要从指定的某一张图片开始查看,可使用 currentImageIndex 属性。 * * @see QMUIImagePreviewViewController */ @interface QMUIImagePreviewView : UIView @property(nonatomic, weak) id delegate; @property(nonatomic, strong, readonly) UICollectionView *collectionView; @property(nonatomic, strong, readonly) QMUICollectionViewPagingLayout *collectionViewLayout; /// 获取当前正在查看的图片 index,也可强制将图片滚动到指定的 index @property(nonatomic, assign) NSUInteger currentImageIndex; - (void)setCurrentImageIndex:(NSUInteger)currentImageIndex animated:(BOOL)animated; /// 每一页里的 loading 的颜色,默认为 UIColorWhite @property(nonatomic, strong) UIColor *loadingColor; @end @interface QMUIImagePreviewView (QMUIZoomImageView) /** * 获取某个 QMUIZoomImageView 所对应的 index * @return zoomImageView 对应的 index,若当前的 zoomImageView 不可见,会返回 0 */ - (NSInteger)indexForZoomImageView:(QMUIZoomImageView *)zoomImageView; /** * 获取某个 index 对应的 zoomImageView * @return 指定的 index 所在的 zoomImageView,若该 index 对应的图片当前不可见(不处于可视区域),则返回 nil */ - (QMUIZoomImageView *)zoomImageViewAtIndex:(NSUInteger)index; @end ================================================ FILE: QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 2016/11/30. // #import "QMUIImagePreviewView.h" #import "QMUICore.h" #import "QMUICollectionViewPagingLayout.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 @property(nonatomic, strong) QMUIZoomImageView *zoomImageView; @end @implementation QMUIImagePreviewCell - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.backgroundColor = UIColorClear; self.zoomImageView = [[QMUIZoomImageView alloc] init]; [self.contentView addSubview:self.zoomImageView]; } return self; } - (void)layoutSubviews { [super layoutSubviews]; self.zoomImageView.qmui_frameApplyTransform = self.contentView.bounds; } @end static NSString * const kLivePhotoCellIdentifier = @"livephoto"; static NSString * const kVideoCellIdentifier = @"video"; static NSString * const kImageOrUnknownCellIdentifier = @"imageorunknown"; @interface QMUIImagePreviewView () @property(nonatomic, assign) BOOL isChangingCollectionViewBounds; @property(nonatomic, assign) CGFloat previousIndexWhenScrolling; @end @implementation QMUIImagePreviewView @synthesize currentImageIndex = _currentImageIndex; - (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:self.frame]; } return self; } - (void)didInitializedWithFrame:(CGRect)frame { _collectionViewLayout = [[QMUICollectionViewPagingLayout alloc] initWithStyle:QMUICollectionViewPagingLayoutStyleDefault]; self.collectionViewLayout.allowsMultipleItemScroll = NO; _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMakeWithSize(frame.size) collectionViewLayout:self.collectionViewLayout]; 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]; [self addSubview:self.collectionView]; self.loadingColor = UIColorWhite; } - (void)layoutSubviews { [super layoutSubviews]; BOOL isCollectionViewSizeChanged = !CGSizeEqualToSize(self.collectionView.bounds.size, self.bounds.size); if (isCollectionViewSizeChanged) { self.isChangingCollectionViewBounds = YES; // 必须先 invalidateLayout,再更新 collectionView.frame,否则横竖屏旋转前后的图片不一致(因为 scrollViewDidScroll: 时 contentSize、contentOffset 那些是错的) [self.collectionViewLayout invalidateLayout]; self.collectionView.frame = self.bounds; [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:self.currentImageIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; self.isChangingCollectionViewBounds = NO; } } - (void)setCurrentImageIndex:(NSUInteger)currentImageIndex { [self setCurrentImageIndex:currentImageIndex animated:NO]; } - (void)setCurrentImageIndex:(NSUInteger)currentImageIndex animated:(BOOL)animated { _currentImageIndex = currentImageIndex; [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 { QMUILog(@"QMUIImagePreviewView", @"dataSource 里的图片数量和当前显示出来的图片数量不匹配, collectionView.numberOfItems = %@, collectionViewDataSource.numberOfItems = %@, currentImageIndex = %@", @([self.collectionView numberOfItemsInSection:0]), @([self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:1]), @(_currentImageIndex)); } } - (void)setLoadingColor:(UIColor *)loadingColor { BOOL isLoadingColorChanged = _loadingColor && ![_loadingColor isEqual:loadingColor]; _loadingColor = loadingColor; if (isLoadingColorChanged) { [self.collectionView reloadItemsAtIndexPaths:self.collectionView.indexPathsForVisibleItems]; } } #pragma mark - - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { if ([self.delegate respondsToSelector:@selector(numberOfImagesInImagePreviewView:)]) { return [self.delegate numberOfImagesInImagePreviewView:self]; } return 0; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { NSString *identifier = kImageOrUnknownCellIdentifier; if ([self.delegate respondsToSelector:@selector(imagePreviewView:assetTypeAtIndex:)]) { QMUIImagePreviewMediaType type = [self.delegate imagePreviewView:self assetTypeAtIndex:indexPath.item]; if (type == QMUIImagePreviewMediaTypeLivePhoto) { identifier = kLivePhotoCellIdentifier; } else if (type == QMUIImagePreviewMediaTypeVideo) { identifier = kVideoCellIdentifier; } } 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.videoPlayerItem = nil; zoomView.livePhoto = nil; if ([self.delegate respondsToSelector:@selector(imagePreviewView:renderZoomImageView:atIndex:)]) { [self.delegate imagePreviewView:self renderZoomImageView:zoomView atIndex:indexPath.item]; } return cell; } - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { QMUIImagePreviewCell *previewCell = (QMUIImagePreviewCell *)cell; [previewCell.zoomImageView revertZooming]; } - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { QMUIImagePreviewCell *previewCell = (QMUIImagePreviewCell *)cell; [previewCell.zoomImageView endPlayingVideo]; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return collectionView.bounds.size; } #pragma mark - - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { if (scrollView != self.collectionView) { return; } // 当前滚动到的页数 if ([self.delegate respondsToSelector:@selector(imagePreviewView:didScrollToIndex:)]) { [self.delegate imagePreviewView:self didScrollToIndex:self.currentImageIndex]; } } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (scrollView != self.collectionView) { return; } if (self.isChangingCollectionViewBounds) { return; } CGFloat pageWidth = [self collectionView:self.collectionView layout:self.collectionViewLayout sizeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]].width; CGFloat pageHorizontalMargin = self.collectionViewLayout.minimumLineSpacing; CGFloat contentOffsetX = self.collectionView.contentOffset.x; CGFloat index = contentOffsetX / (pageWidth + pageHorizontalMargin); // 在滑动过临界点的那一次才去调用 delegate,避免过于频繁的调用 BOOL isFirstDidScroll = self.previousIndexWhenScrolling == 0; // 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]) { // 不调用 setter,避免又走一次 scrollToItem _currentImageIndex = index; if ([self.delegate respondsToSelector:@selector(imagePreviewView:willScrollHalfToIndex:)]) { [self.delegate imagePreviewView:self willScrollHalfToIndex:index]; } } } self.previousIndexWhenScrolling = index; } @end @implementation QMUIImagePreviewView (QMUIZoomImageView) - (NSInteger)indexForZoomImageView:(QMUIZoomImageView *)zoomImageView { if ([zoomImageView.superview.superview isKindOfClass:[QMUIImagePreviewCell class]]) { return [self.collectionView indexPathForCell:(QMUIImagePreviewCell *)zoomImageView.superview.superview].item; } else { QMUIAssert(NO, @"QMUIImagePreviewView (QMUIZoomImageView)", @"尝试通过 %s 获取 QMUIZoomImageView 所在的 index,但找不到 QMUIZoomImageView 所在的 cell,index 获取失败。%@", __func__, zoomImageView); } return NSNotFound; } - (QMUIZoomImageView *)zoomImageViewAtIndex:(NSUInteger)index { QMUIImagePreviewCell *cell = (QMUIImagePreviewCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]]; return cell.zoomImageView; } - (void)checkIfDelegateMissing { #ifdef DEBUG [NSObject qmui_enumerateProtocolMethods:@protocol(QMUIZoomImageViewDelegate) usingBlock:^(SEL selector) { if (![self respondsToSelector:selector]) { QMUIAssert(NO, @"QMUIImagePreviewView (QMUIZoomImageView)", @"%@ 需要响应 %@ 的方法 -%@", NSStringFromClass([self class]), NSStringFromProtocol(@protocol(QMUIZoomImageViewDelegate)), NSStringFromSelector(selector)); } }]; #endif } #pragma mark - - (void)singleTouchInZoomingImageView:(QMUIZoomImageView *)imageView location:(CGPoint)location { [self checkIfDelegateMissing]; if ([self.delegate respondsToSelector:_cmd]) { [self.delegate singleTouchInZoomingImageView:imageView location:location]; } } - (void)doubleTouchInZoomingImageView:(QMUIZoomImageView *)imageView location:(CGPoint)location { [self checkIfDelegateMissing]; if ([self.delegate respondsToSelector:_cmd]) { [self.delegate doubleTouchInZoomingImageView:imageView location:location]; } } - (void)longPressInZoomingImageView:(QMUIZoomImageView *)imageView { [self checkIfDelegateMissing]; if ([self.delegate respondsToSelector:_cmd]) { [self.delegate longPressInZoomingImageView:imageView]; } } - (void)didTouchICloudRetryButtonInZoomImageView:(QMUIZoomImageView *)imageView { [self checkIfDelegateMissing]; if ([self.delegate respondsToSelector:_cmd]) { [self.delegate didTouchICloudRetryButtonInZoomImageView:imageView]; } } - (void)zoomImageView:(QMUIZoomImageView *)imageView didHideVideoToolbar:(BOOL)didHide { [self checkIfDelegateMissing]; if ([self.delegate respondsToSelector:_cmd]) { [self.delegate zoomImageView:imageView didHideVideoToolbar:didHide]; } } - (BOOL)enabledZoomViewInZoomImageView:(QMUIZoomImageView *)imageView { [self checkIfDelegateMissing]; if ([self.delegate respondsToSelector:_cmd]) { return [self.delegate enabledZoomViewInZoomImageView:imageView]; } return YES; } @end ================================================ FILE: QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewTransitionAnimator.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIKeyboardManager.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIKeyboardManager.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILabel.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILabel.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILayouter/QMUILayouter.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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" ================================================ FILE: QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterItem.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearHorizontal.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILayouter/QMUILayouterLinearVertical.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILog/QMUILog.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILog/QMUILogItem.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILog/QMUILogItem.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILog/QMUILogNameManager.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILog/QMUILogger.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILog/QMUILogger.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILogManagerViewController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILogManagerViewController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUILogger+QMUIConfigurationTemplate.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIMarqueeLabel.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIMarqueeLabel.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIMoreOperationController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIMoreOperationController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIMultipleDelegates/NSObject+QMUIMultipleDelegates.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIMultipleDelegates/QMUIMultipleDelegates.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUINavigationTitleView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUINavigationTitleView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIOrderedDictionary.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIOrderedDictionary.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIPieProgressView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIPieProgressView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIPopupContainerView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIPopupContainerView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.h ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItem.m ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.h ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemView.m ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuItemViewProtocol.h ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingAnimator.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUINavigationBarScrollingSnapAnimator.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIScrollAnimator/QMUIScrollAnimator.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUISearchBar.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUISearchBar.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUISearchController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUISearchController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUISegmentedControl.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 14/11/3. // #import /* * QMUISegmentedControl,继承自 UISegmentedControl * 如果需要更大程度地修改样式,比如说字体大小,选中的 segment 的文字颜色等等,可以使用下面的第一个方法来做 * QMUISegmentedControl 也同样支持使用图片来做样式,需要五张图片。 */ @interface QMUISegmentedControl : UISegmentedControl /// 获取当前的所有 segmentItem,可能包括 NSString 或 UIImage。 @property(nonatomic, copy, readonly) NSArray *segmentItems; /** * 重新渲染 UISegmentedControl 的 UI,可以比较大程度地修改样式。比如 tintColor,selectedTextColor 等等。 * * @param tintColor Segmented 的 tintColor,作用范围包括字体颜色和按钮 border * @param selectedTextColor Segmented 选中状态的字体颜色 * @param fontSize Segmented 上字体的大小 */ - (void)updateSegmentedUIWithTintColor:(UIColor *)tintColor selectedTextColor:(UIColor *)selectedTextColor fontSize:(UIFont *)fontSize; /** * 用图片而非 tintColor 来渲染 UISegmentedControl 的 UI * * @param normalImage Segmented 非选中状态的背景图 * @param selectedImage Segmented 选中状态的背景图 * @param devideImage00 Segmented 在两个没有选中按钮 item 之间的分割线 * @param devideImage01 Segmented 在左边没选中右边选中两个 item 之间的分割线 * @param devideImage10 Segmented 在左边选中右边没选中两个 item 之间的分割线 * @param textColor Segmented 的字体颜色 * @param selectedTextColor Segmented 选中状态的字体颜色 * @param fontSize Segmented 的字体大小 */ - (void)setBackgroundWithNormalImage:(UIImage *)normalImage selectedImage:(UIImage *)selectedImage devideImage00:(UIImage *)devideImage00 devideImage01:(UIImage *)devideImage01 devideImage10:(UIImage *)devideImage10 textColor:(UIColor *)textColor selectedTextColor:(UIColor *)selectedTextColor fontSize:(UIFont *)fontSize; @end ================================================ FILE: QMUIKit/QMUIComponents/QMUISegmentedControl.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 14/11/3. // #import "QMUISegmentedControl.h" @implementation QMUISegmentedControl { NSMutableArray *_items; UIImage *_preSelectedImage; NSUInteger _preSegmentIndex; } - (instancetype)initWithItems:(NSArray *)items { self = [super initWithItems:items]; if (self) { _items = [[NSMutableArray alloc] initWithArray:items]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { _items = [[NSMutableArray alloc] init]; } return self; } - (void)updateSegmentedUIWithTintColor:(UIColor *)tintColor selectedTextColor:(UIColor *)selectedTextColor fontSize:(UIFont *)fontSize { [self setTintColor:tintColor]; [self setTitleTextAttributesWithTextColor:tintColor selectedTextColor:selectedTextColor fontSize:fontSize]; } - (void)setBackgroundWithNormalImage:(UIImage *)normalImage selectedImage:(UIImage *)selectedImage devideImage00:(UIImage *)devideImage00 devideImage01:(UIImage *)devideImage01 devideImage10:(UIImage *)devideImage10 textColor:(UIColor *)textColor selectedTextColor:(UIColor *)selectedTextColor fontSize:(UIFont *)fontSize; { [self setTitleTextAttributesWithTextColor:textColor selectedTextColor:selectedTextColor fontSize:fontSize]; [self setBackgroundWithNormalImage:normalImage selectedImage:selectedImage devideImage00:devideImage00 devideImage01:devideImage01 devideImage10:devideImage10]; } - (void)setTitleTextAttributesWithTextColor:(UIColor *)textColor selectedTextColor:(UIColor *)selectedTextColor fontSize:(UIFont *)fontSize { [self setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys: textColor, NSForegroundColorAttributeName, fontSize, NSFontAttributeName, nil] forState:UIControlStateNormal]; [self setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys: selectedTextColor, NSForegroundColorAttributeName, fontSize, NSFontAttributeName, nil] forState:UIControlStateSelected]; } - (void)setBackgroundWithNormalImage:(UIImage *)normalImage selectedImage:(UIImage *)selectedImage devideImage00:(UIImage *)devideImage00 devideImage01:(UIImage *)devideImage01 devideImage10:(UIImage *)devideImage10 { CGFloat devideImageWidth = devideImage00.size.width; [self setBackgroundImage:[normalImage resizableImageWithCapInsets:UIEdgeInsetsMake(12, 20, 12, 20)] forState:UIControlStateNormal barMetrics:UIBarMetricsDefault]; [self setBackgroundImage:[selectedImage resizableImageWithCapInsets:UIEdgeInsetsMake(12, 20, 12, 20)] forState:UIControlStateSelected barMetrics:UIBarMetricsDefault]; [self setDividerImage:[devideImage00 resizableImageWithCapInsets:UIEdgeInsetsMake(12, devideImageWidth/2, 12, devideImageWidth/2)] forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault]; [self setDividerImage:[devideImage10 resizableImageWithCapInsets:UIEdgeInsetsMake(12, devideImageWidth/2, 12, devideImageWidth/2)] forLeftSegmentState:UIControlStateSelected rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault]; [self setDividerImage:[devideImage01 resizableImageWithCapInsets:UIEdgeInsetsMake(12, devideImageWidth/2, 12, devideImageWidth/2)] forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateSelected barMetrics:UIBarMetricsDefault]; [self setContentPositionAdjustment:UIOffsetMake(- (12 - devideImageWidth) / 2, 0) forSegmentType:UISegmentedControlSegmentLeft barMetrics:UIBarMetricsDefault]; [self setContentPositionAdjustment:UIOffsetMake((12 - devideImageWidth) / 2, 0) forSegmentType:UISegmentedControlSegmentRight barMetrics:UIBarMetricsDefault]; } #pragma mark - Copy Items - (void)insertSegmentWithTitle:(nullable NSString *)title atIndex:(NSUInteger)segment animated:(BOOL)animated { [super insertSegmentWithTitle:title atIndex:segment animated:animated]; [_items insertObject:title atIndex:segment]; } - (void)insertSegmentWithImage:(nullable UIImage *)image atIndex:(NSUInteger)segment animated:(BOOL)animated { [super insertSegmentWithImage:image atIndex:segment animated:animated]; [_items insertObject:image atIndex:segment]; } - (void)removeSegmentAtIndex:(NSUInteger)segment animated:(BOOL)animated { [super removeSegmentAtIndex:segment animated:animated]; [_items removeObjectAtIndex:segment]; } - (void)removeAllSegments { [super removeAllSegments]; [_items removeAllObjects]; } - (NSArray *)segmentItems { return [NSArray arrayWithArray:_items]; } @end ================================================ FILE: QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationNavigationBar.h ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationNavigationBar.m ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationSupports.h ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUISheetPresentation/QMUISheetPresentationSupports.m ================================================ // // 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITableView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITableView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITableViewCell.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITableViewCell.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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; @property(nonatomic, strong) QMUIButton *defaultAccessoryButton; @property(nonatomic, strong) UIView *defaultDetailDisclosureView; @end @implementation QMUITableViewCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { if (!self.initByTableView) { [self didInitializeWithStyle:style]; } } return self; } - (instancetype)initForTableView:(UITableView *)tableView withStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)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; } - (instancetype)initForTableView:(UITableView *)tableView withReuseIdentifier:(NSString *)reuseIdentifier { return [self initForTableView:tableView withStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier]; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super initWithCoder:aDecoder]) { [self didInitializeWithStyle:UITableViewCellStyleDefault]; } return self; } // layoutSubviews 里不可以拿 textLabel 的 minX 来设置 separatorInset,如果要设置只能写死一个值,否则会导致 textLabel 的 minX 逐渐叠加从而使 textLabel 被移出屏幕外 - (void)layoutSubviews { [super layoutSubviews]; BOOL hasCustomAccessoryEdgeInset = self.accessoryView.superview && !UIEdgeInsetsEqualToEdgeInsets(self.accessoryEdgeInsets, UIEdgeInsetsZero); if (hasCustomAccessoryEdgeInset) { CGRect accessoryViewOldFrame = self.accessoryView.frame; accessoryViewOldFrame = CGRectSetX(accessoryViewOldFrame, CGRectGetMinX(accessoryViewOldFrame) - self.accessoryEdgeInsets.right); accessoryViewOldFrame = CGRectSetY(accessoryViewOldFrame, CGRectGetMinY(accessoryViewOldFrame) + self.accessoryEdgeInsets.top - self.accessoryEdgeInsets.bottom); self.accessoryView.frame = accessoryViewOldFrame; CGRect contentViewOldFrame = self.contentView.frame; contentViewOldFrame = CGRectSetWidth(contentViewOldFrame, CGRectGetMinX(accessoryViewOldFrame) - self.accessoryEdgeInsets.left); self.contentView.frame = contentViewOldFrame; } if (self.style == UITableViewCellStyleDefault || self.style == UITableViewCellStyleSubtitle) { BOOL hasCustomImageEdgeInsets = self.imageView.image && !UIEdgeInsetsEqualToEdgeInsets(self.imageEdgeInsets, UIEdgeInsetsZero); BOOL hasCustomTextLabelEdgeInsets = self.textLabel.text.length > 0 && !UIEdgeInsetsEqualToEdgeInsets(self.textLabelEdgeInsets, UIEdgeInsetsZero); BOOL shouldChangeDetailTextLabelFrame = self.style == UITableViewCellStyleSubtitle; BOOL hasCustomDetailLabelEdgeInsets = shouldChangeDetailTextLabelFrame && self.detailTextLabel.text.length > 0 && !UIEdgeInsetsEqualToEdgeInsets(self.detailTextLabelEdgeInsets, UIEdgeInsetsZero); CGRect imageViewFrame = self.imageView.frame; CGRect textLabelFrame = self.textLabel.frame; CGRect detailTextLabelFrame = self.detailTextLabel.frame; if (hasCustomImageEdgeInsets) { imageViewFrame.origin.x += self.imageEdgeInsets.left - self.imageEdgeInsets.right; imageViewFrame.origin.y += self.imageEdgeInsets.top - self.imageEdgeInsets.bottom; textLabelFrame.origin.x += self.imageEdgeInsets.left; textLabelFrame.size.width = fmin(CGRectGetWidth(textLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(textLabelFrame)); if (shouldChangeDetailTextLabelFrame) { detailTextLabelFrame.origin.x += self.imageEdgeInsets.left; detailTextLabelFrame.size.width = fmin(CGRectGetWidth(detailTextLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(detailTextLabelFrame)); } } if (hasCustomTextLabelEdgeInsets) { textLabelFrame.origin.x += self.textLabelEdgeInsets.left - self.textLabelEdgeInsets.right; textLabelFrame.origin.y += self.textLabelEdgeInsets.top - self.textLabelEdgeInsets.bottom; textLabelFrame.size.width = fmin(CGRectGetWidth(textLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(textLabelFrame)); } if (hasCustomDetailLabelEdgeInsets) { detailTextLabelFrame.origin.x += self.detailTextLabelEdgeInsets.left - self.detailTextLabelEdgeInsets.right; detailTextLabelFrame.origin.y += self.detailTextLabelEdgeInsets.top - self.detailTextLabelEdgeInsets.bottom; detailTextLabelFrame.size.width = fmin(CGRectGetWidth(detailTextLabelFrame), CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(detailTextLabelFrame)); } self.imageView.frame = imageViewFrame; self.textLabel.frame = textLabelFrame; self.detailTextLabel.frame = detailTextLabelFrame; } // 由于调整 accessoryEdgeInsets 可能会影响 contentView 的宽度,所以几个 subviews 的布局也要保护一下 if (hasCustomAccessoryEdgeInset) { if (CGRectGetMaxX(self.textLabel.frame) > CGRectGetWidth(self.contentView.bounds)) { self.textLabel.frame = CGRectSetWidth(self.textLabel.frame, CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(self.textLabel.frame)); } if (CGRectGetMaxX(self.detailTextLabel.frame) > CGRectGetWidth(self.contentView.bounds)) { self.detailTextLabel.frame = CGRectSetWidth(self.detailTextLabel.frame, CGRectGetWidth(self.contentView.bounds) - CGRectGetMinX(self.detailTextLabel.frame)); } } } // 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 = self.qmui_styledTextLabelColor; if (titleLabelColor) { self.textLabel.textColor = titleLabelColor; } UIColor *detailLabelColor = self.qmui_styledDetailTextLabelColor; if (detailLabelColor) { self.detailTextLabel.textColor = detailLabelColor; } } else { self.userInteractionEnabled = NO; UIColor *disabledColor = UIColorDisabled; if (disabledColor) { self.textLabel.textColor = disabledColor; self.detailTextLabel.textColor = disabledColor; } } _enabled = enabled; } } - (void)initDefaultAccessoryImageViewIfNeeded { if (!self.defaultAccessoryImageView) { self.defaultAccessoryImageView = [[UIImageView alloc] init]; self.defaultAccessoryImageView.contentMode = UIViewContentModeCenter; } } - (void)initDefaultAccessoryButtonIfNeeded { if (!self.defaultAccessoryButton) { self.defaultAccessoryButton = [[QMUIButton alloc] init]; [self.defaultAccessoryButton addTarget:self action:@selector(handleAccessoryButtonEvent:) forControlEvents:UIControlEventTouchUpInside]; self.defaultAccessoryButton.accessibilityLabel = @"更多信息"; } } - (void)initDefaultDetailDisclosureViewIfNeeded { if (!self.defaultDetailDisclosureView) { self.defaultDetailDisclosureView = [[UIView alloc] init]; } } // 重写accessoryType,如果是UITableViewCellAccessoryDisclosureIndicator类型的,则使用 QMUIConfigurationTemplate.m 配置表里的图片 - (void)setAccessoryType:(UITableViewCellAccessoryType)accessoryType { [super setAccessoryType:accessoryType]; if (accessoryType == UITableViewCellAccessoryDisclosureIndicator) { UIImage *indicatorImage = TableViewCellDisclosureIndicatorImage; if (indicatorImage) { [self initDefaultAccessoryImageViewIfNeeded]; self.defaultAccessoryImageView.image = indicatorImage; [self.defaultAccessoryImageView sizeToFit]; self.accessoryView = self.defaultAccessoryImageView; return; } } if (accessoryType == UITableViewCellAccessoryCheckmark) { UIImage *checkmarkImage = TableViewCellCheckmarkImage; if (checkmarkImage) { [self initDefaultAccessoryImageViewIfNeeded]; self.defaultAccessoryImageView.image = checkmarkImage; [self.defaultAccessoryImageView sizeToFit]; self.accessoryView = self.defaultAccessoryImageView; return; } } if (accessoryType == UITableViewCellAccessoryDetailButton) { UIImage *detailButtonImage = TableViewCellDetailButtonImage; if (detailButtonImage) { [self initDefaultAccessoryButtonIfNeeded]; [self.defaultAccessoryButton setImage:detailButtonImage forState:UIControlStateNormal]; [self.defaultAccessoryButton sizeToFit]; self.accessoryView = self.defaultAccessoryButton; return; } } if (accessoryType == UITableViewCellAccessoryDetailDisclosureButton) { UIImage *detailButtonImage = TableViewCellDetailButtonImage; UIImage *indicatorImage = TableViewCellDisclosureIndicatorImage; if (detailButtonImage) { QMUIAssert(!!indicatorImage, NSStringFromClass(self.class), @"TableViewCellDetailButtonImage 和 TableViewCellDisclosureIndicatorImage 必须同时使用,但目前后者为 nil"); [self initDefaultDetailDisclosureViewIfNeeded]; [self initDefaultAccessoryButtonIfNeeded]; [self.defaultAccessoryButton setImage:detailButtonImage forState:UIControlStateNormal]; [self.defaultAccessoryButton sizeToFit]; if (self.accessoryView == self.defaultAccessoryButton) { self.accessoryView = nil; } [self.defaultDetailDisclosureView addSubview:self.defaultAccessoryButton]; } if (indicatorImage) { QMUIAssert(!!detailButtonImage, NSStringFromClass(self.class), @"TableViewCellDetailButtonImage 和 TableViewCellDisclosureIndicatorImage 必须同时使用,但目前前者为 nil"); [self initDefaultDetailDisclosureViewIfNeeded]; [self initDefaultAccessoryImageViewIfNeeded]; self.defaultAccessoryImageView.image = indicatorImage; [self.defaultAccessoryImageView sizeToFit]; if (self.accessoryView == self.defaultAccessoryImageView) { self.accessoryView = nil; } [self.defaultDetailDisclosureView addSubview:self.defaultAccessoryImageView]; } if (indicatorImage && detailButtonImage) { CGFloat spacingBetweenDetailButtonAndIndicatorImage = TableViewCellSpacingBetweenDetailButtonAndDisclosureIndicator; self.defaultDetailDisclosureView.frame = CGRectFlatMake(CGRectGetMinX(self.defaultDetailDisclosureView.frame), CGRectGetMinY(self.defaultDetailDisclosureView.frame), CGRectGetWidth(self.defaultAccessoryButton.frame) + spacingBetweenDetailButtonAndIndicatorImage + CGRectGetWidth(self.defaultAccessoryImageView.frame), fmax(CGRectGetHeight(self.defaultAccessoryButton.frame), CGRectGetHeight(self.defaultAccessoryImageView.frame))); self.defaultAccessoryButton.frame = CGRectSetXY(self.defaultAccessoryButton.frame, 0, CGRectGetMinYVerticallyCenterInParentRect(self.defaultDetailDisclosureView.frame, self.defaultAccessoryButton.frame)); self.defaultAccessoryImageView.frame = CGRectSetXY(self.defaultAccessoryImageView.frame, CGRectGetMaxX(self.defaultAccessoryButton.frame) + spacingBetweenDetailButtonAndIndicatorImage, CGRectGetMinYVerticallyCenterInParentRect(self.defaultDetailDisclosureView.frame, self.defaultAccessoryImageView.frame)); self.accessoryView = self.defaultDetailDisclosureView; return; } } self.accessoryView = nil; } #pragma mark - // 为了修复因优化accessoryView导致的向左滑动cell容易触发accessoryView事件 a little dirty by molice - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { self.accessoryView.userInteractionEnabled = NO; } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { self.accessoryView.userInteractionEnabled = YES; } #pragma mark - Touch Event - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *view = [super hitTest:point withEvent:event]; if (!view) { return nil; } // 对于使用自定义的accessoryView的情况,扩大其响应范围。最小范围至少是一个靠在屏幕右边缘的“宽高都为cell高度”的正方形区域 if (self.accessoryView && !self.accessoryView.hidden && self.accessoryView.userInteractionEnabled && !self.editing // UISwitch被点击时,[super hitTest:point withEvent:event]返回的不是UISwitch,而是它的一个subview,如果这里直接返回UISwitch会导致控件无法使用,因此对UISwitch做特殊屏蔽 && ![self.accessoryView isKindOfClass:[UISwitch class]] ) { CGRect accessoryViewFrame = self.accessoryView.frame; CGRect responseEventFrame; responseEventFrame.origin.x = CGRectGetMinX(accessoryViewFrame) + self.accessoryHitTestEdgeInsets.left; responseEventFrame.origin.y = CGRectGetMinY(accessoryViewFrame) + self.accessoryHitTestEdgeInsets.top; responseEventFrame.size.width = CGRectGetWidth(accessoryViewFrame) + UIEdgeInsetsGetHorizontalValue(self.accessoryHitTestEdgeInsets); responseEventFrame.size.height = CGRectGetHeight(accessoryViewFrame) + UIEdgeInsetsGetVerticalValue(self.accessoryHitTestEdgeInsets); if (CGRectContainsPoint(responseEventFrame, point)) { return self.accessoryView; } } return view; } - (void)handleAccessoryButtonEvent:(QMUIButton *)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]]; } } @end @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.qmui_tableView) { QMUITableViewCellPosition position = [self.qmui_tableView qmui_positionForRowAtIndexPath:indexPath]; self.cellPosition = position; } else { self.cellPosition = QMUITableViewCellPositionNone; } } @end ================================================ FILE: QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITableViewProtocols.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITestView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITestView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITextField.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITextField.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITextView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITextView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/QMUITheme.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 */ ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManagerCenter.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManagerCenter.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/UIViewController+QMUITheme.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/UIViewController+QMUITheme.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/UIVisualEffect+QMUITheme.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITheme/UIVisualEffect+QMUITheme.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITips.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUITips.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIWeakObjectContainer.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIWeakObjectContainer.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIZoomImageView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/QMUIZoomImageView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellData.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 2017/6/20. // #import #import @class QMUIStaticTableViewCellData; @class QMUIStaticTableViewCellDataSource; @class QMUITableViewCell; /** * 这个控件是为了方便地实现那种类似设置界面的列表(每个 cell 的样式、内容、操作控件均不太一样,每个 cell 之间不复用),使用方式: * 1. 创建一个带 UITableView 的 viewController。 * 2. 通过 init 或 initWithCellDataSections: 创建一个 dataSource。若通过 init 方法初始化,则请在 tableView 渲染前(viewDidLoad 或更早)手动设置一个 cellDataSections 数组。 * 3. 将第 2 步里的 dataSource 赋值给 tableView.qmui_staticCellDataSource 即可完成一般情况下的界面展示。 * 4. 若需要重写某些 UITableViewDataSource、UITableViewDelegate 方法,则在 viewController 里直接实现该方法,并在方法里调用 QMUIStaticTableViewCellDataSource (Manual) 提供的同名方法即可,具体可参考 QMUI Demo。 */ @interface QMUIStaticTableViewCellDataSource : NSObject /// 列表的数据源,是一个二维数组,其中一维表示 section,二维表示某个 section 里的 rows,每次调用这个属性的 setter 方法都会自动刷新 tableView 内容。 @property(nonatomic, copy) NSArray *> *cellDataSections; /// 数据源绑定到的列表,在 UITableView (QMUI_StaticCell) 里会被赋值 @property(nonatomic, weak, readonly) UITableView *tableView; - (instancetype)init NS_DESIGNATED_INITIALIZER; - (instancetype)initWithCellDataSections:(NSArray *> *)cellDataSections NS_DESIGNATED_INITIALIZER; @end /// 当需要重写某些 UITableViewDataSource、UITableViewDelegate 方法时,这个分类里提供的同名方法需要在该方法中被调用,否则可能导致 QMUIStaticTableViewCellData 里设置的一些值无效。 @interface QMUIStaticTableViewCellDataSource (Manual) /** * 从 dataSource 里获取处于 indexPath 位置的 QMUIStaticTableViewCellData 对象 * @param indexPath cell 所处的位置 */ - (QMUIStaticTableViewCellData *)cellDataAtIndexPath:(NSIndexPath *)indexPath; /** * 根据 dataSource 计算出指定的 indexPath 的 cell 所对应的 reuseIdentifier(static tableView 里一般每个 cell 的 reuseIdentifier 都是不一样的,避免复用) * @param indexPath cell 所处的位置 */ - (NSString *)reuseIdentifierForCellAtIndexPath:(NSIndexPath *)indexPath; /** * 用于结合 indexPath 和 dataSource 生成 cell 的方法,其中 cell 使用的是 QMUITableViewCell * @prama indexPath 当前 cell 的 indexPath */ - (__kindof QMUITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath; /** * 从 dataSource 里获取指定位置的 cell 的高度 * @prama indexPath 当前 cell 的 indexPath * @return 该位置的 cell 的高度 */ - (CGFloat)heightForRowAtIndexPath:(NSIndexPath *)indexPath; /** * 在 tableView:didSelectRowAtIndexPath: 里调用,可从 dataSource 里读取对应 indexPath 的 cellData,然后触发其中的 target 和 action * @param indexPath 当前 cell 的 indexPath */ - (void)didSelectRowAtIndexPath:(NSIndexPath *)indexPath; /** * 在 tableView:accessoryButtonTappedForRowWithIndexPath: 里调用,可从 dataSource 里读取对应 indexPath 的 cellData,然后触发其中的 target 和 action * @param indexPath 当前 cell 的 indexPath */ - (void)accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath; @end ================================================ FILE: QMUIKit/QMUIComponents/StaticTableView/QMUIStaticTableViewCellDataSource.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/StaticTableView/UITableView+QMUIStaticCell.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ToastView/QMUIToastAnimator.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ToastView/QMUIToastBackgroundView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ToastView/QMUIToastContentView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ToastView/QMUIToastView.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIComponents/ToastView/QMUIToastView.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUICore/QMUICommonDefines.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 */ ================================================ FILE: QMUIKit/QMUICore/QMUIConfiguration.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUICore/QMUIConfiguration.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUICore/QMUIConfigurationMacros.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 下正常控制滚动条的位置。 ================================================ FILE: QMUIKit/QMUICore/QMUICore.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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" ================================================ FILE: QMUIKit/QMUICore/QMUIHelper.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUICore/QMUIHelper.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUICore/QMUILab.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 */ ================================================ FILE: QMUIKit/QMUICore/QMUIRuntime.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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); ================================================ FILE: QMUIKit/QMUICore/QMUIRuntime.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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; } ================================================ FILE: QMUIKit/QMUIKit.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 = @"4.8.0"; #if __has_include("CAAnimation+QMUI.h") #import "CAAnimation+QMUI.h" #endif #if __has_include("CALayer+QMUI.h") #import "CALayer+QMUI.h" #endif #if __has_include("CALayer+QMUIViewAnimation.h") #import "CALayer+QMUIViewAnimation.h" #endif #if __has_include("NSArray+QMUI.h") #import "NSArray+QMUI.h" #endif #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" #endif #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" #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" #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" #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" #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" #endif #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 */ ================================================ FILE: QMUIKit/QMUIMainFrame/QMUICommonTableViewController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIMainFrame/QMUICommonViewController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIMainFrame/QMUICommonViewController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIMainFrame/QMUINavigationController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIMainFrame/QMUINavigationController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIMainFrame/QMUITabBarViewController.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIMainFrame/QMUITabBarViewController.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16.imageset/Contents.json ================================================ { "images" : [ { "filename" : "QMUI_checkbox16.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_checked.imageset/Contents.json ================================================ { "images" : [ { "filename" : "QMUI_checkbox16_checked.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_disabled.imageset/Contents.json ================================================ { "images" : [ { "filename" : "QMUI_checkbox16_disabled.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_checkbox16_indeterminate.imageset/Contents.json ================================================ { "images" : [ { "filename" : "QMUI_checkbox16_indeterminate.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_console_clear.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_console_clear.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_console_filter.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_console_filter_selected.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_console_filter_selected.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_console_logo.imageset/Contents.json ================================================ { "images" : [ { "filename" : "QMUI_console_logo.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_emotion_delete.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_emotion_delete.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_hiddenAlbum.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_hiddenAlbum.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_icloud_download_fault.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_icloud_download_fault.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_pickerImage_checkbox.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_checkbox_checked.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_pickerImage_checkbox_checked.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_favorite.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_pickerImage_favorite.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_pickerImage_video_mark.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_pickerImage_video_mark.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_previewImage_checkbox.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_previewImage_checkbox_checked.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_previewImage_checkbox_checked.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_done.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_tips_done.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_error.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_tips_error.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/QMUIResources/Images.xcassets/QMUI_tips_info.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "QMUI_tips_info.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: QMUIKit/UIKitExtensions/CALayer+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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 上 */ - (void)qmui_sendSublayerToBack:(CALayer *)sublayer; /** * 把某个 sublayer 移动到当前所有 sublayers 的最前面 * @param sublayer 要被移动的layer * @warning 要被移动的 sublayer 必须已经添加到当前 layer 上 */ - (void)qmui_bringSublayerToFront:(CALayer *)sublayer; /** * 移除 CALayer(包括 CAShapeLayer 和 CAGradientLayer)所有支持动画的属性的默认动画,方便需要一个不带动画的 layer 时使用。 */ - (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 */ + (CALayer *)qmui_separatorLayer; /** * 产生一个适用于做列表分隔线的 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 ================================================ FILE: QMUIKit/UIKitExtensions/CALayer+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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) 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 调用则需要在主线程才行 } } } } 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 { NSMutableDictionary> *actions = @{NSStringFromSelector(@selector(bounds)): [NSNull null], NSStringFromSelector(@selector(position)): [NSNull null], NSStringFromSelector(@selector(zPosition)): [NSNull null], NSStringFromSelector(@selector(anchorPoint)): [NSNull null], NSStringFromSelector(@selector(anchorPointZ)): [NSNull null], NSStringFromSelector(@selector(transform)): [NSNull null], BeginIgnoreClangWarning(-Wundeclared-selector) NSStringFromSelector(@selector(hidden)): [NSNull null], NSStringFromSelector(@selector(doubleSided)): [NSNull null], EndIgnoreClangWarning NSStringFromSelector(@selector(sublayerTransform)): [NSNull null], NSStringFromSelector(@selector(masksToBounds)): [NSNull null], NSStringFromSelector(@selector(contents)): [NSNull null], NSStringFromSelector(@selector(contentsRect)): [NSNull null], NSStringFromSelector(@selector(contentsScale)): [NSNull null], NSStringFromSelector(@selector(contentsCenter)): [NSNull null], NSStringFromSelector(@selector(minificationFilterBias)): [NSNull null], NSStringFromSelector(@selector(backgroundColor)): [NSNull null], NSStringFromSelector(@selector(cornerRadius)): [NSNull null], NSStringFromSelector(@selector(borderWidth)): [NSNull null], NSStringFromSelector(@selector(borderColor)): [NSNull null], NSStringFromSelector(@selector(opacity)): [NSNull null], NSStringFromSelector(@selector(compositingFilter)): [NSNull null], NSStringFromSelector(@selector(filters)): [NSNull null], NSStringFromSelector(@selector(backgroundFilters)): [NSNull null], NSStringFromSelector(@selector(shouldRasterize)): [NSNull null], NSStringFromSelector(@selector(rasterizationScale)): [NSNull null], NSStringFromSelector(@selector(shadowColor)): [NSNull null], NSStringFromSelector(@selector(shadowOpacity)): [NSNull null], NSStringFromSelector(@selector(shadowOffset)): [NSNull null], NSStringFromSelector(@selector(shadowRadius)): [NSNull null], NSStringFromSelector(@selector(shadowPath)): [NSNull null], NSStringFromSelector(@selector(maskedCorners)): [NSNull null], }.mutableCopy; if ([self isKindOfClass:[CAShapeLayer class]]) { [actions addEntriesFromDictionary:@{NSStringFromSelector(@selector(path)): [NSNull null], NSStringFromSelector(@selector(fillColor)): [NSNull null], NSStringFromSelector(@selector(strokeColor)): [NSNull null], NSStringFromSelector(@selector(strokeStart)): [NSNull null], NSStringFromSelector(@selector(strokeEnd)): [NSNull null], NSStringFromSelector(@selector(lineWidth)): [NSNull null], NSStringFromSelector(@selector(miterLimit)): [NSNull null], NSStringFromSelector(@selector(lineDashPhase)): [NSNull null]}]; } if ([self isKindOfClass:[CAGradientLayer class]]) { [actions addEntriesFromDictionary:@{NSStringFromSelector(@selector(colors)): [NSNull null], NSStringFromSelector(@selector(locations)): [NSNull null], NSStringFromSelector(@selector(startPoint)): [NSNull null], NSStringFromSelector(@selector(endPoint)): [NSNull null]}]; } 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]; layer.backgroundColor = UIColorSeparator.CGColor; layer.frame = CGRectMake(0, 0, 0, PixelOne); return layer; } + (CALayer *)qmui_separatorLayerForTableView { CALayer *layer = [self qmui_separatorLayer]; layer.backgroundColor = TableViewSeparatorColor.CGColor; return layer; } @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 ================================================ FILE: QMUIKit/UIKitExtensions/NSArray+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSArray+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSAttributedString+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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) /** * @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; /** * @brief 创建一个包含图片的 attributedString * @param image 要用的图片 * @param offset 图片相对基线的垂直偏移(当 offset > 0 时,图片会向上偏移) * @param leftMargin 图片距离左侧内容的间距 * @param rightMargin 图片距离右侧内容的间距 * @note leftMargin 和 rightMargin 必须大于或等于 0 */ + (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 ================================================ FILE: QMUIKit/UIKitExtensions/NSAttributedString+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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) + (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_attributedStringWithFixedSpace:(CGFloat)width { UIGraphicsBeginImageContext(CGSizeMake(width, 1)); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return [self qmui_attributedStringWithImage:image]; } - (NSTextAlignment)qmui_textAlignment { if (!self.length) return NSTextAlignmentLeft; NSParagraphStyle *p = [self attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:nil]; if (!p) return NSTextAlignmentLeft; NSTextAlignment alignment = p.alignment; return alignment; } #pragma mark - - (NSUInteger)qmui_lengthWhenCountingNonASCIICharacterAsTwo { return self.string.qmui_lengthWhenCountingNonASCIICharacterAsTwo; } - (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { return [QMUIStringPrivate substring:self avoidBreakingUpCharacterSequencesFromIndex:index lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; } - (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index { return [self qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:index lessValue:YES countingNonASCIICharacterAsTwo:NO]; } - (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { return [QMUIStringPrivate substring:self avoidBreakingUpCharacterSequencesToIndex:index lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; } - (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index { return [self qmui_substringAvoidBreakingUpCharacterSequencesToIndex:index lessValue:YES countingNonASCIICharacterAsTwo:NO]; } - (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo { return [QMUIStringPrivate substring:self avoidBreakingUpCharacterSequencesWithRange:range lessValue:lessValue countingNonASCIICharacterAsTwo:countingNonASCIICharacterAsTwo]; } - (instancetype)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range { return [self qmui_substringAvoidBreakingUpCharacterSequencesWithRange:range lessValue:YES countingNonASCIICharacterAsTwo:NO]; } - (instancetype)qmui_stringByRemoveCharacterAtIndex:(NSUInteger)index { return [QMUIStringPrivate string:self avoidBreakingUpCharacterSequencesByRemoveCharacterAtIndex:index]; } - (instancetype)qmui_stringByRemoveLastCharacter { return [self qmui_stringByRemoveCharacterAtIndex:self.length - 1]; } @end @implementation NSMutableAttributedString (QMUI) - (void)qmui_applyParagraphStyle:(void (^)(NSMutableParagraphStyle * _Nonnull, NSRange))block { if (!self.length || !block) return; __block BOOL applied = NO; [self enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, self.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSParagraphStyle * _Nullable value, NSRange range, BOOL * _Nonnull stop) { applied = YES; NSMutableParagraphStyle *p = value.mutableCopy; block(p, range); [self addAttribute:NSParagraphStyleAttributeName value:p.copy range:range]; }]; if (!applied) { NSMutableParagraphStyle *p = NSMutableParagraphStyle.new; NSRange range = NSMakeRange(0, self.length); block(p, range); [self addAttribute:NSParagraphStyleAttributeName value:p.copy range:range]; } } - (void)setQmui_textAlignment:(NSTextAlignment)qmui_textAlignment { [self qmui_applyParagraphStyle:^(NSMutableParagraphStyle * _Nonnull aParagraphStyle, NSRange aRange) { aParagraphStyle.alignment = qmui_textAlignment; }]; } @end @implementation UIImage (QMUI_NSAttributedStringSupports) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ 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; }; }); }); } + (UIImage *)qmui_imageWithAttributedString:(NSAttributedString *)attributedString { CGSize stringSize = [attributedString boundingRectWithSize:CGSizeMax options:NSStringDrawingUsesLineFragmentOrigin context:nil].size; stringSize = CGSizeCeil(stringSize); UIImage *image = [UIImage qmui_imageWithSize:stringSize opaque:NO scale:0 actions:^(CGContextRef contextRef) { [attributedString drawInRect:CGRectMakeWithSize(stringSize)]; }]; [image qmui_bindObject:attributedString.copy forKey:kQMUIImageOriginalAttributedStringKey]; return image; } - (NSAttributedString *)qmui_attributedString { return [self qmui_getBoundObjectForKey:kQMUIImageOriginalAttributedStringKey]; } - (NSDictionary *)qmui_stringAttributes { NSAttributedString *string = self.qmui_attributedString; NSRange range = NSMakeRange(0, string.length); return [[self qmui_attributedString] attributesAtIndex:0 effectiveRange:&range]; } @end @implementation QMUIHelper (NSAttributedStringSupports) + (CGFloat)topMarginForAttributedImage:(UIImage *)image attributes:(NSDictionary *)attributes { if (!image || !attributes) return 0; CGFloat marginTop = 0; CGFloat fontCapHeight = ({ UIFont *font = attributes[NSFontAttributeName]; font ? font.capHeight : 0; }); CGFloat fontLineHeight = ({ UIFont *font = attributes[NSFontAttributeName]; font ? font.lineHeight : 0; }); CGFloat lineHeight = ({ NSParagraphStyle *paragraphStyle = attributes[NSParagraphStyleAttributeName]; paragraphStyle ? paragraphStyle.maximumLineHeight : 0; }); CGFloat imageHeight = image.size.height; if (fontCapHeight) { marginTop = -(fontCapHeight - imageHeight) / 2; } if (fontLineHeight && lineHeight) { marginTop -= (lineHeight - fontLineHeight) / 2; } return marginTop; } @end ================================================ FILE: QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSCharacterSet+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSDictionary+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSDictionary+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSMethodSignature+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSNumber+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSNumber+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSObject+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 2016/11/1. // #import #import NS_ASSUME_NONNULL_BEGIN @interface NSObject (QMUI) /** 判断当前类是否有重写某个父类的指定方法 @param selector 要判断的方法 @param superclass 要比较的父类,必须是当前类的某个 superclass @return YES 表示子类有重写了父类方法,NO 表示没有重写(异常情况也返回 NO,例如当前类与指定的类并非父子关系、父类本身也无法响应指定的方法) */ - (BOOL)qmui_hasOverrideMethod:(SEL)selector ofSuperclass:(Class)superclass; /** 判断指定的类是否有重写某个父类的指定方法 @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 */ - (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 */ - (nullable id)qmui_performSelectorToSuperclass:(SEL)aSelector withObject:(nullable id)object; /** * 调用一个无参数、返回值类型为非对象的 selector。如果返回值类型为对象,请直接使用系统的 performSelector: 方法。 * @param selector 要被调用的方法名 * @param returnValue selector 的返回值的指针地址,请先定义一个变量再将其指针地址传进来,例如 &result * * @code * CGFloat alpha; * [view qmui_performSelector:@selector(alpha) withPrimitiveReturnValue:&alpha]; * @endcode */ - (void)qmui_performSelector:(SEL)selector withPrimitiveReturnValue:(nullable void *)returnValue; /** * 调用一个带参数的 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 */ - (nullable id)qmui_performSelector:(SEL)selector withArguments:(nullable void *)firstArgument, ...; /** * 调用一个返回值类型为非对象且带参数的 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSObject+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 2016/11/1. // #import "NSObject+QMUI.h" #import "QMUIWeakObjectContainer.h" #import "QMUICore.h" #import "NSString+QMUI.h" #import @implementation NSObject (QMUI) - (BOOL)qmui_hasOverrideMethod:(SEL)selector ofSuperclass:(Class)superclass { return [NSObject qmui_hasOverrideMethod:selector forClass:self.class ofSuperclass:superclass]; } + (BOOL)qmui_hasOverrideMethod:(SEL)selector forClass:(Class)aClass ofSuperclass:(Class)superclass { if (![aClass isSubclassOfClass:superclass]) { return NO; } if (![superclass instancesRespondToSelector:selector]) { return NO; } Method superclassMethod = class_getInstanceMethod(superclass, selector); Method instanceMethod = class_getInstanceMethod(aClass, selector); if (!instanceMethod || instanceMethod == superclassMethod) { return NO; } return YES; } - (id)qmui_performSelectorToSuperclass:(SEL)aSelector { struct objc_super mySuper; mySuper.receiver = self; mySuper.super_class = class_getSuperclass(object_getClass(self)); id (*objc_superAllocTyped)(struct objc_super *, SEL) = (void *)&objc_msgSendSuper; return (*objc_superAllocTyped)(&mySuper, aSelector); } - (id)qmui_performSelectorToSuperclass:(SEL)aSelector withObject:(id)object { struct objc_super mySuper; mySuper.receiver = self; mySuper.super_class = class_getSuperclass(object_getClass(self)); id (*objc_superAllocTyped)(struct objc_super *, SEL, ...) = (void *)&objc_msgSendSuper; return (*objc_superAllocTyped)(&mySuper, aSelector, object); } - (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 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(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++) { struct objc_method_description methodDescription = methods[i]; if (block) { block(methodDescription.name); } } free(methods); } @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 ================================================ FILE: QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 16/8/9. // #import #import @interface NSParagraphStyle (QMUI) /** * 快速创建一个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对象 */ + (instancetype)qmui_paragraphStyleWithLineHeight:(CGFloat)lineHeight; /** * 快速创建一个NSMutableParagraphStyle,等同于`qmui_paragraphStyleWithLineHeight:lineBreakMode:textAlignment:NSTextAlignmentLeft` * @param lineHeight 行高 * @param lineBreakMode 换行模式 * @return 一个NSMutableParagraphStyle对象 */ + (instancetype)qmui_paragraphStyleWithLineHeight:(CGFloat)lineHeight lineBreakMode:(NSLineBreakMode)lineBreakMode; /** * 快速创建一个NSMutableParagraphStyle * @param lineHeight 行高 * @param lineBreakMode 换行模式 * @param textAlignment 文本对齐方式 * @return 一个NSMutableParagraphStyle对象 */ + (instancetype)qmui_paragraphStyleWithLineHeight:(CGFloat)lineHeight lineBreakMode:(NSLineBreakMode)lineBreakMode textAlignment:(NSTextAlignment)textAlignment; @end ================================================ FILE: QMUIKit/UIKitExtensions/NSParagraphStyle+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 16/8/9. // #import "NSParagraphStyle+QMUI.h" @implementation NSParagraphStyle (QMUI) + (instancetype)qmui_paragraphStyleWithLineHeight:(CGFloat)lineHeight { return [self qmui_paragraphStyleWithLineHeight:lineHeight lineBreakMode:NSLineBreakByWordWrapping textAlignment:NSTextAlignmentLeft]; } + (instancetype)qmui_paragraphStyleWithLineHeight:(CGFloat)lineHeight lineBreakMode:(NSLineBreakMode)lineBreakMode { return [self qmui_paragraphStyleWithLineHeight:lineHeight lineBreakMode:lineBreakMode textAlignment:NSTextAlignmentLeft]; } + (instancetype)qmui_paragraphStyleWithLineHeight:(CGFloat)lineHeight lineBreakMode:(NSLineBreakMode)lineBreakMode textAlignment:(NSTextAlignment)textAlignment { 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; paragraphStyle.alignment = textAlignment; return paragraphStyle; } @end ================================================ FILE: QMUIKit/UIKitExtensions/NSPointerArray+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSPointerArray+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSRegularExpression+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSRegularExpression+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSShadow+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSShadow+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSString+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import #import NS_ASSUME_NONNULL_BEGIN @protocol QMUIStringProtocol /** * 按照中文 2 个字符、英文 1 个字符的方式来计算文本长度 */ @property(readonly) NSUInteger qmui_lengthWhenCountingNonASCIICharacterAsTwo; /** * 将字符串从指定的 index 开始裁剪到结尾,裁剪时会避免将 emoji 等 "character sequences" 拆散(一个 emoji 表情占用1-4个长度的字符)。 * * 例如对于字符串“😊😞”,它的长度为4,若调用 [string qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:1],将返回“😊😞”。 * 若调用系统的 [string substringFromIndex:1],将返回“?😞”。(?表示乱码,因为第一个 emoji 表情被从中间裁开了)。 * * @param index 要从哪个 index 开始裁剪文字,如果 countingNonASCIICharacterAsTwo 为 YES,则 index 也要按 YES 的方式来算 * @param lessValue 要按小的长度取,还是按大的长度取 * @param countingNonASCIICharacterAsTwo 是否按照 英文 1 个字符长度、中文 2 个字符长度的方式来裁剪 * @return 裁剪完的字符 */ - (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; /** * 相当于 `qmui_substringAvoidBreakingUpCharacterSequencesFromIndex: lessValue:YES` countingNonASCIICharacterAsTwo:NO * @see qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:lessValue:countingNonASCIICharacterAsTwo: */ - (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesFromIndex:(NSUInteger)index; /** * 将字符串从开头裁剪到指定的 index,裁剪时会避免将 emoji 等 "character sequences" 拆散(一个 emoji 表情占用1-4个长度的字符)。 * * 例如对于字符串“😊😞”,它的长度为4,若调用 [string qmui_substringAvoidBreakingUpCharacterSequencesToIndex:1 lessValue:NO countingNonASCIICharacterAsTwo:NO],将返回“😊”。 * 若调用系统的 [string substringToIndex:1],将返回“?”。(?表示乱码,因为第一个 emoji 表情被从中间裁开了)。 * * @param index 要裁剪到哪个 index 为止(不包含该 index,策略与系统的 substringToIndex: 一致),如果 countingNonASCIICharacterAsTwo 为 YES,则 index 也要按 YES 的方式来算 * @param lessValue 裁剪时若遇到“character sequences”,是向下取整还是向上取整。 * @param countingNonASCIICharacterAsTwo 是否按照 英文 1 个字符长度、中文 2 个字符长度的方式来裁剪 * @return 裁剪完的字符 */ - (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; /** * 相当于 `qmui_substringAvoidBreakingUpCharacterSequencesToIndex:lessValue:YES` countingNonASCIICharacterAsTwo:NO * @see qmui_substringAvoidBreakingUpCharacterSequencesToIndex:lessValue:countingNonASCIICharacterAsTwo: */ - (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesToIndex:(NSUInteger)index; /** * 将字符串里指定 range 的子字符串裁剪出来,会避免将 emoji 等 "character sequences" 拆散(一个 emoji 表情占用1-4个长度的字符)。 * * 例如对于字符串“😊😞”,它的长度为4,在 lessValue 模式下,裁剪 (0, 1) 得到的是空字符串,裁剪 (0, 2) 得到的是“😊”。 * 在非 lessValue 模式下,裁剪 (0, 1) 或 (0, 2),得到的都是“😊”。 * * @param range 要裁剪的文字位置 * @param lessValue 裁剪时若遇到“character sequences”,是向下取整还是向上取整(系统的 rangeOfComposedCharacterSequencesForRange: 会尽量把给定 range 里包含的所有 character sequences 都包含在内,也即 lessValue = NO)。 * @param countingNonASCIICharacterAsTwo 是否按照 英文 1 个字符长度、中文 2 个字符长度的方式来裁剪 * @return 裁剪完的字符 */ - (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range lessValue:(BOOL)lessValue countingNonASCIICharacterAsTwo:(BOOL)countingNonASCIICharacterAsTwo; /** * 相当于 `qmui_substringAvoidBreakingUpCharacterSequencesWithRange:lessValue:YES` countingNonASCIICharacterAsTwo:NO * @see qmui_substringAvoidBreakingUpCharacterSequencesWithRange:lessValue:countingNonASCIICharacterAsTwo: */ - (nullable instancetype)qmui_substringAvoidBreakingUpCharacterSequencesWithRange:(NSRange)range; /** * 移除指定位置的字符,可兼容emoji表情的情况(一个emoji表情占1-4个length) * @param index 要删除的位置 */ - (nullable instancetype)qmui_stringByRemoveCharacterAtIndex:(NSUInteger)index; /** * 移除最后一个字符,可兼容emoji表情的情况(一个emoji表情占1-4个length) * @see `qmui_stringByRemoveCharacterAtIndex:` */ - (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) + (NSString *)qmui_stringWithNSInteger:(NSInteger)integerValue; + (NSString *)qmui_stringWithCGFloat:(CGFloat)floatValue; + (NSString *)qmui_stringWithCGFloat:(CGFloat)floatValue decimal:(NSUInteger)decimal; @end NS_ASSUME_NONNULL_END ================================================ FILE: QMUIKit/UIKitExtensions/NSString+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import "NSString+QMUI.h" #import #import "QMUICore.h" #import "NSArray+QMUI.h" #import "NSCharacterSet+QMUI.h" #import "QMUIStringPrivate.h" #import "NSRegularExpression+QMUI.h" @implementation NSString (QMUI) - (NSArray *)qmui_toArray { if (!self.length) { return nil; } NSMutableArray *array = [[NSMutableArray alloc] init]; for (NSInteger i = 0; i < self.length; i++) { NSString *stringItem = [self substringWithRange:NSMakeRange(i, 1)]; [array addObject:stringItem]; } return [array copy]; } - (NSArray *)qmui_toTrimmedArray { return [[self qmui_toArray] qmui_filterWithBlock:^BOOL(NSString *item) { return item.qmui_trim.length > 0; }]; } - (NSString *)qmui_trim { NSMutableCharacterSet * characterSet = [NSMutableCharacterSet whitespaceAndNewlineCharacterSet]; [characterSet addCharactersInString:@"\0"]; return [self stringByTrimmingCharactersInSet:characterSet]; } - (NSString *)qmui_trimAllWhiteSpace { return [self stringByReplacingOccurrencesOfString:@"\\s" withString:@"" options:NSRegularExpressionSearch range:NSMakeRange(0, self.length)]; } - (NSString *)qmui_trimLineBreakCharacter { return [self stringByReplacingOccurrencesOfString:@"[\r\n]" withString:@" " options:NSRegularExpressionSearch range:NSMakeRange(0, self.length)]; } - (NSString *)qmui_md5 { const char *cStr = [self UTF8String]; unsigned char result[CC_MD5_DIGEST_LENGTH]; BeginIgnoreDeprecatedWarning CC_MD5(cStr, (CC_LONG)strlen(cStr), result); EndIgnoreDeprecatedWarning return [NSString stringWithFormat: @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", result[0], result[1], result[2], result[3], 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 { QMUIAssert(integer < 16, @"NSString (QMUI)", @"%s 参数仅接受小于16的值,当前传入的是 %@", __func__, @(integer)); NSString *letter = nil; switch (integer) { case 10: letter = @"A"; break; case 11: letter = @"B"; break; case 12: letter = @"C"; break; case 13: letter = @"D"; break; case 14: letter = @"E"; break; case 15: letter = @"F"; break; default: letter = [[NSString alloc]initWithFormat:@"%@", @(integer)]; break; } return letter; } + (NSString *)qmui_hexStringWithInteger:(NSInteger)integer { NSString *hexString = @""; NSInteger remainder = 0; for (NSInteger i = 0; i < 9; i++) { remainder = integer % 16; integer = integer / 16; NSString *letter = [self hexLetterStringWithInteger:remainder]; hexString = [letter stringByAppendingString:hexString]; if (integer == 0) { break; } } return hexString; } + (NSString *)qmui_stringByConcat:(id)firstArgv, ... { if (firstArgv) { NSMutableString *result = [[NSMutableString alloc] initWithFormat:@"%@", firstArgv]; va_list argumentList; va_start(argumentList, firstArgv); id argument; while ((argument = va_arg(argumentList, id))) { [result appendFormat:@"%@", argument]; } va_end(argumentList); return [result copy]; } return nil; } + (NSString *)qmui_timeStringWithMinsAndSecsFromSecs:(double)seconds { NSUInteger min = floor(seconds / 60); NSUInteger sec = floor(seconds - min * 60); return [NSString stringWithFormat:@"%02ld:%02ld", (long)min, (long)sec]; } - (NSString *)qmui_removeMagicalChar { if (self.length == 0) { return self; } NSRegularExpression *regex = [NSRegularExpression qmui_cachedRegularExpressionWithPattern:@"[\u0300-\u036F]"]; NSString *modifiedString = [regex stringByReplacingMatchesInString:self options:NSMatchingReportProgress range:NSMakeRange(0, self.length) withTemplate:@""]; return modifiedString; } - (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 nil; } - (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]; } } 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]; } #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)) { length += 1; } else { length += 2; } } return length; } - (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 NSString (QMUI_StringFormat) + (NSString *)qmui_stringWithNSInteger:(NSInteger)integerValue { return @(integerValue).stringValue; } + (NSString *)qmui_stringWithCGFloat:(CGFloat)floatValue { return [NSString qmui_stringWithCGFloat:floatValue decimal:2]; } + (NSString *)qmui_stringWithCGFloat:(CGFloat)floatValue decimal:(NSUInteger)decimal { NSString *formatString = [NSString stringWithFormat:@"%%.%@f", @(decimal)]; return [NSString stringWithFormat:formatString, floatValue]; } @end ================================================ FILE: QMUIKit/UIKitExtensions/NSURL+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/NSURL+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocol.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/QMUIBarProtocol/QMUIBarProtocolPrivate.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/QMUIStringPrivate.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/QMUIStringPrivate.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import NS_ASSUME_NONNULL_BEGIN /** 内部通过重写系统方法来让 UIActivityIndicatorView 支持 setFrame: 方式修改尺寸,业务就像使用一个普通 UIView 一样去使用它即可。 */ @interface UIActivityIndicatorView (QMUI) /// 内部转圈的那个 imageView @property(nonatomic, strong, readonly) UIImageView *qmui_animatingView; @end NS_ASSUME_NONNULL_END ================================================ FILE: QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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) 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 nil; } @end ================================================ FILE: QMUIKit/UIKitExtensions/UIApplication+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIApplication+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIBarItem+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIBarItem+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIBezierPath+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 16/8/9. // #import #import NS_ASSUME_NONNULL_BEGIN @interface UIBezierPath (QMUI) /** * 创建一条支持四个角的圆角值不相同的路径 * @param rect 路径的rect * @param cornerRadius 圆角大小的数字,长度必须为4,顺序分别为[左上角、左下角、右下角、右上角] * @param lineWidth 描边的大小,如果不需要描边(例如path是用于fill而不是用于stroke),则填0 */ + (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 ================================================ FILE: QMUIKit/UIKitExtensions/UIBezierPath+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 16/8/9. // #import "UIBezierPath+QMUI.h" @implementation UIBezierPath (QMUI) + (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 = [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)]; [path addArcWithCenter:CGPointMake(bottomLeftCornerRadius, CGRectGetHeight(rect) - bottomLeftCornerRadius) radius:bottomLeftCornerRadius - lineCenter startAngle:M_PI endAngle:M_PI * 0.5 clockwise:NO]; [path addLineToPoint:CGPointMake(CGRectGetWidth(rect) - bottomRightCornerRadius, CGRectGetHeight(rect) - lineCenter)]; [path addArcWithCenter:CGPointMake(CGRectGetWidth(rect) - bottomRightCornerRadius, CGRectGetHeight(rect) - bottomRightCornerRadius) radius:bottomRightCornerRadius - lineCenter startAngle:M_PI * 0.5 endAngle:0.0 clockwise:NO]; [path addLineToPoint:CGPointMake(CGRectGetWidth(rect) - lineCenter, topRightCornerRadius)]; [path addArcWithCenter:CGPointMake(CGRectGetWidth(rect) - topRightCornerRadius, topRightCornerRadius) radius:topRightCornerRadius - lineCenter startAngle:0.0 endAngle:M_PI * 1.5 clockwise:NO]; [path closePath]; 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIBlurEffect+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIBlurEffect+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIButton+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import NS_ASSUME_NONNULL_BEGIN @interface UIButton (QMUI) - (instancetype)qmui_initWithImage:(nullable UIImage *)image title:(nullable NSString *)title; /** * 在UIButton的样式(如字体)设置完后,将button的text设置为一个测试字符,再调用sizeToFit,从而令button的高度适应字体 * @warning 会调用setText:forState:,因此请确保在设置完按钮的样式之后、设置text之前调用 */ - (void)qmui_calculateHeightAfterSetAppearance; /** * 通过这个方法设置了 attributes 之后,setTitle:forState: 会自动把文字转成 attributedString 再添加上去,无需每次都自己构造 attributedString * @note 即使先调用 setTitle:forState: 然后再调用这个方法,之前的 title 仍然会被应用上这些 attributes * @note 该方法和 setTitleColor:forState: 均可设置字体颜色,如果二者冲突,则代码顺序较后的方法定义的颜色会最终生效 * @note 如果包含了 NSKernAttributeName ,则此方法会自动帮你去掉最后一个字的 kern 效果,否则容易导致文字整体在视觉上不居中 */ - (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 ================================================ FILE: QMUIKit/UIKitExtensions/UIButton+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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, ^{ 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)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; } - (void)qmui_calculateHeightAfterSetAppearance { [self setTitle:@"测" forState:UIControlStateNormal]; [self sizeToFit]; [self setTitle:nil forState:UIControlStateNormal]; } #pragma mark - TitleAttributes - (void)qmui_setTitleAttributes:(NSDictionary *)attributes forState:(UIControlState)state { if (!attributes && self.qbt_titleAttributes) { [self.qbt_titleAttributes removeObjectForKey:@(state)]; return; } [UIButton qbt_swizzleForTitleAttributesIfNeeded]; if (!self.qbt_titleAttributes) { self.qbt_titleAttributes = [[NSMutableDictionary alloc] init]; } // 从 Normal 同步样式到其他 state if (state != UIControlStateNormal && self.qbt_titleAttributes[@(UIControlStateNormal)]) { NSMutableDictionary *temp = attributes.mutableCopy; NSDictionary *normalAttributes = self.qbt_titleAttributes[@(UIControlStateNormal)]; for (NSAttributedStringKey key in normalAttributes.allKeys) { if (!temp[key]) { temp[key] = normalAttributes[key]; } } attributes = temp.copy; } self.qbt_titleAttributes[@(state)] = attributes; // 确保调用此方法设置 attributes 之前已经通过 setTitle:forState: 设置的文字也能应用上新的 attributes [self qbt_syncTitleByStates]; // 一个系统的不好的特性(bug?):如果你给 UIControlStateHighlighted(或者 normal 之外的任何 state)设置了包含 NSFont/NSKern/NSUnderlineAttributeName 之类的 attributedString ,但又仅用 setTitle:forState: 给 UIControlStateNormal 设置了普通的 string ,则按钮从 highlighted 切换回 normal 状态时,font 之类的属性依然会停留在 highlighted 时的状态 // 为了解决这个问题,我们要确保一旦有 normal 之外的 state 通过设置 qbt_titleAttributes 属性而导致使用了 attributedString,则 normal 也必须使用 attributedString if (self.qbt_titleAttributes.count && !self.qbt_titleAttributes[@(UIControlStateNormal)]) { [self qmui_setTitleAttributes:@{} forState:UIControlStateNormal]; } } // 如果 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]; } } + (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 *)qbt_attributedStringByRemovingLastKern:(NSAttributedString *)string { if (!string.length) { return string; } NSMutableAttributedString *attributedString = string.mutableCopy; [attributedString removeAttribute:NSKernAttributeName range:NSMakeRange(string.length - 1, 1)]; return attributedString.copy; } #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]; } + (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 ================================================ FILE: QMUIKit/UIKitExtensions/UICollectionView+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import #import @interface UICollectionView (QMUI) /** * 清除所有已选中的item的选中状态 */ - (void)qmui_clearsSelection; /** * 重新`reloadData`,同时保持`reloadData`前item的选中状态 */ - (void)qmui_reloadDataKeepingSelection; /** * 获取某个view在collectionView内对应的indexPath * * 例如某个view是某个cell里的subview,在这个view的点击事件回调方法里,就能通过`qmui_indexPathForItemAtView:`获取被点击的view所处的cell的indexPath * * @warning 注意返回的indexPath有可能为nil,要做保护。 */ - (NSIndexPath *)qmui_indexPathForItemAtView:(id)sender; /** * 判断当前 indexPath 的 item 是否为可视的 item */ - (BOOL)qmui_itemVisibleAtIndexPath:(NSIndexPath *)indexPath; /** * 对系统的 indexPathsForVisibleItems 进行了排序后的结果 */ - (NSArray *)qmui_indexPathsForVisibleItems; /** * 获取可视区域内第一个cell的indexPath。 * * 为什么需要这个方法是因为系统的indexPathsForVisibleItems方法返回的数组成员是无序排列的,所以不能直接通过firstObject拿到第一个cell。 * * @warning 若可视区域为CGRectZero,则返回nil */ - (NSIndexPath *)qmui_indexPathForFirstVisibleCell; @end ================================================ FILE: QMUIKit/UIKitExtensions/UICollectionView+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import "UICollectionView+QMUI.h" #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) { [self deselectItemAtIndexPath:indexPath animated:YES]; } } - (void)qmui_reloadDataKeepingSelection { NSArray *selectedIndexPaths = [self indexPathsForSelectedItems]; [self reloadData]; for (NSIndexPath *indexPath in selectedIndexPaths) { [self selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone]; } } /// 递归找到view在哪个cell里,不存在则返回nil - (UICollectionViewCell *)parentCellForView:(UIView *)view { if (!view.superview) { return nil; } if ([view.superview isKindOfClass:[UICollectionViewCell class]]) { return (UICollectionViewCell *)view.superview; } return [self parentCellForView:view.superview]; } - (NSIndexPath *)qmui_indexPathForItemAtView:(id)sender { if (sender && [sender isKindOfClass:[UIView class]]) { UIView *view = (UIView *)sender; UICollectionViewCell *parentCell = [self parentCellForView:view]; if (parentCell) { return [self indexPathForCell:parentCell]; } } return nil; } - (BOOL)qmui_itemVisibleAtIndexPath:(NSIndexPath *)indexPath { NSArray *visibleItemIndexPaths = self.indexPathsForVisibleItems; for (NSIndexPath *visibleIndexPath in visibleItemIndexPaths) { if ([indexPath isEqual:visibleIndexPath]) { return YES; } } 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 qmui_indexPathsForVisibleItems]; if (!visibleIndexPaths || visibleIndexPaths.count <= 0) { return nil; } return visibleIndexPaths.firstObject; } @end ================================================ FILE: QMUIKit/UIKitExtensions/UICollectionViewCell+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UICollectionViewCell+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIColor+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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 支持以 # 开头和不以 # 开头的 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) * #AARRGGBB 例如#00ff00ff,等同于RGBA(255, 0, 255, 0) * * @return UIColor对象 */ + (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" */ + (nullable UIColor *)qmui_colorWithRGBAString:(nullable NSString *)rgbaString; /** 将当前色值转换成“255,255,255,1.00”的字符串,如果 alpha 通道为1也会输出出来。其中 alpha 通道必定带两位小数点,其他三个通道都是整数。 */ @property(nonatomic, copy, readonly) NSString *qmui_RGBAString; /** * 获取当前 UIColor 对象里的红色色值 * * @return 红色通道的色值,值范围为0.0-1.0 */ @property(nonatomic, assign, readonly) CGFloat qmui_red; /** * 获取当前 UIColor 对象里的绿色色值 * * @return 绿色通道的色值,值范围为0.0-1.0 */ @property(nonatomic, assign, readonly) CGFloat qmui_green; /** * 获取当前 UIColor 对象里的蓝色色值 * * @return 蓝色通道的色值,值范围为0.0-1.0 */ @property(nonatomic, assign, readonly) CGFloat qmui_blue; /** * 获取当前 UIColor 对象里的透明色值 * * @return 透明通道的色值,值范围为0.0-1.0 */ @property(nonatomic, assign, readonly) CGFloat qmui_alpha; /** * 获取当前 UIColor 对象里的 hue(色相),注意 hue 的值是一个角度,所以0和1(0°和360°)是等价的,用 return 值去做判断时要特别注意。 */ @property(nonatomic, assign, readonly) CGFloat qmui_hue; /** * 获取当前 UIColor 对象里的 saturation(饱和度) */ @property(nonatomic, assign, readonly) CGFloat qmui_saturation; /** * 获取当前 UIColor 对象里的 brightness(亮度) */ @property(nonatomic, assign, readonly) CGFloat qmui_brightness; /** * 将当前UIColor对象剥离掉alpha通道后得到的色值。相当于把当前颜色的半透明值强制设为1.0后返回 * * @return alpha通道为1.0,其他rgb通道与原UIColor对象一致的新UIColor对象 */ - (nullable UIColor *)qmui_colorWithoutAlpha; /** * 计算当前color叠加了alpha之后放在指定颜色的背景上的色值 */ - (UIColor *)qmui_colorWithAlpha:(CGFloat)alpha backgroundColor:(nullable UIColor *)backgroundColor; /** * 计算当前color叠加了alpha之后放在白色背景上的色值 */ - (UIColor *)qmui_colorWithAlphaAddedToWhite:(CGFloat)alpha; /** * 将自身变化到某个目标颜色,可通过参数progress控制变化的程度,最终得到一个纯色 * @param toColor 目标颜色 * @param progress 变化程度,取值范围0.0f~1.0f */ - (UIColor *)qmui_transitionToColor:(nullable UIColor *)toColor progress:(CGFloat)progress; /** * 判断当前颜色是否为深色,可用于根据不同色调动态设置不同文字颜色的场景。 * * @link http://stackoverflow.com/questions/19456288/text-color-based-on-background-image @/link * * @return 若为深色则返回“YES”,浅色则返回“NO” */ @property(nonatomic, assign, readonly) BOOL qmui_colorIsDark; /** * @return 当前颜色的反色,不管传入的颜色属于什么 colorSpace,最终返回的反色都是 RGB * * @link http://stackoverflow.com/questions/5893261/how-to-get-inverse-color-from-uicolor @/link */ - (UIColor *)qmui_inverseColor; /** * 判断当前颜色是否等于系统默认的 tintColor 颜色。 * 背景:如果将一个 UIView.tintColor 设置为 nil,表示这个 view 的 tintColor 希望跟随 superview.tintColor 变化而变化,所以设置完再获取 view.tintColor,得到的并非 nil,而是 superview.tintColor 的值,而如果整棵 view 层级树里的 view 都没有设置自己的 tintColor,则会返回系统默认的 tintColor(也即 [UIColor qmui_systemTintColor]),所以才提供这个方法用于代替判断 tintColor == nil 的作用。 */ @property(nonatomic, assign, readonly) BOOL qmui_isSystemTintColor; /** * 获取当前系统的默认 tintColor 色值 */ @property(class, nonatomic, strong, readonly) UIColor *qmui_systemTintColor; /** 获取两个颜色之间的差异程度,0表示相同,值越大表示差距越大,例如纯白和纯黑会返回 86,如果遇到异常情况(例如传进来的 color 为 nil,则会返回 CGFLOAT_MAX)。 原理是将两个颜色摆放在 HSB(HSV) 模型内,取两个点之间的距离。由于 HSB(HSV) 没有 alpha 的概念,所以色值相同半透明程度不同的两个颜色会返回 0,也即相等。 */ - (CGFloat)qmui_distanceBetweenColor:(UIColor *)color; /** * 计算两个颜色叠加之后的最终色(注意区分前景色后景色的顺序)
* @link http://stackoverflow.com/questions/10781953/determine-rgba-colour-received-by-combining-two-colours @/link */ + (UIColor *)qmui_colorWithBackendColor:(UIColor *)backendColor frontColor:(UIColor *)frontColor; /** * 将颜色A变化到颜色B,可通过progress控制变化的程度 * @param fromColor 起始颜色 * @param toColor 目标颜色 * @param progress 变化程度,取值范围0.0f~1.0f */ + (UIColor *)qmui_colorFromColor:(UIColor *)fromColor toColor:(UIColor *)toColor progress:(CGFloat)progress; /** * 产生一个随机色,大部分情况下用于测试 */ + (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 ================================================ FILE: QMUIKit/UIKitExtensions/UIColor+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 使用 [UIColor colorWithRed:green:blue:alpha:] 或 [UIColor colorWithHue:saturation:brightness:alpha:] 方法创建的颜色是 UIDeviceRGBColor 类型的而不是 UIColor 类型的 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; }); }); } + (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]) { case 3: // #RGB alpha = 1.0f; red = [self colorComponentFrom: colorString start: 0 length: 1]; green = [self colorComponentFrom: colorString start: 1 length: 1]; blue = [self colorComponentFrom: colorString start: 2 length: 1]; break; case 4: // #ARGB alpha = [self colorComponentFrom: colorString start: 0 length: 1]; red = [self colorComponentFrom: colorString start: 1 length: 1]; green = [self colorComponentFrom: colorString start: 2 length: 1]; blue = [self colorComponentFrom: colorString start: 3 length: 1]; break; case 6: // #RRGGBB alpha = 1.0f; red = [self colorComponentFrom: colorString start: 0 length: 2]; green = [self colorComponentFrom: colorString start: 2 length: 2]; blue = [self colorComponentFrom: colorString start: 4 length: 2]; break; case 8: // #AARRGGBB alpha = [self colorComponentFrom: colorString start: 0 length: 2]; red = [self colorComponentFrom: colorString start: 2 length: 2]; green = [self colorComponentFrom: colorString start: 4 length: 2]; blue = [self colorComponentFrom: colorString start: 6 length: 2]; break; 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]; } - (NSString *)qmui_hexString { NSInteger alpha = self.qmui_alpha * 255; NSInteger red = self.qmui_red * 255; NSInteger green = self.qmui_green * 255; NSInteger blue = self.qmui_blue * 255; return [[NSString stringWithFormat:@"#%@%@%@%@", [self alignColorHexStringLength:[NSString qmui_hexStringWithInteger:alpha]], [self alignColorHexStringLength:[NSString qmui_hexStringWithInteger:red]], [self alignColorHexStringLength:[NSString qmui_hexStringWithInteger:green]], [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; } + (CGFloat)colorComponentFrom:(NSString *)string start:(NSUInteger)start length:(NSUInteger)length { NSString *substring = [string substringWithRange: NSMakeRange(start, length)]; NSString *fullHex = length == 2 ? substring : [NSString stringWithFormat: @"%@%@", substring, substring]; unsigned hexComponent; [[NSScanner scannerWithString: fullHex] scanHexInt: &hexComponent]; return hexComponent / 255.0; } - (CGFloat)qmui_red { CGFloat r; if ([self getRed:&r green:0 blue:0 alpha:0]) { return r; } return 0; } - (CGFloat)qmui_green { CGFloat g; if ([self getRed:0 green:&g blue:0 alpha:0]) { return g; } return 0; } - (CGFloat)qmui_blue { CGFloat b; if ([self getRed:0 green:0 blue:&b alpha:0]) { return b; } return 0; } - (CGFloat)qmui_alpha { CGFloat a; if ([self getRed:0 green:0 blue:0 alpha:&a]) { return a; } return 0; } - (CGFloat)qmui_hue { CGFloat h; if ([self getHue:&h saturation:0 brightness:0 alpha:0]) { return h; } return 0; } - (CGFloat)qmui_saturation { CGFloat s; if ([self getHue:0 saturation:&s brightness:0 alpha:0]) { return s; } return 0; } - (CGFloat)qmui_brightness { CGFloat b; if ([self getHue:0 saturation:0 brightness:&b alpha:0]) { return b; } return 0; } - (UIColor *)qmui_colorWithoutAlpha { CGFloat r; CGFloat g; CGFloat b; if ([self getRed:&r green:&g blue:&b alpha:0]) { return [UIColor colorWithRed:r green:g blue:b alpha:1]; } else { return nil; } } - (UIColor *)qmui_colorWithAlpha:(CGFloat)alpha backgroundColor:(UIColor *)backgroundColor { return [UIColor qmui_colorWithBackendColor:backgroundColor frontColor:[self colorWithAlphaComponent:alpha]]; } - (UIColor *)qmui_colorWithAlphaAddedToWhite:(CGFloat)alpha { return [self qmui_colorWithAlpha:alpha backgroundColor:UIColorWhite]; } - (UIColor *)qmui_transitionToColor:(UIColor *)toColor progress:(CGFloat)progress { return [UIColor qmui_colorFromColor:self toColor:toColor progress:progress]; } - (BOOL)qmui_colorIsDark { 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 { const CGFloat *componentColors = CGColorGetComponents(self.CGColor); UIColor *newColor = [[UIColor alloc] initWithRed:(1.0 - componentColors[0]) green:(1.0 - componentColors[1]) blue:(1.0 - componentColors[2]) alpha:componentColors[3]]; return newColor; } - (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) { UIView *view = [[UIView alloc] init]; systemTintColor = view.tintColor; } return systemTintColor; } + (UIColor *)qmui_colorWithBackendColor:(UIColor *)backendColor frontColor:(UIColor *)frontColor { CGFloat bgAlpha = [backendColor qmui_alpha]; CGFloat bgRed = [backendColor qmui_red]; CGFloat bgGreen = [backendColor qmui_green]; CGFloat bgBlue = [backendColor qmui_blue]; CGFloat frAlpha = [frontColor qmui_alpha]; CGFloat frRed = [frontColor qmui_red]; CGFloat frGreen = [frontColor qmui_green]; CGFloat frBlue = [frontColor qmui_blue]; CGFloat resultAlpha = frAlpha + bgAlpha * (1 - frAlpha); CGFloat resultRed = (frRed * frAlpha + bgRed * bgAlpha * (1 - frAlpha)) / resultAlpha; CGFloat resultGreen = (frGreen * frAlpha + bgGreen * bgAlpha * (1 - frAlpha)) / resultAlpha; CGFloat resultBlue = (frBlue * frAlpha + bgBlue * bgAlpha * (1 - frAlpha)) / resultAlpha; return [UIColor colorWithRed:resultRed green:resultGreen blue:resultBlue alpha:resultAlpha]; } + (UIColor *)qmui_colorFromColor:(UIColor *)fromColor toColor:(UIColor *)toColor progress:(CGFloat)progress { progress = MIN(progress, 1.0f); CGFloat fromRed = fromColor.qmui_red; CGFloat fromGreen = fromColor.qmui_green; CGFloat fromBlue = fromColor.qmui_blue; CGFloat fromAlpha = fromColor.qmui_alpha; CGFloat toRed = toColor.qmui_red; CGFloat toGreen = toColor.qmui_green; CGFloat toBlue = toColor.qmui_blue; CGFloat toAlpha = toColor.qmui_alpha; CGFloat finalRed = fromRed + (toRed - fromRed) * progress; CGFloat finalGreen = fromGreen + (toGreen - fromGreen) * progress; CGFloat finalBlue = fromBlue + (toBlue - fromBlue) * progress; CGFloat finalAlpha = fromAlpha + (toAlpha - fromAlpha) * progress; return [UIColor colorWithRed:finalRed green:finalGreen blue:finalBlue alpha:finalAlpha]; } + (UIColor *)qmui_randomColor { CGFloat red = ( arc4random() % 255 / 255.0 ); CGFloat green = ( arc4random() % 255 / 255.0 ); CGFloat blue = ( arc4random() % 255 / 255.0 ); return [UIColor colorWithRed:red green:green blue:blue alpha:1.0]; } @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 ================================================ FILE: QMUIKit/UIKitExtensions/UIControl+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import @interface UIControl (QMUI) /** * 是否优化 UIControl 被放在 UIScrollView 上时的点击体验。系统默认行为下,UIControl 在 UIScrollView 上会有300毫秒的延迟,当你快速点击某个 UIControl 时,将不会看到 setHighlighted 的效果。 * * 此时可以将 UIControl.qmui_automaticallyAdjustTouchHighlightedInScrollView 属性置为 YES,会使用自己的一套计算方式去判断触发 setHighlighted 的时机,从而保证既不影响 UIScrollView 的滚动,又能让 UIControl 在被快速点击时也能立马看到 setHighlighted 的效果。 * * @warning 使用了这个属性则不需要设置 UIScrollView.delaysContentTouches。因为如果将 UIScrollView.delaysContentTouches 置为 NO 来取消这个延迟,则系统无法判断 touch 时是要点击还是要滚动,你就会观察到当你想要滚动 UIScrollView 时,手指触摸到的那个 UIControl 会呈现出 highlighted 的效果,但通常这并不符合预期。 */ @property(nonatomic, assign) BOOL 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) 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIControl+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import "UIControl+QMUI.h" #import "QMUICore.h" @interface UIControl () @property(nonatomic,assign) BOOL qmuictl_canSetHighlighted; @property(nonatomic,assign) NSInteger qmuictl_touchEndCount; @end @implementation UIControl (QMUI) QMUISynthesizeBOOLProperty(qmuictl_canSetHighlighted, setQmuictl_canSetHighlighted) QMUISynthesizeNSIntegerProperty(qmuictl_touchEndCount, setQmuictl_touchEndCount) #pragma mark - Automatically Adjust Touch Highlighted In ScrollView static char kAssociatedObjectKey_automaticallyAdjustTouchHighlightedInScrollView; - (void)setQmui_automaticallyAdjustTouchHighlightedInScrollView:(BOOL)qmui_automaticallyAdjustTouchHighlightedInScrollView { objc_setAssociatedObject(self, &kAssociatedObjectKey_automaticallyAdjustTouchHighlightedInScrollView, @(qmui_automaticallyAdjustTouchHighlightedInScrollView), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (qmui_automaticallyAdjustTouchHighlightedInScrollView) { [QMUIHelper executeBlock:^{ OverrideImplementation([UIControl class], @selector(touchesBegan:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIControl *selfObject, NSSet *touches, UIEvent *event) { // call super void (^callSuperBlock)(void) = ^{ void (*originSelectorIMP)(id, SEL, NSSet *, UIEvent *); originSelectorIMP = (void (*)(id, SEL, NSSet *, UIEvent *))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, touches, event); }; selfObject.qmuictl_touchEndCount = 0; if (selfObject.qmui_automaticallyAdjustTouchHighlightedInScrollView) { selfObject.qmuictl_canSetHighlighted = YES; callSuperBlock(); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (selfObject.qmuictl_canSetHighlighted) { [selfObject setHighlighted:YES]; } }); } else { callSuperBlock(); } }; }); OverrideImplementation([UIControl class], @selector(touchesMoved:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIControl *selfObject, NSSet *touches, UIEvent *event) { if (selfObject.qmui_automaticallyAdjustTouchHighlightedInScrollView) { selfObject.qmuictl_canSetHighlighted = NO; } // call super 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 [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_automaticallyAdjustTouchHighlightedInScrollView)) boolValue]; } // 这段代码需要以一个独立的方法存在,因为一旦有坑,外面可以直接通过runtime调用这个方法 // 但,不要开放到.h文件里,理论上外面不应该用到它 - (void)sendActionsForAllTouchEventsIfCan { self.qmuictl_touchEndCount += 1; if (self.qmuictl_touchEndCount == 1) { [self sendActionsForControlEvents:UIControlEventAllTouchEvents]; } } #pragma mark - Prevents Repeated TouchUpInside Event static char kAssociatedObjectKey_preventsRepeatedTouchUpInsideEvent; - (void)setQmui_preventsRepeatedTouchUpInsideEvent:(BOOL)qmui_preventsRepeatedTouchUpInsideEvent { objc_setAssociatedObject(self, &kAssociatedObjectKey_preventsRepeatedTouchUpInsideEvent, @(qmui_preventsRepeatedTouchUpInsideEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (qmui_preventsRepeatedTouchUpInsideEvent) { [QMUIHelper executeBlock:^{ OverrideImplementation([UIControl class], @selector(sendAction:to:forEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIControl *selfObject, SEL action, id target, UIEvent *event) { if (selfObject.qmui_preventsRepeatedTouchUpInsideEvent) { NSArray *actions = [selfObject actionsForTarget:target forControlEvent:UIControlEventTouchUpInside]; if (!actions) { // iOS 10 UIBarButtonItem 里的 UINavigationButton 点击事件用的是 UIControlEventPrimaryActionTriggered actions = [selfObject actionsForTarget:target forControlEvent:UIControlEventPrimaryActionTriggered]; } if ([actions containsObject:NSStringFromSelector(action)]) { UITouch *touch = event.allTouches.anyObject; if (touch.tapCount > 1) { return; } } } // call super void (*originSelectorIMP)(id, SEL, SEL, id, UIEvent *); originSelectorIMP = (void (*)(id, SEL, SEL, id, UIEvent *))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, action, target, event); }; }); } oncePerIdentifier:@"UIControl preventsRepeatedTouchUpInsideEvent"]; } } - (BOOL)qmui_preventsRepeatedTouchUpInsideEvent { return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_preventsRepeatedTouchUpInsideEvent)) boolValue]; } #pragma mark - Highlighted Block static char kAssociatedObjectKey_setHighlightedBlock; - (void)setQmui_setHighlightedBlock:(void (^)(BOOL))qmui_setHighlightedBlock { objc_setAssociatedObject(self, &kAssociatedObjectKey_setHighlightedBlock, qmui_setHighlightedBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); if (qmui_setHighlightedBlock) { [QMUIHelper executeBlock:^{ OverrideImplementation([UIControl class], @selector(setHighlighted:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIControl *selfObject, BOOL highlighted) { // call super void (*originSelectorIMP)(id, SEL, BOOL); originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, highlighted); if (selfObject.qmui_setHighlightedBlock) { selfObject.qmui_setHighlightedBlock(highlighted); } }; }); } oncePerIdentifier:@"UIControl setHighlighted:"]; } } - (void (^)(BOOL))qmui_setHighlightedBlock { return (void (^)(BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_setHighlightedBlock); } #pragma mark - Selected Block static char kAssociatedObjectKey_setSelectedBlock; - (void)setQmui_setSelectedBlock:(void (^)(BOOL))qmui_setSelectedBlock { objc_setAssociatedObject(self, &kAssociatedObjectKey_setSelectedBlock, qmui_setSelectedBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); if (qmui_setSelectedBlock) { [QMUIHelper executeBlock:^{ OverrideImplementation([UIControl class], @selector(setSelected:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIControl *selfObject, BOOL selected) { // call super void (*originSelectorIMP)(id, SEL, BOOL); originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, selected); if (selfObject.qmui_setSelectedBlock) { selfObject.qmui_setSelectedBlock(selected); } }; }); } oncePerIdentifier:@"UIControl setSelected:"]; } } - (void (^)(BOOL))qmui_setSelectedBlock { return (void (^)(BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_setSelectedBlock); } #pragma mark - Enabled Block static char kAssociatedObjectKey_setEnabledBlock; - (void)setQmui_setEnabledBlock:(void (^)(BOOL))qmui_setEnabledBlock { objc_setAssociatedObject(self, &kAssociatedObjectKey_setEnabledBlock, qmui_setEnabledBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); if (qmui_setEnabledBlock) { [QMUIHelper executeBlock:^{ OverrideImplementation([UIControl class], @selector(setEnabled:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIControl *selfObject, BOOL enabled) { // call super void (*originSelectorIMP)(id, SEL, BOOL); originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, enabled); if (selfObject.qmui_setEnabledBlock) { selfObject.qmui_setEnabledBlock(enabled); } }; }); } oncePerIdentifier:@"UIControl setEnabled:"]; } } - (void (^)(BOOL))qmui_setEnabledBlock { return (void (^)(BOOL))objc_getAssociatedObject(self, &kAssociatedObjectKey_setEnabledBlock); } #pragma mark - Tap Block static char kAssociatedObjectKey_tapBlock; - (void)setQmui_tapBlock:(void (^)(__kindof UIControl *))qmui_tapBlock { if (qmui_tapBlock) { [QMUIHelper executeBlock:^{ OverrideImplementation([UIControl class], @selector(removeTarget:action:forControlEvents:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIControl *selfObject, id target, SEL action, UIControlEvents controlEvents) { // call super void (*originSelectorIMP)(id, SEL, id, SEL, UIControlEvents); originSelectorIMP = (void (*)(id, SEL, id, SEL, UIControlEvents))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, target, action, controlEvents); BOOL isTouchUpInsideEvent = controlEvents & UIControlEventTouchUpInside; BOOL shouldRemoveTouchUpInsideSelector = (action == @selector(qmui_handleTouchUpInside:)) || (target == selfObject && !action) || (!target && !action); if (isTouchUpInsideEvent && shouldRemoveTouchUpInsideSelector) { // 避免触发 setter 又反过来 removeTarget,然后就死循环了 objc_setAssociatedObject(selfObject, &kAssociatedObjectKey_tapBlock, nil, OBJC_ASSOCIATION_COPY_NONATOMIC); } }; }); } oncePerIdentifier:@"UIControl tapBlock"]; } SEL action = @selector(qmui_handleTouchUpInside:); if (!qmui_tapBlock) { [self removeTarget:self action:action forControlEvents:UIControlEventTouchUpInside]; } else { [self addTarget:self action:action forControlEvents:UIControlEventTouchUpInside]; } objc_setAssociatedObject(self, &kAssociatedObjectKey_tapBlock, qmui_tapBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (void (^)(__kindof UIControl *))qmui_tapBlock { return (void (^)(__kindof UIControl *))objc_getAssociatedObject(self, &kAssociatedObjectKey_tapBlock); } - (void)qmui_handleTouchUpInside:(__kindof UIControl *)sender { if (self.qmui_tapBlock) { self.qmui_tapBlock(self); } } @end ================================================ FILE: QMUIKit/UIKitExtensions/UIFont+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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, // 对应 UIFontWeightLight QMUIFontWeightNormal, // 对应 UIFontWeightRegular QMUIFontWeightMedium, // 对应 UIFontWeightMedium QMUIFontWeightBold // 对应 UIFontWeightSemibold }; @interface UIFont (QMUI) /** * 返回系统字体的细体 * * @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 字号大小 * @param weight 字体粗细 * @param italic 是否斜体 */ + (UIFont *)qmui_systemFontOfSize:(CGFloat)size weight:(QMUIFontWeight)weight italic:(BOOL)italic; /** * 根据需要生成一个支持响应动态字体大小调整的 UIFont 对象并返回 * @param size 字号大小 * @param weight 字重 * @param italic 是否斜体 * @return 支持响应动态字体大小调整的 UIFont 对象 */ + (UIFont *)qmui_dynamicSystemFontOfSize:(CGFloat)size weight:(QMUIFontWeight)weight italic:(BOOL)italic; /** * 返回支持动态字体的UIFont,支持定义最小和最大字号 * * @param pointSize 默认的size * @param upperLimitSize 最大的字号限制 * @param lowerLimitSize 最小的字号显示 * @param weight 字重 * @param italic 是否斜体 * * @return 支持响应动态字体大小调整的 UIFont 对象 */ + (UIFont *)qmui_dynamicSystemFontOfSize:(CGFloat)pointSize upperLimitSize:(CGFloat)upperLimitSize lowerLimitSize:(CGFloat)lowerLimitSize weight:(QMUIFontWeight)weight italic:(BOOL)italic; @end ================================================ FILE: QMUIKit/UIKitExtensions/UIFont+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import "UIFont+QMUI.h" #import "QMUICore.h" @implementation UIFont (QMUI) + (UIFont *)qmui_lightSystemFontOfSize:(CGFloat)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 { UIFont *font = nil; 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; } w; }); font = [UIFont systemFontOfSize:size weight:fontWeight]; if (!italic) { return font; } UIFontDescriptor *fontDescriptor = font.fontDescriptor; UIFontDescriptorSymbolicTraits trait = fontDescriptor.symbolicTraits; trait |= UIFontDescriptorTraitItalic; fontDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:trait]; font = [UIFont fontWithDescriptor:fontDescriptor size:0]; return font; } + (UIFont *)qmui_dynamicSystemFontOfSize:(CGFloat)size weight:(QMUIFontWeight)weight italic:(BOOL)italic { return [self qmui_dynamicSystemFontOfSize:size upperLimitSize:size + 5 lowerLimitSize:0 weight:weight italic:italic]; } + (UIFont *)qmui_dynamicSystemFontOfSize:(CGFloat)pointSize upperLimitSize:(CGFloat)upperLimitSize lowerLimitSize:(CGFloat)lowerLimitSize weight:(QMUIFontWeight)weight italic:(BOOL)italic { // 计算出 body 类型比默认的大小要变化了多少,然后在 pointSize 的基础上叠加这个变化 UIFont *font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; CGFloat offsetPointSize = font.pointSize - 17;// default UIFontTextStyleBody fontSize is 17 CGFloat finalPointSize = pointSize + offsetPointSize; finalPointSize = MAX(MIN(finalPointSize, upperLimitSize), lowerLimitSize); font = [UIFont qmui_systemFontOfSize:finalPointSize weight:weight italic:NO]; return font; } @end ================================================ FILE: QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIImage+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import #import #define CGContextInspectSize(size) [QMUIHelper inspectContextSize:size] #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, // 尖头向上的三角形 QMUIImageShapeDisclosureIndicator, // 列表 cell 右边的箭头 QMUIImageShapeCheckmark, // 列表 cell 右边的checkmark QMUIImageShapeDetailButtonImage, // 列表 cell 右边的 i 按钮图片 QMUIImageShapeNavBack, // 返回按钮的箭头 QMUIImageShapeNavClose // 导航栏的关闭icon }; typedef NS_OPTIONS(NSInteger, QMUIImageBorderPosition) { QMUIImageBorderPositionAll = 0, QMUIImageBorderPositionTop = 1 << 0, QMUIImageBorderPositionLeft = 1 << 1, QMUIImageBorderPositionBottom = 1 << 2, 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 * * @return 代表图片平均颜色的UIColor对象 */ - (UIColor *)qmui_averageColor; /** * 置灰当前图片 * * @return 已经置灰的图片 */ - (nullable UIImage *)qmui_grayImage; /** * 设置一张图片的透明度 * * @param alpha 要用于渲染透明度 * * @return 设置了透明度之后的图片 */ - (nullable UIImage *)qmui_imageWithAlpha:(CGFloat)alpha; /** * 保持当前图片的形状不变,使用指定的颜色去重新渲染它,生成一张新图片并返回 * * @param tintColor 要用于渲染的新颜色 * * @return 与当前图片形状一致但颜色与参数tintColor相同的新图片 */ - (nullable UIImage *)qmui_imageWithTintColor:(nullable UIColor *)tintColor; /** * 以 CIColorBlendMode 的模式为当前图片叠加一个颜色,生成一张新图片并返回,在叠加过程中会保留图片内的纹理。 * * @param blendColor 要叠加的颜色 * * @return 基于当前图片纹理保持不变的情况下颜色变为指定的叠加颜色的新图片 * * @warning 这个方法可能比较慢,会卡住主线程,建议异步使用 */ - (nullable UIImage *)qmui_imageWithBlendColor:(nullable UIColor *)blendColor; /** * 在当前图片的基础上叠加一张图片,并指定绘制叠加图片的起始位置 * * 叠加上去的图片将保持原图片的大小不变,不被压缩、拉伸 * * @param image 要叠加的图片 * @param point 所叠加图片的绘制的起始位置 * * @return 返回一张与原图大小一致的图片,所叠加的图片若超出原图大小,则超出部分被截掉 */ - (nullable UIImage *)qmui_imageWithImageAbove:(UIImage *)image atPoint:(CGPoint)point; /** * 在当前图片的上下左右增加一些空白(不支持负值),通常用于调节NSAttributedString里的图片与文字的间距 * @param extension 要拓展的大小 * @return 拓展后的图片 */ - (nullable UIImage *)qmui_imageWithSpacingExtensionInsets:(UIEdgeInsets)extension; /** * 切割出在指定位置中的图片 * * @param rect 要切割的rect * * @return 切割后的新图片 */ - (nullable UIImage *)qmui_imageWithClippedRect:(CGRect)rect; /** * 切割出在指定圆角的图片 * * @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_imageResizedInLimitedSize:resizingMode:scale: */ - (nullable UIImage *)qmui_imageResizedInLimitedSize:(CGSize)size; /** * 将原图按指定的 QMUIImageResizingMode 缩放,使其缩放后的大小不超过指定的大小,并返回缩放后的图片,缩放后的图片的倍数保持与原图一致。 * @param size 在这个约束的 size 内进行缩放后的大小,处理后返回的图片的 size 会根据 resizingMode 不同而不同,但必定不会超过 size。 * @param resizingMode 希望使用的缩放模式 * * @return 处理完的图片 * @see qmui_imageResizedInLimitedSize:resizingMode:scale: */ - (nullable UIImage *)qmui_imageResizedInLimitedSize:(CGSize)size resizingMode:(QMUIImageResizingMode)resizingMode; /** * 将原图按指定的 QMUIImageResizingMode 缩放,使其缩放后的大小不超过指定的大小,并返回缩放后的图片。 * @param size 在这个约束的 size 内进行缩放后的大小,处理后返回的图片的 size 会根据 resizingMode 不同而不同,但必定不会超过 size。 * @param resizingMode 希望使用的缩放模式 * @param scale 用于指定缩放后的图片的倍数 * * @return 处理完的图片 */ - (nullable UIImage *)qmui_imageResizedInLimitedSize:(CGSize)size resizingMode:(QMUIImageResizingMode)resizingMode scale:(CGFloat)scale; /** * 将原图进行旋转,只能选择上下左右四个方向 * * @param direction 旋转的方向 * * @return 处理完的图片 */ - (nullable UIImage *)qmui_imageWithOrientation:(UIImageOrientation)direction; /** * 为图片加上一个border,border的路径为path * * @param borderColor border的颜色 * @param path border的路径 * * @return 带border的UIImage * @warning 注意通过`path.lineWidth`设置边框大小,同时注意路径要考虑像素对齐(`path.lineWidth / 2.0`) */ - (nullable UIImage *)qmui_imageWithBorderColor:(nullable UIColor *)borderColor path:(nullable UIBezierPath *)path; /** * 为图片加上一个border,border的路径为borderColor、cornerRadius和borderWidth所创建的path * * @param borderColor border的颜色 * @param borderWidth border的宽度 * @param cornerRadius border的圆角 * * @param dashedLengths 一个CGFloat的数组,例如`CGFloat dashedLengths[] = {2, 4}`。如果不需要虚线,则传0即可 * * @return 带border的UIImage */ - (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; /** * 为图片加上一个border(可以是任意一条边,也可以是多条组合;只能创建矩形的border,不能添加圆角) * * @param borderColor border的颜色 * @param borderWidth border的宽度 * @param borderPosition border的位置 * * @return 带border的UIImage */ - (nullable UIImage *)qmui_imageWithBorderColor:(nullable UIColor *)borderColor borderWidth:(CGFloat)borderWidth borderPosition:(QMUIImageBorderPosition)borderPosition; /** * 返回一个被mask的图片 * * @param maskImage mask图片 * @param usingMaskImageMode 是否使用“mask image”的方式,若为 YES,则黑色部分显示,白色部分消失,透明部分显示,其他颜色会按照颜色的灰色度对图片做透明处理。若为 NO,则 maskImage 要求必须为灰度颜色空间的图片(黑白图),白色部分显示,黑色部分消失,透明部分消失,其他灰色度对图片做透明处理。 * * @return 被mask的图片 */ - (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 * * @param color 图片的颜色 * * @return 纯色的UIImage */ + (nullable UIImage *)qmui_imageWithColor:(nullable UIColor *)color; /** * 创建一个纯色的UIImage * * @param color 图片的颜色 * @param size 图片的大小 * @param cornerRadius 图片的圆角 * * @return 纯色的UIImage */ + (nullable UIImage *)qmui_imageWithColor:(nullable UIColor *)color size:(CGSize)size cornerRadius:(CGFloat)cornerRadius; /** * 创建一个纯色的UIImage,支持为四个角设置不同的圆角 * @param color 图片的颜色 * @param size 图片的大小 * @param cornerRadius 四个角的圆角值的数组,长度必须为4,顺序分别为[左上角、左下角、右下角、右上角] */ + (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 * * @param strokeColor border的颜色 * @param path border的路径 * @param addClip 是否要调path的addClip * * @return 带border的UIImage */ + (nullable UIImage *)qmui_imageWithStrokeColor:(nullable UIColor *)strokeColor size:(CGSize)size path:(nullable UIBezierPath *)path addClip:(BOOL)addClip; /** * 创建一个带边框路径,没有背景色的路径图片,border的路径为strokeColor、cornerRadius和lineWidth所创建的path * * @param strokeColor border的颜色 * @param lineWidth border的宽度 * @param cornerRadius border的圆角 * * @return 带border的UIImage */ + (nullable UIImage *)qmui_imageWithStrokeColor:(nullable UIColor *)strokeColor size:(CGSize)size lineWidth:(CGFloat)lineWidth cornerRadius:(CGFloat)cornerRadius; /** * 创建一个带边框路径,没有背景色的路径图片(可以是任意一条边,也可以是多条组合;只能创建矩形的border,不能添加圆角) * * @param strokeColor 路径的颜色 * @param size 图片的大小 * @param lineWidth 路径的大小 * @param borderPosition 图片的路径位置,上左下右 * * @return 带路径,没有背景色的UIImage */ + (nullable UIImage *)qmui_imageWithStrokeColor:(nullable UIColor *)strokeColor size:(CGSize)size lineWidth:(CGFloat)lineWidth borderPosition:(QMUIImageBorderPosition)borderPosition; /** * 创建一个指定大小和颜色的形状图片 * @param shape 图片形状 * @param size 图片大小 * @param tintColor 图片颜色 */ + (nullable UIImage *)qmui_imageWithShape:(QMUIImageShape)shape size:(CGSize)size tintColor:(nullable UIColor *)tintColor; /** * 创建一个指定大小和颜色的形状图片 * @param shape 图片形状 * @param size 图片大小 * @param lineWidth 路径大小,不会影响最终size * @param tintColor 图片颜色 */ + (nullable UIImage *)qmui_imageWithShape:(QMUIImageShape)shape size:(CGSize)size lineWidth:(CGFloat)lineWidth tintColor:(nullable UIColor *)tintColor; /** 对传进来的 `UIView` 截图,生成一个 `UIImage` 并返回。注意这里使用的是 view.layer 来渲染图片内容。 @param view 要截图的 `UIView` @return `UIView` 的截图 @warning UIView 的 transform 并不会在截图里生效 */ + (nullable UIImage *)qmui_imageWithView:(UIView *)view; /** 对传进来的 `UIView` 截图,生成一个 `UIImage` 并返回。注意这里使用的是 iOS 7的系统截图接口。 @param view 要截图的 `UIView` @param afterUpdates 是否要在界面更新完成后才截图 @return `UIView` 的截图 @warning UIView 的 transform 并不会在截图里生效 */ + (nullable UIImage *)qmui_imageWithView:(UIView *)view afterScreenUpdates:(BOOL)afterUpdates; @end NS_ASSUME_NONNULL_END ================================================ FILE: QMUIKit/UIKitExtensions/UIImage+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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 CGSizeFlatSpecificScale(CGSize size, float scale) { return CGSizeMake(flatSpecificScale(size.width, scale), flatSpecificScale(size.height, scale)); } @implementation UIImage (QMUI) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ 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; }; }); }); } + (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, nil); CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), self.CGImage); CGColorSpaceRelease(colorSpace); CGContextRelease(context); if(rgba[3] > 0) { return [UIColor colorWithRed:((CGFloat)rgba[0] / rgba[3]) green:((CGFloat)rgba[1] / rgba[3]) blue:((CGFloat)rgba[2] / rgba[3]) alpha:((CGFloat)rgba[3] / 255.0)]; } else { return [UIColor colorWithRed:((CGFloat)rgba[0]) / 255.0 green:((CGFloat)rgba[1]) / 255.0 blue:((CGFloat)rgba[2]) / 255.0 alpha:((CGFloat)rgba[3]) / 255.0]; } } - (UIImage *)qmui_grayImage { // CGBitmapContextCreate 是无倍数的,所以要自己换算成1倍 CGSize size = self.qmui_sizeInPixel; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); CGContextRef context = CGBitmapContextCreate(NULL, size.width, size.height, 8, 0, colorSpace, kCGBitmapByteOrderDefault); CGContextInspectContext(context, nil); CGColorSpaceRelease(colorSpace); if (context == NULL) { return nil; } CGRect imageRect = CGRectMakeWithSize(size); CGContextDrawImage(context, imageRect, self.CGImage); UIImage *grayImage = nil; CGImageRef imageRef = CGBitmapContextCreateImage(context); if (self.qmui_opaque) { grayImage = [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation]; } else { 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); grayImage = [UIImage imageWithCGImage:maskedGrayImageRef scale:self.scale orientation:self.imageOrientation]; CGImageRelease(mask); CGImageRelease(maskedGrayImageRef); CGContextRelease(alphaContext); // 用 CGBitmapContextCreateImage 方式创建出来的图片,CGImageAlphaInfo 总是为 CGImageAlphaInfoNone,导致 qmui_opaque 与原图不一致,所以这里再做多一步 grayImage = [UIImage qmui_imageWithSize:grayImage.size opaque:NO scale:grayImage.scale actions:^(CGContextRef contextRef) { [grayImage drawInRect:CGRectMakeWithSize(grayImage.size)]; }]; } CGContextRelease(context); CGImageRelease(imageRef); return grayImage; } - (UIImage *)qmui_imageWithAlpha:(CGFloat)alpha { 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 { // 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 { UIImage *coloredImage = [self qmui_imageWithTintColor:blendColor]; CIFilter *filter = [CIFilter filterWithName:@"CIColorBlendMode"]; [filter setValue:[CIImage imageWithCGImage:self.CGImage] forKey:kCIInputBackgroundImageKey]; [filter setValue:[CIImage imageWithCGImage:coloredImage.CGImage] forKey:kCIInputImageKey]; CIImage *outputImage = filter.outputImage; CIContext *context = [CIContext contextWithOptions:nil]; CGImageRef imageRef = [context createCGImage:outputImage fromRect:outputImage.extent]; UIImage *resultImage = [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation]; CGImageRelease(imageRef); return resultImage; } - (UIImage *)qmui_imageWithImageAbove:(UIImage *)image atPoint:(CGPoint)point { 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)); 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 { CGContextInspectSize(rect.size); CGRect imageRect = CGRectMakeWithSize(self.size); if (CGRectContainsRect(rect, imageRect)) { // 要裁剪的区域比自身大,所以不用裁剪直接返回自身即可 return self; } // 由于CGImage是以pixel为单位来计算的,而UIImage是以point为单位,所以这里需要将传进来的point转换为pixel CGRect scaledRect = CGRectApplyScale(rect, self.scale); CGImageRef imageRef = CGImageCreateWithImageInRect(self.CGImage, scaledRect); UIImage *imageOut = [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation]; CGImageRelease(imageRef); return imageOut; } - (UIImage *)qmui_imageWithClippedCornerRadius:(CGFloat)cornerRadius { return [self qmui_imageWithClippedCornerRadius:cornerRadius 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_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); CGSize imageSize = self.size; CGRect drawingRect = CGRectZero;// 图片绘制的 rect CGSize contextSize = CGSizeZero;// 画布的大小 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 (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 { // default is Center originY = CGFloatGetCenter(contextSize.height, resizedSize.height); } drawingRect.origin.y = originY; drawingRect.size = resizedSize; } else { // 默认按照 QMUIImageResizingModeScaleToFill drawingRect = CGRectMakeWithSize(size); contextSize = size; } return [UIImage qmui_imageWithSize:contextSize opaque:self.qmui_opaque scale:scale actions:^(CGContextRef contextRef) { [self drawInRect:drawingRect]; }]; } - (UIImage *)qmui_imageWithOrientation:(UIImageOrientation)orientation { if (orientation == UIImageOrientationUp) { return self; } CGSize contextSize = self.size; if (orientation == UIImageOrientationLeft || orientation == UIImageOrientationRight) { contextSize = CGSizeMake(contextSize.height, contextSize.width); } contextSize = CGSizeFlatSpecificScale(contextSize, self.scale); 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 { if (!borderColor) { return self; } 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 { return [self qmui_imageWithBorderColor:borderColor borderWidth:borderWidth cornerRadius:cornerRadius dashedLengths:0]; } - (UIImage *)qmui_imageWithBorderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth cornerRadius:(CGFloat)cornerRadius dashedLengths:(const CGFloat *)dashedLengths { if (!borderColor || !borderWidth) { return self; } UIBezierPath *path; CGRect rect = CGRectInset(CGRectMake(0, 0, self.size.width, self.size.height), borderWidth / 2, borderWidth / 2);// 调整rect,从而保证绘制描边时像素对齐 if (cornerRadius > 0) { path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius]; } else { path = [UIBezierPath bezierPathWithRect:rect]; } path.lineWidth = borderWidth; if (dashedLengths) { [path setLineDash:dashedLengths count:2 phase:0]; } return [self qmui_imageWithBorderColor:borderColor path:path]; } - (UIImage *)qmui_imageWithBorderColor:(UIColor *)borderColor borderWidth:(CGFloat)borderWidth borderPosition:(QMUIImageBorderPosition)borderPosition { if (borderPosition == QMUIImageBorderPositionAll) { return [self qmui_imageWithBorderColor:borderColor borderWidth:borderWidth cornerRadius:0]; } else { // TODO 使用bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:这个系统接口 UIBezierPath* path = [UIBezierPath bezierPath]; if ((QMUIImageBorderPositionBottom & borderPosition) == QMUIImageBorderPositionBottom) { [path moveToPoint:CGPointMake(0, self.size.height - borderWidth / 2)]; [path addLineToPoint:CGPointMake(self.size.width, self.size.height - borderWidth / 2)]; } if ((QMUIImageBorderPositionTop & borderPosition) == QMUIImageBorderPositionTop) { [path moveToPoint:CGPointMake(0, borderWidth / 2)]; [path addLineToPoint:CGPointMake(self.size.width, borderWidth / 2)]; } if ((QMUIImageBorderPositionLeft & borderPosition) == QMUIImageBorderPositionLeft) { [path moveToPoint:CGPointMake(borderWidth / 2, 0)]; [path addLineToPoint:CGPointMake(borderWidth / 2, self.size.height)]; } if ((QMUIImageBorderPositionRight & borderPosition) == QMUIImageBorderPositionRight) { [path moveToPoint:CGPointMake(self.size.width - borderWidth / 2, 0)]; [path addLineToPoint:CGPointMake(self.size.width - borderWidth / 2, self.size.height)]; } [path setLineWidth:borderWidth]; [path closePath]; return [self qmui_imageWithBorderColor:borderColor path:path]; } return self; } - (UIImage *)qmui_imageWithMaskImage:(UIImage *)maskImage usingMaskImageMode:(BOOL)usingMaskImageMode { CGImageRef maskRef = [maskImage CGImage]; CGImageRef mask; if (usingMaskImageMode) { // 用CGImageMaskCreate创建生成的 image mask。 // 黑色部分显示,白色部分消失,透明部分显示,其他颜色会按照颜色的灰色度对图片做透明处理。 mask = CGImageMaskCreate(CGImageGetWidth(maskRef), CGImageGetHeight(maskRef), CGImageGetBitsPerComponent(maskRef), CGImageGetBitsPerPixel(maskRef), CGImageGetBytesPerRow(maskRef), CGImageGetDataProvider(maskRef), nil, YES); } else { // 用一个纯CGImage作为mask。这个image必须是单色(例如:黑白色、灰色)、没有alpha通道、不能被其他图片mask。系统的文档:If `mask' is an image, then it must be in a monochrome color space (e.g. DeviceGray, GenericGray, etc...), may not have alpha, and may not itself be masked by an image mask or a masking color. // 白色部分显示,黑色部分消失,透明部分消失,其他灰色度对图片做透明处理。 mask = maskRef; } CGImageRef maskedImage = CGImageCreateWithMask(self.CGImage, mask); UIImage *returnImage = [UIImage imageWithCGImage:maskedImage scale:self.scale orientation:self.imageOrientation]; if (usingMaskImageMode) { CGImageRelease(mask); } CGImageRelease(maskedImage); 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); 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 { CGContextInspectSize(size); // 往里面缩一半的lineWidth,应为stroke绘制线的时候是往两边绘制的 // 如果cornerRadius为0的时候使用bezierPathWithRoundedRect:cornerRadius:会有问题,左上角老是会多出一点,所以区分开 UIBezierPath *path; CGRect rect = CGRectInset(CGRectMakeWithSize(size), lineWidth / 2, lineWidth / 2); if (cornerRadius > 0) { path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:cornerRadius]; } else { path = [UIBezierPath bezierPathWithRect:rect]; } [path setLineWidth:lineWidth]; return [UIImage qmui_imageWithStrokeColor:strokeColor size:size path:path addClip:NO]; } + (UIImage *)qmui_imageWithStrokeColor:(UIColor *)strokeColor size:(CGSize)size lineWidth:(CGFloat)lineWidth borderPosition:(QMUIImageBorderPosition)borderPosition { CGContextInspectSize(size); if (borderPosition == QMUIImageBorderPositionAll) { return [UIImage qmui_imageWithStrokeColor:strokeColor size:size lineWidth:lineWidth cornerRadius:0]; } else { // TODO 使用bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:这个系统接口 UIBezierPath* path = [UIBezierPath bezierPath]; if ((QMUIImageBorderPositionBottom & borderPosition) == QMUIImageBorderPositionBottom) { [path moveToPoint:CGPointMake(0, size.height - lineWidth / 2)]; [path addLineToPoint:CGPointMake(size.width, size.height - lineWidth / 2)]; } if ((QMUIImageBorderPositionTop & borderPosition) == QMUIImageBorderPositionTop) { [path moveToPoint:CGPointMake(0, lineWidth / 2)]; [path addLineToPoint:CGPointMake(size.width, lineWidth / 2)]; } if ((QMUIImageBorderPositionLeft & borderPosition) == QMUIImageBorderPositionLeft) { [path moveToPoint:CGPointMake(lineWidth / 2, 0)]; [path addLineToPoint:CGPointMake(lineWidth / 2, size.height)]; } if ((QMUIImageBorderPositionRight & borderPosition) == QMUIImageBorderPositionRight) { [path moveToPoint:CGPointMake(size.width - lineWidth / 2, 0)]; [path addLineToPoint:CGPointMake(size.width - lineWidth / 2, size.height)]; } [path setLineWidth:lineWidth]; [path closePath]; return [UIImage qmui_imageWithStrokeColor:strokeColor size:size path:path addClip:NO]; } } + (UIImage *)qmui_imageWithColor:(UIColor *)color { return [UIImage qmui_imageWithColor:color size:CGSizeMake(4, 4) cornerRadius:0]; } + (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadius:(CGFloat)cornerRadius { size = CGSizeFlatted(size); CGContextInspectSize(size); color = color ? color : UIColorClear; BOOL opaque = (cornerRadius == 0.0 && [color qmui_alpha] == 1.0); 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 切换时无法刷新。"); } return result; } + (UIImage *)qmui_imageWithColor:(UIColor *)color size:(CGSize)size cornerRadiusArray:(NSArray *)cornerRadius { size = CGSizeFlatted(size); CGContextInspectSize(size); 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); tintColor = tintColor ? : [UIColor colorWithRed:1 green:1 blue:1 alpha:1]; 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; } if (drawByStroke) { CGContextSetStrokeColorWithColor(contextRef, tintColor.CGColor); [path stroke]; } else { CGContextSetFillColorWithColor(contextRef, 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))]; } }]; } + (UIImage *)qmui_imageWithShape:(QMUIImageShape)shape size:(CGSize)size tintColor:(UIColor *)tintColor { CGFloat lineWidth = 0; switch (shape) { case QMUIImageShapeNavBack: lineWidth = 2.0f; break; case QMUIImageShapeDisclosureIndicator: lineWidth = 1.5f; break; case QMUIImageShapeCheckmark: lineWidth = 1.5f; break; case QMUIImageShapeDetailButtonImage: lineWidth = 1.0f; break; case QMUIImageShapeNavClose: lineWidth = 1.2f; // 取消icon默认的lineWidth break; default: break; } return [UIImage qmui_imageWithShape:shape size:size lineWidth:lineWidth tintColor:tintColor]; } + (UIImage *)qmui_imageWithView:(UIView *)view { CGContextInspectSize(view.bounds.size); 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。 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIImageView+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 16/8/9. // #import #import @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 * * 建议在设置完x/y之后调用 */ - (void)qmui_sizeToFitKeepingImageAspectRatioInSize:(CGSize)limitSize; @end ================================================ FILE: QMUIKit/UIKitExtensions/UIImageView+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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; } CGSize currentSize = self.frame.size; if (currentSize.width <= 0) { currentSize.width = self.image.size.width; } if (currentSize.height <= 0) { currentSize.height = self.image.size.height; } CGFloat horizontalRatio = limitSize.width / currentSize.width; CGFloat verticalRatio = limitSize.height / currentSize.height; CGFloat ratio = fmin(horizontalRatio, verticalRatio); CGRect frame = self.frame; frame.size.width = flat(currentSize.width * ratio); frame.size.height = flat(currentSize.height * ratio); self.frame = frame; } @end ================================================ FILE: QMUIKit/UIKitExtensions/UIInterface+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIInterface+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UILabel+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import NS_ASSUME_NONNULL_BEGIN extern const CGFloat QMUILineHeightIdentity; @interface UILabel (QMUI) - (instancetype)qmui_initWithFont:(nullable UIFont *)font textColor:(nullable UIColor *)textColor; /** * @brief 在需要特殊样式时,可通过此属性直接给整个 label 添加 NSAttributeName 系列样式,然后 setText 即可,无需使用繁琐的 attributedText * * @note 即使先调用 setText/attributedText ,然后再设置此属性,此属性仍然会生效 * @note 如果此属性包含了 NSKernAttributeName ,则最后一个字的 kern 效果会自动被移除,否则容易导致文字在视觉上不居中 * * @note 当你设置了此属性后,每次你调用 setText: 时,其实都会被自动转而调用 setAttributedText: * * 现在你有三种方法控制 label 的样式: * 1. 本身的样式属性(如 textColor, font 等) * 2. qmui_textAttributes * 3. 构造 NSAttributedString * 这三种方式可以同时使用,如果样式发生冲突(比如先通过方法1将文字设成红色,又通过方法2将文字设成蓝色),则绝大部分情况下代码执行顺序靠后的会最终生效 * 唯一例外的极端情况是:先用方法2将文字设成红色,再用方法1将文字设成蓝色,最后再 setText,这时虽然代码执行顺序靠后的是方法1,但最终生效的会是方法2,为了避免这种极端情况的困扰,建议不要同时使用方法1和方法2去设置同一种样式。 * */ @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; /** 获取当前 font.xHeight 的中心点在 label.bounds.size.height 里的y值(代表x这种矮的字符的中心点位置),从而令业务在试图将文本垂直居中时可以基于该属性的返回值去计算。 @warning 仅对单行文本有意义,对 label 的高度没有要求,但 label 不应该调整过 baselineOffset */ @property(nonatomic, assign, readonly) CGFloat qmui_centerOfXHeight; /** * 将目标UILabel的样式属性设置到当前UILabel上 * * 将会复制的样式属性包括:font、textColor、backgroundColor * @param label 要从哪个目标UILabel上复制样式 */ - (void)qmui_setTheSameAppearanceAsLabel:(UILabel *)label; /** * 在UILabel的样式(如字体)设置完后,将label的text设置为一个测试字符,再调用sizeToFit,从而令label的高度适应字体 * @warning 会setText:,因此确保在配置完样式后、设置text之前调用 */ - (void)qmui_calculateHeightAfterSetAppearance; /** * UILabel在显示中文字符时,会比显示纯英文字符额外多了一个sublayers,并且这个layer超出了label.bounds的范围,这会导致label必定需要做像素合成,所以通过一些方式来避免合成操作 * @see http://stackoverflow.com/questions/34895641/uilabel-is-marked-as-red-when-color-blended-layers-is-selected */ - (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 ================================================ FILE: QMUIKit/UIKitExtensions/UILabel+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import "UILabel+QMUI.h" #import "QMUICore.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, ^{ 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); } }); } - (instancetype)qmui_initWithFont:(UIFont *)font textColor:(UIColor *)textColor { BeginIgnoreClangWarning(-Wunused-value) [self init]; EndIgnoreClangWarning self.font = font; self.textColor = textColor; return self; } - (void)qmuilb_setText:(NSString *)text { if (!text) { [self qmuilb_setText:text]; return; } if (!self.qmui_textAttributes.count && ![self _hasSetQmuiLineHeight]) { [self qmuilb_setText:text]; return; } NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:text attributes:self.qmui_textAttributes]; [self qmuilb_setAttributedText:[self attributedStringWithKernAndLineHeightAdjusted:attributedString]]; } // 在 qmui_textAttributes 样式基础上添加用户传入的 attributedString 中包含的新样式。换句话说,如果这个方法里有样式冲突,则以 attributedText 为准 - (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]; attributedString = [[self attributedStringWithKernAndLineHeightAdjusted:attributedString] mutableCopy]; [text enumerateAttributesInRange:NSMakeRange(0, text.length) options:0 usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { [attributedString addAttributes:attrs range:range]; }]; [self qmuilb_setAttributedText:attributedString]; } static char kAssociatedObjectKey_textAttributes; // 在现有样式基础上增加 qmui_textAttributes 样式。换句话说,如果这个方法里有样式冲突,则以 qmui_textAttributes 为准 - (void)setQmui_textAttributes:(NSDictionary *)qmui_textAttributes { NSDictionary *prevTextAttributes = self.qmui_textAttributes; if ([prevTextAttributes isEqualToDictionary:qmui_textAttributes]) { return; } objc_setAssociatedObject(self, &kAssociatedObjectKey_textAttributes, qmui_textAttributes, OBJC_ASSOCIATION_COPY_NONATOMIC); if (!self.text.length) { return; } NSMutableAttributedString *string = [self.attributedText mutableCopy]; NSRange fullRange = NSMakeRange(0, string.length); // 1)当前 attributedText 包含的样式可能来源于两方面:通过 qmui_textAttributes 设置的、通过直接传入 attributedString 设置的,这里要过滤删除掉前者的样式效果,保留后者的样式效果 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) { // 如果存在 kern 属性,则只有 range 是第一个字至倒数第二个字,才有可能是通过 qmui_textAttribtus 设置的 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:^(NSAttributedStringKey _Nonnull attr, id _Nonnull value, BOOL * _Nonnull stop) { if (prevTextAttributes[attr] == value) { [willRemovedAttributes addObject:attr]; } }]; }]; [willRemovedAttributes enumerateObjectsUsingBlock:^(id _Nonnull attr, NSUInteger idx, BOOL * _Nonnull stop) { [string removeAttribute:attr range:fullRange]; }]; } // 2)添加新样式 if (qmui_textAttributes) { [string addAttributes:qmui_textAttributes range:fullRange]; } // 不能调用 setAttributedText: ,否则若遇到样式冲突,那个方法会让用户传进来的 NSAttributedString 样式覆盖 qmui_textAttributes 的样式 [self qmuilb_setAttributedText:[self attributedStringWithKernAndLineHeightAdjusted:string]]; } - (NSDictionary *)qmui_textAttributes { return (NSDictionary *)objc_getAssociatedObject(self, &kAssociatedObjectKey_textAttributes); } // 去除最后一个字的 kern 效果,并且在有必要的情况下应用 qmui_setLineHeight: 设置的行高 - (NSAttributedString *)attributedStringWithKernAndLineHeightAdjusted:(NSAttributedString *)string { if (!string.length) { return string; } NSMutableAttributedString *attributedString = nil; if ([string isKindOfClass:[NSMutableAttributedString class]]) { attributedString = (NSMutableAttributedString *)string; } else { attributedString = [string mutableCopy]; } // 去除最后一个字的 kern 效果,使得文字整体在视觉上居中 // 只有当 qmui_textAttributes 中设置了 kern 时这里才应该做调整 if (self.qmui_textAttributes[NSKernAttributeName]) { [attributedString removeAttribute:NSKernAttributeName range:NSMakeRange(string.length - 1, 1)]; } // 判断是否应该应用上通过 qmui_setLineHeight: 设置的行高 __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))) { if (style && (style.maximumLineHeight || style.minimumLineHeight)) { shouldAdjustLineHeight = NO; *stop = YES; } } }]; 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 { if (qmui_lineHeight == QMUILineHeightIdentity) { objc_setAssociatedObject(self, &kAssociatedObjectKey_lineHeight, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } else { objc_setAssociatedObject(self, &kAssociatedObjectKey_lineHeight, @(qmui_lineHeight), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } // 注意:对于 UILabel,只要你设置过 text,则 attributedText 就是有值的,因此这里无需区分 setText 还是 setAttributedText // 注意:这里需要刷新一下 qmui_textAttributes 对 text 的样式,否则刚进行设置的 lineHeight 就会无法设置。 NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:self.attributedText.string attributes:self.qmui_textAttributes]; attributedString = [[self attributedStringWithKernAndLineHeightAdjusted:attributedString] mutableCopy]; [self setAttributedText:attributedString]; } - (CGFloat)qmui_lineHeight { 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; } - (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; } 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 { self.font = label.font; self.textColor = label.textColor; self.backgroundColor = label.backgroundColor; self.lineBreakMode = label.lineBreakMode; self.textAlignment = label.textAlignment; 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]; } } - (void)qmui_calculateHeightAfterSetAppearance { self.text = @"测"; [self sizeToFit]; self.text = nil; } - (void)qmui_avoidBlendedLayersIfShowingChineseWithBackgroundColor:(UIColor *)color { self.opaque = YES;// 本来默认就是YES,这里还是明确写一下 self.backgroundColor = color; 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIMenuController+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIMenuController+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UINavigationBar+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UINavigationController+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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 @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 /** * 拦截系统默认返回按钮事件,有时候需要在点击系统返回按钮,或者手势返回的时候想要拦截事件,比如要判断当前界面编辑的的内容是否要保存,或者返回的时候需要做一些额外的逻辑处理等等。 * */ @protocol UINavigationControllerBackButtonHandlerProtocol @optional /** * 点击系统返回按钮或者手势返回的时候是否要相应界面返回(手动调用代码pop排除)。支持参数判断是点击系统返回按钮还是通过手势触发 * 一般使用的场景是:可以在这个返回里面做一些业务的判断,比如点击返回按钮的时候,如果输入框里面的文本没有满足条件的则可以弹 Alert 并且返回 NO 来阻止用户退出界面导致不合法的数据或者数据丢失。 */ - (BOOL)shouldPopViewControllerByBackButtonOrPopGesture:(BOOL)byPopGesture; /// 当自定义了`leftBarButtonItem`按钮之后,系统的手势返回就失效了。可以通过`forceEnableInteractivePopGestureRecognizer`来决定要不要把那个手势返回强制加回来。当 interactivePopGestureRecognizer.enabled = NO 或者当前`UINavigationController`堆栈的viewControllers小于2的时候此方法无效。 - (BOOL)forceEnableInteractivePopGestureRecognizer; @end /** * @see UINavigationControllerBackButtonHandlerProtocol */ @interface UIViewController (BackBarButtonSupport) @end NS_ASSUME_NONNULL_END ================================================ FILE: QMUIKit/UIKitExtensions/UINavigationController+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 16/1/12. // #import "UINavigationController+QMUI.h" #import "QMUICore.h" #import "QMUILog.h" #import "UIViewController+QMUI.h" @interface _QMUINavigationInteractiveGestureDelegator : NSObject @property(nonatomic, weak, readonly) UINavigationController *parentViewController; - (instancetype)initWithParentViewController:(UINavigationController *)parentViewController; @end @interface UINavigationController () @property(nonatomic, strong) NSMutableArray *qmuinc_navigationActionDidChangeBlocks; @property(nullable, nonatomic, readwrite) UIViewController *qmui_endedTransitionTopViewController; @property(nullable, nonatomic, weak, readonly) id qmui_interactivePopGestureRecognizerDelegate; @property(nullable, nonatomic, strong) _QMUINavigationInteractiveGestureDelegator *qmui_interactiveGestureDelegator; @end @implementation UINavigationController (QMUI) QMUISynthesizeBOOLProperty(qmui_alwaysInvokeAppearanceMethods, setQmui_alwaysInvokeAppearanceMethods) QMUISynthesizeIdStrongProperty(qmuinc_navigationActionDidChangeBlocks, setQmuinc_navigationActionDidChangeBlocks) QMUISynthesizeIdWeakProperty(qmui_endedTransitionTopViewController, setQmui_endedTransitionTopViewController) QMUISynthesizeIdWeakProperty(qmui_interactivePopGestureRecognizerDelegate, setQmui_interactivePopGestureRecognizerDelegate) QMUISynthesizeIdStrongProperty(qmui_interactiveGestureDelegator, setQmui_interactiveGestureDelegator) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ OverrideImplementation([UINavigationController class], @selector(initWithNibName:bundle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UINavigationController *(UINavigationController *selfObject, NSString *firstArgv, NSBundle *secondArgv) { // call super UINavigationController *(*originSelectorIMP)(id, SEL, NSString *, NSBundle *); originSelectorIMP = (UINavigationController *(*)(id, SEL, NSString *, NSBundle *))originalIMPProvider(); UINavigationController *result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); [selfObject qmui_didInitialize]; return result; }; }); OverrideImplementation([UINavigationController class], @selector(initWithCoder:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UINavigationController *(UINavigationController *selfObject, NSCoder *firstArgv) { // call super UINavigationController *(*originSelectorIMP)(id, SEL, NSCoder *); originSelectorIMP = (UINavigationController *(*)(id, SEL, NSCoder *))originalIMPProvider(); UINavigationController *result = originSelectorIMP(selfObject, originCMD, firstArgv); [selfObject qmui_didInitialize]; return result; }; }); // iOS 12 及以前,initWithNavigationBarClass:toolbarClass:、initWithRootViewController: 会调用 initWithNibName:bundle:,所以这两个方法在 iOS 12 下不需要再次调用 qmui_didInitialize 了。 OverrideImplementation([UINavigationController class], @selector(initWithNavigationBarClass:toolbarClass:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UINavigationController *(UINavigationController *selfObject, Class firstArgv, Class secondArgv) { // call super UINavigationController *(*originSelectorIMP)(id, SEL, Class, Class); originSelectorIMP = (UINavigationController *(*)(id, SEL, Class, Class))originalIMPProvider(); UINavigationController *result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); [selfObject qmui_didInitialize]; return result; }; }); OverrideImplementation([UINavigationController class], @selector(initWithRootViewController:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UINavigationController *(UINavigationController *selfObject, UIViewController *firstArgv) { // call super UINavigationController *(*originSelectorIMP)(id, SEL, UIViewController *); originSelectorIMP = (UINavigationController *(*)(id, SEL, UIViewController *))originalIMPProvider(); UINavigationController *result = originSelectorIMP(selfObject, originCMD, firstArgv); [selfObject qmui_didInitialize]; return result; }; }); ExtendImplementationOfVoidMethodWithoutArguments([UINavigationController class], @selector(viewDidLoad), ^(UINavigationController *selfObject) { 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]; [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)qmui_didInitialize { } static char kAssociatedObjectKey_navigationAction; - (void)setQmui_navigationAction:(QMUINavigationAction)qmui_navigationAction animated:(BOOL)animated appearingViewController:(UIViewController *)appearingViewController disappearingViewControllers:(NSArray *)disappearingViewControllers { BOOL valueChanged = self.qmui_navigationAction != qmui_navigationAction; objc_setAssociatedObject(self, &kAssociatedObjectKey_navigationAction, @(qmui_navigationAction), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (valueChanged && self.qmuinc_navigationActionDidChangeBlocks.count) { [self.qmuinc_navigationActionDidChangeBlocks enumerateObjectsUsingBlock:^(QMUINavigationActionDidChangeBlock _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { obj(qmui_navigationAction, animated, self, appearingViewController, disappearingViewControllers); }]; } } - (QMUINavigationAction)qmui_navigationAction { return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_navigationAction)) unsignedIntegerValue]; } - (void)qmui_addNavigationActionDidChangeBlock:(QMUINavigationActionDidChangeBlock)block { if (!self.qmuinc_navigationActionDidChangeBlocks) { self.qmuinc_navigationActionDidChangeBlocks = NSMutableArray.new; } [self.qmuinc_navigationActionDidChangeBlocks addObject:block]; } - (BOOL)qmui_isPushing { BOOL isPushing = self.qmui_navigationAction > QMUINavigationActionWillPush && self.qmui_navigationAction <= QMUINavigationActionPushCompleted; return isPushing; } - (BOOL)qmui_isPopping { BOOL isPopping = self.qmui_navigationAction > QMUINavigationActionWillPop && self.qmui_navigationAction <= QMUINavigationActionPopCompleted; return isPopping; } - (UINavigationControllerOperation)qmui_lastOperation { // -[UINavigationController lastOperation] SEL operationSEL = NSSelectorFromString([NSString qmui_stringByConcat:@"last", @"Operation", nil]); if ([self respondsToSelector:operationSEL]) { UINavigationControllerOperation operation = UINavigationControllerOperationNone; [self qmui_performSelector:operationSEL withPrimitiveReturnValue:&operation]; return operation; } return UINavigationControllerOperationNone; } - (UIViewController *)qmui_topViewController { if (self.qmui_isPushing) { return self.topViewController; } return self.qmui_endedTransitionTopViewController ? self.qmui_endedTransitionTopViewController : self.topViewController; } - (nullable UIViewController *)qmui_rootViewController { 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; } - (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 byPopGesture:(BOOL)byPopGesture { BOOL canPopViewController = YES; if ([viewController respondsToSelector:@selector(shouldPopViewControllerByBackButtonOrPopGesture:)] && [viewController shouldPopViewControllerByBackButtonOrPopGesture:byPopGesture] == NO) { canPopViewController = NO; } return canPopViewController; } - (BOOL)shouldForceEnableInteractivePopGestureRecognizer { UIViewController *viewController = [self topViewController]; return self.viewControllers.count > 1 && self.interactivePopGestureRecognizer.enabled && [viewController respondsToSelector:@selector(forceEnableInteractivePopGestureRecognizer)] && [viewController forceEnableInteractivePopGestureRecognizer]; } @end @implementation _QMUINavigationInteractiveGestureDelegator - (instancetype)initWithParentViewController:(UINavigationController *)parentViewController { if (self = [super init]) { _parentViewController = parentViewController; } return self; } #pragma mark - // iOS 13.4 开始会优先询问该方法,只有返回 YES 后才会继续后续的逻辑 - (BOOL)_gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveEvent:(UIEvent *)event { if (gestureRecognizer == self.parentViewController.interactivePopGestureRecognizer) { NSObject *originGestureDelegate = self.parentViewController.qmui_interactivePopGestureRecognizerDelegate; if ([originGestureDelegate respondsToSelector:_cmd]) { BOOL originalValue = YES; [originGestureDelegate qmui_performSelector:_cmd withPrimitiveReturnValue:&originalValue arguments:&gestureRecognizer, &event, nil]; if (!originalValue // 在开启 forceEnableInteractivePopGestureRecognizer 的界面被 push 的过程中快速手势返回,容易导致 App 卡死 // https://github.com/Tencent/QMUI_iOS/issues/1498 && self.parentViewController.qmui_navigationAction == QMUINavigationActionUnknow && [self.parentViewController shouldForceEnableInteractivePopGestureRecognizer]) { return YES; } return originalValue; } } return YES; } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if (gestureRecognizer == self.parentViewController.interactivePopGestureRecognizer) { BOOL canPopViewController = [self.parentViewController canPopViewController:self.parentViewController.topViewController byPopGesture:YES]; if (canPopViewController) { if ([self.parentViewController.qmui_interactivePopGestureRecognizerDelegate respondsToSelector:_cmd]) { BOOL result = [self.parentViewController.qmui_interactivePopGestureRecognizerDelegate gestureRecognizerShouldBegin:gestureRecognizer]; return result; } else { return NO; } } else { return NO; } } return YES; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { 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 originalValue; } } return YES; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)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; } // 是否要gestureRecognizer检测失败了,才去检测otherGestureRecognizer - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { if (gestureRecognizer == self.parentViewController.interactivePopGestureRecognizer) { // 如果只是实现了上面几个手势的delegate,那么返回的手势和当前界面上的scrollview或者其他存在的手势会冲突,所以如果判断是返回手势,则优先响应返回手势再响应其他手势。 // 不知道为什么,系统竟然没有实现这个delegate,那么它是怎么处理返回手势和其他手势的优先级的 return YES; } return NO; } @end @implementation UIViewController (BackBarButtonSupport) @end ================================================ FILE: QMUIKit/UIKitExtensions/UINavigationItem+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UINavigationItem+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIScrollView+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import typedef NS_ENUM(NSInteger, QMUIScrollPosition) { QMUIScrollPositionNone, // 滚动到临近的区域(可能是 Top 也可能是 Bottom) QMUIScrollPositionTop, // 滚动到可视区域最顶部 QMUIScrollPositionMiddle, // 滚动到可视区域中间 QMUIScrollPositionBottom, // 滚动到可视区域底部 }; @interface UIScrollView (QMUI) /// 判断UIScrollView是否已经处于顶部(当UIScrollView内容不够多不可滚动时,也认为是在顶部) @property(nonatomic, assign, readonly) BOOL qmui_alreadyAtTop; /// 判断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混淆 */ - (BOOL)qmui_canScroll; /** * 不管当前scrollView是否可滚动,直接将其滚动到最顶部 * @param force 是否无视[self qmui_canScroll]而强制滚动 * @param animated 是否用动画表现 */ - (void)qmui_scrollToTopForce:(BOOL)force animated:(BOOL)animated; /** * 等同于[self qmui_scrollToTopForce:NO animated:animated] */ - (void)qmui_scrollToTopAnimated:(BOOL)animated; /// 等同于[self qmui_scrollToTopAnimated:NO] - (void)qmui_scrollToTop; /** 滚到列表顶部,但如果 contentInset.top 与上一次相同则不会执行滚动操作,通常用于 UIScrollViewContentInsetAdjustmentNever 的 scrollView 设置完业务的 contentInset 后将列表滚到顶部。 */ - (void)qmui_scrollToTopUponContentInsetTopChange; /** * 如果当前的scrollView可滚动,则将其滚动到最底部 * @param animated 是否用动画表现 * @see [UIScrollView qmui_canScroll] */ - (void)qmui_scrollToBottomAnimated:(BOOL)animated; /// 等同于[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 ================================================ FILE: QMUIKit/UIKitExtensions/UIScrollView+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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, ^{ OverrideImplementation([UIScrollView class], @selector(description), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^NSString *(UIScrollView *selfObject) { // call super NSString *(*originSelectorIMP)(id, SEL); originSelectorIMP = (NSString *(*)(id, SEL))originalIMPProvider(); NSString *result = originSelectorIMP(selfObject, originCMD); if (NSThread.isMainThread) { result = ([NSString stringWithFormat:@"%@, contentInset = %@", result, NSStringFromUIEdgeInsets(selfObject.contentInset)]).mutableCopy; } return result; }; }); if (QMUICMIActivated && AdjustScrollIndicatorInsetsByContentInsetAdjustment) { OverrideImplementation([UIScrollView class], @selector(setContentInsetAdjustmentBehavior:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIScrollView *selfObject, UIScrollViewContentInsetAdjustmentBehavior firstArgv) { // call super void (*originSelectorIMP)(id, SEL, UIScrollViewContentInsetAdjustmentBehavior); originSelectorIMP = (void (*)(id, SEL, UIScrollViewContentInsetAdjustmentBehavior))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, firstArgv); if (firstArgv == UIScrollViewContentInsetAdjustmentNever) { selfObject.automaticallyAdjustsScrollIndicatorInsets = NO; } else { selfObject.automaticallyAdjustsScrollIndicatorInsets = YES; } }; }); } }); } - (BOOL)qmui_alreadyAtTop { if (CGFloatEqualToFloat(self.contentOffset.y, -self.adjustedContentInset.top)) { return YES; } return NO; } - (BOOL)qmui_alreadyAtBottom { if (!self.qmui_canScroll) { return YES; } 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.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.adjustedContentInset.left, -self.adjustedContentInset.top) animated:animated]; } } - (void)qmui_scrollToTopAnimated:(BOOL)animated { [self qmui_scrollToTopForce:NO animated:animated]; } - (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.adjustedContentInset.bottom - CGRectGetHeight(self.bounds)) animated:animated]; } } - (void)qmui_scrollToBottom { [self qmui_scrollToBottomAnimated:NO]; } - (void)qmui_stopDeceleratingIfNeeded { if (self.decelerating) { [self setContentOffset:self.contentOffset animated:NO]; } } - (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 ================================================ FILE: QMUIKit/UIKitExtensions/UISearchBar+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 16/5/26. // #import #import NS_ASSUME_NONNULL_BEGIN /** 提供更丰富的接口来修改 UISearchBar 的样式,注意大部分接口都同时支持配置表和 UIAppearance,如果有使用配置表并且该项的值不为 nil,则以配置表的值为准。 */ @interface UISearchBar (QMUI) /** 获取与 searchBar 关联的 UISearchController */ @property(nonatomic, strong, readonly) UISearchController *qmui_searchController; /** 当以 tableHeaderView 的方式使用 UISearchBar 时,建议将这个属性置为 YES,从而可以帮你处理 https://github.com/Tencent/QMUI_iOS/issues/233 里列出的问题(抖动、iPhone X 适配等),默认为 NO */ @property(nonatomic, assign) BOOL qmui_usedAsTableHeaderView; /// 是否让搜索框的 search icon、placeholder 在非搜索状态下居中(iOS 11 及以上,系统默认是居左的,iOS 10 及以下版本,系统默认就是居中),默认为 NO,也即维持系统默认表现不变。 @property(nonatomic, assign) BOOL qmui_centerPlaceholder UI_APPEARANCE_SELECTOR; /// 输入框内 placeholder 的颜色 @property(nullable, nonatomic, strong) UIColor *qmui_placeholderColor UI_APPEARANCE_SELECTOR; /// 输入框的文字颜色 @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; /// 取消按钮相对于系统原有布局位置的上下左右的偏移。 @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 ================================================ FILE: QMUIKit/UIKitExtensions/UISearchBar+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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, ^{ 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)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); if (self.placeholder) { // 触发 setPlaceholder 里更新 placeholder 样式的逻辑 self.placeholder = self.placeholder; } } - (UIColor *)qmui_placeholderColor { return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_PlaceholderColor); } static char kAssociatedObjectKey_TextColor; - (void)setQmui_textColor:(UIColor *)qmui_textColor { objc_setAssociatedObject(self, &kAssociatedObjectKey_TextColor, qmui_textColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.searchTextField.textColor = qmui_textColor; } - (UIColor *)qmui_textColor { return (UIColor *)objc_getAssociatedObject(self, &kAssociatedObjectKey_TextColor); } static char kAssociatedObjectKey_font; - (void)setQmui_font:(UIFont *)qmui_font { objc_setAssociatedObject(self, &kAssociatedObjectKey_font, qmui_font, OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (self.placeholder) { // 触发 setPlaceholder 里更新 placeholder 样式的逻辑 self.placeholder = self.placeholder; } // 更新输入框的文字样式 self.searchTextField.font = qmui_font; } - (UIFont *)qmui_font { return (UIFont *)objc_getAssociatedObject(self, &kAssociatedObjectKey_font); } - (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 { if (!QMUICMIActivated) { return; } // 搜索框的字号及 placeholder 的字号 self.qmui_font = SearchBarFont; // 搜索框的文字颜色 self.qmui_textColor = SearchBarTextColor; // placeholder 的文字颜色 self.qmui_placeholderColor = SearchBarPlaceholderColor; self.placeholder = @"搜索"; self.autocorrectionType = UITextAutocorrectionTypeNo; self.autocapitalizationType = UITextAutocapitalizationTypeNone; // 设置搜索icon UIImage *searchIconImage = SearchBarSearchIconImage; if (searchIconImage) { 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; // 输入框背景图 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的背景 // 为了让 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 ================================================ FILE: QMUIKit/UIKitExtensions/UISearchController+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UISearchController+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UISlider+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UISlider+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UISwitch+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UISwitch+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UITabBar+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 2017/2/14. // #import #import "QMUIBarProtocol.h" NS_ASSUME_NONNULL_BEGIN #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 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 ================================================ FILE: QMUIKit/UIKitExtensions/UITabBar+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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 () @property(nonatomic, assign) BOOL canItemRespondDoubleTouch; @property(nonatomic, assign) NSInteger lastTouchedTabBarItemViewIndex; @property(nonatomic, assign) NSInteger tabBarItemViewTouchCount; @end @implementation UITabBar (QMUI) QMUISynthesizeBOOLProperty(canItemRespondDoubleTouch, setCanItemRespondDoubleTouch) QMUISynthesizeNSIntegerProperty(lastTouchedTabBarItemViewIndex, setLastTouchedTabBarItemViewIndex) QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouchCount) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // -[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)qmuitb_handleTabBarItemViewEvent:(UIControl *)itemView { if (!self.canItemRespondDoubleTouch) { return; } if (!self.selectedItem.qmui_doubleTapBlock) { return; } // 如果一定时间后仍未触发双击,则废弃当前的点击状态 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self qmuitb_revertTabBarItemTouch]; }); NSInteger selectedIndex = [self.items indexOfObject:self.selectedItem]; if (self.lastTouchedTabBarItemViewIndex == kLastTouchedTabBarItemIndexNone) { // 记录第一次点击的 index self.lastTouchedTabBarItemViewIndex = selectedIndex; } else if (self.lastTouchedTabBarItemViewIndex != selectedIndex) { // 后续的点击如果与第一次点击的 index 不一致,则认为是重新开始一次新的点击 [self qmuitb_revertTabBarItemTouch]; self.lastTouchedTabBarItemViewIndex = selectedIndex; return; } self.tabBarItemViewTouchCount ++; if (self.tabBarItemViewTouchCount == 2) { // 第二次点击了相同的 tabBarItem,触发双击事件 UITabBarItem *item = self.items[selectedIndex]; if (item.qmui_doubleTapBlock) { item.qmui_doubleTapBlock(item, selectedIndex); } [self qmuitb_revertTabBarItemTouch]; } } - (void)qmuitb_revertTabBarItemTouch { self.lastTouchedTabBarItemViewIndex = kLastTouchedTabBarItemIndexNone; self.tabBarItemViewTouchCount = 0; } @end @implementation UITabBarAppearance (QMUI) - (void)qmui_applyItemAppearanceWithBlock:(void (^)(UITabBarItemAppearance * _Nonnull))block { block(self.stackedLayoutAppearance); block(self.inlineLayoutAppearance); block(self.compactInlineLayoutAppearance); } @end ================================================ FILE: QMUIKit/UIKitExtensions/UITabBarItem+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import NS_ASSUME_NONNULL_BEGIN @interface UITabBarItem (QMUI) /** * 双击 tabBarItem 时的回调,默认为 nil。 * @param tabBarItem 被双击的 UITabBarItem,若需要拿到当前的 view 则通过 qmui_view 获取。 * @param index 被双击的 UITabBarItem 的序号 */ @property(nonatomic, copy, nullable) void (^qmui_doubleTapBlock)(UITabBarItem *tabBarItem, NSInteger index); /** * 获取一个UITabBarItem内显示图标的UIImageView,如果找不到则返回nil */ - (nullable UIImageView *)qmui_imageView; + (nullable UIImageView *)qmui_imageViewInTabBarButton:(nullable UIView *)tabBarButton; @end NS_ASSUME_NONNULL_END ================================================ FILE: QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import "UITabBarItem+QMUI.h" #import "QMUICore.h" #import "UIBarItem+QMUI.h" @implementation UITabBarItem (QMUI) QMUISynthesizeIdCopyProperty(qmui_doubleTapBlock, setQmui_doubleTapBlock) - (UIImageView *)qmui_imageView { return [self.class qmui_imageViewInTabBarButton:self.qmui_view]; } + (UIImageView *)qmui_imageViewInTabBarButton:(UIView *)tabBarButton { if (!tabBarButton) { return nil; } return [tabBarButton qmui_valueForKey:@"_imageView"]; } @end ================================================ FILE: QMUIKit/UIKitExtensions/UITableView+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import #import 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统一定义的宏来渲染外观 - (void)qmui_styledAsQMUITableView; /** * 获取某个 view 在 tableView 里的 indexPath * * 使用场景:例如每个 cell 内均有一个按钮,在该按钮的 addTarget 点击事件回调里可以用这个方法计算出按钮所在的 indexPath * * @param view 要计算的 UIView * @return view 所在的 indexPath,若不存在则返回 nil */ - (nullable NSIndexPath *)qmui_indexPathForRowAtView:(nullable UIView *)view; /** * 计算某个 view 处于当前 tableView 里的哪个 sectionHeaderView 内 * @param view 要计算的 UIView * @return view 所在的 sectionHeaderView 的 section,若不存在则返回 -1 */ - (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:(nullable NSIndexPath *)indexPath; /// 取消选择状态 - (void)qmui_clearsSelection; /** * 将指定的row滚到指定的位置(row的顶边缘和指定位置重叠),并对一些特殊情况做保护(例如列表内容不够一屏、要滚动的row是最后一条等) * @param offsetY 目标row要滚到的y值,这个y值是相对于tableView的frame而言的 * @param indexPath 要滚动的目标indexPath,如果该 indexPath 不合法则该方法不会有任何效果 * @param 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`替代。 * * `qmui_realContentSize`是实时通过计算最后一个section的frame,与footerView的frame比较得到实际的内容高度,这个过程不会导致额外的cellForRow调用,请放心使用。 */ @property(nonatomic, assign, readonly) CGSize qmui_realContentSize; /** * UITableView的tableHeaderView如果是UISearchBar的话,tableView.contentSize会强制设置为至少比bounds高(从而实现headerView的吸附效果),从而导致qmui_canScroll的判断不准确。所以为UITableView重写了qmui_canScroll方法 */ - (BOOL)qmui_canScroll; /** 等同于 UITableView 自 iOS 11 开始新增的同名方法,但兼容 iOS 11 以下的系统使用。 @param updates insert/delete/reload/move calls @param completion completion callback */ - (void)qmui_performBatchUpdates:(void (NS_NOESCAPE ^ _Nullable)(void))updates completion:(void (^ _Nullable)(BOOL finished))completion DEPRECATED_MSG_ATTRIBUTE("请使用系统的 -[UITableView performBatchUpdates:completion:],QMUI 4.4.0 已不再支持 iOS 10,没必要提供该兼容性之的接口了,后续会删除。"); @end /** 系统在 iOS 13 新增了 UITableViewStyleInsetGrouped 类型用于展示往内缩进、cell 带圆角的列表,而这个 Category 让 iOS 12 及以下的系统也能支持这种样式,iOS 13 也可以通过这个 Category 修改左右的缩进值和 cell 的圆角。 使用方式: 对于 UITableView,通过 -[UITableView initWithStyle:UITableViewStyleInsetGrouped] 初始化 tableView。 对于 UITableViewController,通过 -[UITableViewController initWithStyle:UITableViewStyleInsetGrouped] 初始化 tableViewController。 可通过 @c qmui_insetGroupedCornerRadius @c qmui_insetGroupedHorizontalInset 统一修改圆角值和左右缩进,如果要为不同 indexPath 指定不同圆角值,可在 -[UITableViewDelegate tableView:willDisplayCell:forRowAtIndexPath:] 内修改 cell.layer.cornerRadius 的值。 @note 对于 sectionHeader/footer,建议使用 QMUITableViewHeaderFooterView,或者继承系统的 UITableViewHeaderFooterView 并重写它的 sizeThatFits:、layoutSubviews 去计算高度和布局,sizeThatFits: 的参数 size.width 即为减去左右缩进后的宽度。如果直接用系统的 UITableViewHeaderFooterView,iOS 10 及以下多行文本时布局会错误,暂时无法解决,但如果业务项目本身不需要支持 iOS 10 及以下系统,那可忽略这个限制。 */ @interface UITableView (QMUI_InsetGrouped) /// 当使用 UITableViewStyleInsetGrouped 时可通过这个属性修改 cell 的圆角值,默认值为 10,也即 iOS 13 系统默认表现。如果要为不同 indexPath 指定不同圆角值,可在 -[UITableViewDelegate tableView:willDisplayCell:forRowAtIndexPath:] 内修改 cell.layer.cornerRadius 的值。 @property(nonatomic, assign) CGFloat qmui_insetGroupedCornerRadius UI_APPEARANCE_SELECTOR; /// 当使用 UITableViewStyleInsetGrouped 时可通过这个属性修改列表的左右缩进值,默认值为 20,也即 iOS 13 系统默认表现。 @property(nonatomic, assign) CGFloat qmui_insetGroupedHorizontalInset UI_APPEARANCE_SELECTOR; @end NS_ASSUME_NONNULL_END ================================================ FILE: QMUIKit/UIKitExtensions/UITableView+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import "UIView+QMUI.h" #import "UITableView+QMUI.h" #import "UITableViewCell+QMUI.h" #import "QMUICore.h" #import "UIScrollView+QMUI.h" #import "QMUILog.h" #import "NSObject+QMUI.h" #import "CALayer+QMUI.h" const NSUInteger kFloatValuePrecision = 4;// 统一一个小数点运算精度 @interface UITableView () @property(nonatomic, assign, readonly) CGRect qmui_indexFrame; @end @implementation UITableView (QMUI) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ 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 { [self qmui_scrollToRowAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; } } - (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.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.superview) { 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, kFloatValuePrecision);// 避免一些浮点数精度问题导致的计算错误 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; } } 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) { return QMUITableViewCellPositionSingleInSection; } if (indexPath.row == 0) { return QMUITableViewCellPositionFirstInSection; } if (indexPath.row == numberOfRowsInSection - 1) { return QMUITableViewCellPositionLastInSection; } return QMUITableViewCellPositionMiddleInSection; } - (BOOL)qmui_cellVisibleAtIndexPath:(NSIndexPath *)indexPath { NSArray *visibleCellIndexPaths = self.indexPathsForVisibleRows; for (NSIndexPath *visibleIndexPath in visibleCellIndexPaths) { if ([indexPath isEqual:visibleIndexPath]) { return YES; } } return NO; } - (void)qmui_clearsSelection { 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; if (canScrollRowToTop) { [self setContentOffset:CGPointMake(self.contentOffset.x, CGRectGetMinY(rectForRow) - offsetY) animated:animated]; } else { [self qmui_scrollToBottomAnimated:animated]; } } - (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; } CGSize contentSize = self.contentSize; CGFloat footerViewMaxY = CGRectGetMaxY(self.tableFooterView.frame); CGSize realContentSize = CGSizeMake(contentSize.width, footerViewMaxY); NSInteger lastSection = [self numberOfSections] - 1; if (lastSection < 0) { // 说明numberOfSetions为0,tableView没有cell,则直接取footerView的底边缘 return realContentSize; } CGRect lastSectionRect = [self rectForSection:lastSection]; realContentSize.height = fmax(realContentSize.height, CGRectGetMaxY(lastSectionRect)); return realContentSize; } - (BOOL)qmui_canScroll { // 没有高度就不用算了,肯定不可滚动,这里只是做个保护 if (CGRectGetHeight(self.bounds) <= 0) { return NO; } if ([self.tableHeaderView isKindOfClass:[UISearchBar class]]) { BOOL canScroll = self.qmui_realContentSize.height + UIEdgeInsetsGetVerticalValue(self.adjustedContentInset) > CGRectGetHeight(self.bounds); return canScroll; } else { return [super qmui_canScroll]; } } - (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]; } } - (void)QMUISymbolicUsingTableViewEstimatedHeightMakeWarning { QMUILog(@"UITableView (QMUI)", @"当开启了 UITableView 的 estimatedRow(SectionHeader / SectionFooter)Height 功能后,不应该手动修改 contentOffset 和 contentSize,也会影响 contentSize、sizeThatFits:、rectForXxx 等方法的计算,请注意确认当前是否存在不合理的业务代码。可添加 'QMUISymbolicUsingTableViewEstimatedHeightMakeWarning' 的 Symbolic Breakpoint 以捕捉此类信息。"); } - (void)qmui_performBatchUpdates:(void (NS_NOESCAPE ^ _Nullable)(void))updates completion:(void (^ _Nullable)(BOOL finished))completion { [self performBatchUpdates:updates completion:completion]; } - (CGRect)qmui_indexFrame { CGRect indexFrame = CGRectZero; [self qmui_performSelector:NSSelectorFromString(@"indexFrame") withPrimitiveReturnValue:&indexFrame]; return indexFrame; } @end @interface UITableViewCell (QMUI_Private) @property(nonatomic, assign, readwrite) QMUITableViewCellPosition qmui_cellPosition; @end @implementation UITableView (QMUI_InsetGrouped) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // -[UITableViewDelegate tableView:willDisplayCell:forRowAtIndexPath:] 比这个还晚,所以不用担心触发 delegate OverrideImplementation([UITableView class], NSSelectorFromString(@"_configureCellForDisplay:forIndexPath:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UITableView *selfObject, UITableViewCell *cell, NSIndexPath *indexPath) { // call super void (*originSelectorIMP)(id, SEL, UITableViewCell *, NSIndexPath *); originSelectorIMP = (void (*)(id, SEL, UITableViewCell *, NSIndexPath *))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, cell, indexPath); // UITableViewCell(QMUI) 内会根据 cellPosition 调整 separator 的布局,所以先在这里赋值以供那边使用 QMUITableViewCellPosition position = [selfObject qmui_positionForRowAtIndexPath:indexPath]; cell.qmui_cellPosition = position; if (selfObject.style == UITableViewStyleInsetGrouped) { CGFloat cornerRadius = selfObject.qmui_insetGroupedCornerRadius; if (position == QMUITableViewCellPositionMiddleInSection || position == QMUITableViewCellPositionNone) { cornerRadius = 0; } cell.layer.cornerRadius = cornerRadius; } if (cell.qmui_configureStyleBlock) { cell.qmui_configureStyleBlock(selfObject, cell, indexPath); } }; }); // -[UITableViewCell _setContentClipCorners:updateCorners:],用来控制系统 InsetGrouped 的圆角(很多情况都会触发系统更新圆角,例如设置 cell.backgroundColor、...,对于 iOS 12 及以下的系统,则靠 -[UITableView _configureCellForDisplay:forIndexPath:] 来处理 // - (void) _setContentClipCorners:(unsigned long)arg1 updateCorners:(BOOL)arg2; (0x10db0a5b7) OverrideImplementation([UITableViewCell class], NSSelectorFromString([NSString qmui_stringByConcat:@"_setContentClipCorners", @":", @"updateCorners", @":", nil]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UITableViewCell *selfObject, CACornerMask firstArgv, BOOL secondArgv) { // call super void (*originSelectorIMP)(id, SEL, CACornerMask, BOOL); originSelectorIMP = (void (*)(id, SEL, CACornerMask, BOOL))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); UITableView *tableView = selfObject.qmui_tableView; if (tableView && tableView.style == UITableViewStyleInsetGrouped) { CGFloat cornerRadius = tableView.qmui_insetGroupedCornerRadius; if (selfObject.qmui_cellPosition == QMUITableViewCellPositionMiddleInSection || selfObject.qmui_cellPosition == QMUITableViewCellPositionNone) { cornerRadius = 0; } selfObject.layer.cornerRadius = cornerRadius; } }; }); // -[UITableView layoutMargins],用来控制系统 InsetGrouped 的左右间距 OverrideImplementation([UITableView class], @selector(layoutMargins), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UIEdgeInsets(UITableView *selfObject) { // call super UIEdgeInsets (*originSelectorIMP)(id, SEL); originSelectorIMP = (UIEdgeInsets (*)(id, SEL))originalIMPProvider(); UIEdgeInsets result = originSelectorIMP(selfObject, originCMD); if (selfObject.style == UITableViewStyleInsetGrouped) { result.left = selfObject.safeAreaInsets.left + selfObject.qmui_insetGroupedHorizontalInset; result.right = selfObject.safeAreaInsets.right + selfObject.qmui_insetGroupedHorizontalInset; } return result; }; }); }); } static char kAssociatedObjectKey_insetGroupedCornerRadius; - (void)setQmui_insetGroupedCornerRadius:(CGFloat)qmui_insetGroupedCornerRadius { objc_setAssociatedObject(self, &kAssociatedObjectKey_insetGroupedCornerRadius, @(qmui_insetGroupedCornerRadius), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (self.style == UITableViewStyleInsetGrouped && self.indexPathsForVisibleRows.count) { [self reloadData]; } } - (CGFloat)qmui_insetGroupedCornerRadius { NSNumber *associatedValue = (NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_insetGroupedCornerRadius); if (!associatedValue) { // 从来没设置过(包括业务主动设置或者通过 UIAppearance 方式设置),则用 iOS 13 系统默认值 // 不在 UITableView init 时设置是因为那样会使 UIAppearance 失效 return 10; } return associatedValue.qmui_CGFloatValue; } static char kAssociatedObjectKey_insetGroupedHorizontalInset; - (void)setQmui_insetGroupedHorizontalInset:(CGFloat)qmui_insetGroupedHorizontalInset { objc_setAssociatedObject(self, &kAssociatedObjectKey_insetGroupedHorizontalInset, @(qmui_insetGroupedHorizontalInset), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (self.style == UITableViewStyleInsetGrouped && self.indexPathsForVisibleRows.count) { [self reloadData]; } } - (CGFloat)qmui_insetGroupedHorizontalInset { NSNumber *associatedValue = (NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_insetGroupedHorizontalInset); if (!associatedValue) { // 从来没设置过(包括业务主动设置或者通过 UIAppearance 方式设置),则用 iOS 13 系统默认值 // 不在 UITableView init 时设置是因为那样会使 UIAppearance 失效 return PreferredValueForVisualDevice(20, 15); } return associatedValue.qmui_CGFloatValue; } @end ================================================ FILE: QMUIKit/UIKitExtensions/UITableViewCell+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UITableViewCell+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UITableViewHeaderFooterView+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UITableViewHeaderFooterView+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UITextField+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 2017/3/29. // #import #import NS_ASSUME_NONNULL_BEGIN @interface UITextField (QMUI) /// UITextView 在输入框开头继续按删除按键,也会触发 shouldChange 的 delegate,但 UITextField 没这个行为,所以提供这个属性,当置为 YES 时,行为与 UITextView 一致,在输入框开头删除也会询问 delegate 并传 range(0, 0) 和空的 text。 /// 默认为 NO。 @property(nonatomic, assign) BOOL qmui_respondsToDeleteActionAtLeading; /// UITextField 只有 selectedTextRange 属性(在 UITextInput 协议里定义),相对而言没有 NSRange 那么直观,因此这里提供 NSRange 类型的操作方式可以主动设置光标的位置或选中的区域 @property(nonatomic, assign) NSRange qmui_selectedRange; /// 输入框右边的 clearButton,在 UITextField 初始化后就存在 @property(nullable, nonatomic, weak, readonly) UIButton *qmui_clearButton; /// 自定义 clearButton 的图片,设置成nil,恢复到系统默认的图片 @property(nullable, nonatomic, strong) UIImage *qmui_clearButtonImage UI_APPEARANCE_SELECTOR; /** * convert UITextRange to NSRange, for example, [self qmui_convertNSRangeFromUITextRange:self.markedTextRange] */ - (NSRange)qmui_convertNSRangeFromUITextRange:(UITextRange *)textRange; /** * convert NSRange to UITextRange * @return return nil if range is invalidate. */ - (nullable UITextRange *)qmui_convertUITextRangeFromNSRange:(NSRange)range; @end NS_ASSUME_NONNULL_END ================================================ FILE: QMUIKit/UIKitExtensions/UITextField+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 2017/3/29. // #import "UITextField+QMUI.h" #import "NSObject+QMUI.h" #import "QMUICore.h" #import "UIImage+QMUI.h" @implementation UITextField (QMUI) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // iOS 13 及以下版本需要重写该方法才能替换 // - (id) _clearButtonImageForState:(unsigned long)arg1; // https://github.com/Tencent/QMUI_iOS/issues/1477 if (@available(iOS 14.0, *)) { } else { OverrideImplementation([UITextField class], NSSelectorFromString(@"_clearButtonImageForState:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UIImage *(UITextField *selfObject, UIControlState firstArgv) { if (selfObject.qmui_clearButtonImage && (firstArgv & UIControlStateNormal) == UIControlStateNormal) { return selfObject.qmui_clearButtonImage; } // call super UIImage *(*originSelectorIMP)(id, SEL, UIControlState); originSelectorIMP = (UIImage *(*)(id, SEL, UIControlState))originalIMPProvider(); UIImage *result = originSelectorIMP(selfObject, originCMD, firstArgv); return result; }; }); } }); } - (void)setQmui_selectedRange:(NSRange)qmui_selectedRange { self.selectedTextRange = [self qmui_convertUITextRangeFromNSRange:qmui_selectedRange]; } - (NSRange)qmui_selectedRange { return [self qmui_convertNSRangeFromUITextRange:self.selectedTextRange]; } - (UIButton *)qmui_clearButton { return [self qmui_valueForKey:@"clearButton"]; } 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]; } } } - (UIImage *)qmui_clearButtonImage { return (UIImage *)objc_getAssociatedObject(self, &kAssociatedObjectKey_clearButtonImage); } - (NSRange)qmui_convertNSRangeFromUITextRange:(UITextRange *)textRange { NSInteger location = [self offsetFromPosition:self.beginningOfDocument toPosition:textRange.start]; NSInteger length = [self offsetFromPosition:textRange.start toPosition:textRange.end]; return NSMakeRange(location, length); } - (UITextRange *)qmui_convertUITextRangeFromNSRange:(NSRange)range { if (range.location == NSNotFound || NSMaxRange(range) > self.text.length) { return nil; } UITextPosition *beginning = self.beginningOfDocument; UITextPosition *startPosition = [self positionFromPosition:beginning offset:range.location]; UITextPosition *endPosition = [self positionFromPosition:beginning offset:NSMaxRange(range)]; return [self textRangeFromPosition:startPosition toPosition:endPosition]; } static char kAssociatedObjectKey_respondsToDeleteActionAtLeading; - (void)setQmui_respondsToDeleteActionAtLeading:(BOOL)respondsToDeleteActionAtLeading { objc_setAssociatedObject(self, &kAssociatedObjectKey_respondsToDeleteActionAtLeading, @(respondsToDeleteActionAtLeading), OBJC_ASSOCIATION_RETAIN_NONATOMIC); [QMUIHelper executeBlock:^{ OverrideImplementation([UITextField class], @selector(deleteBackward), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UITextField *selfObject) { BOOL deletingAtLeading = NSEqualRanges(selfObject.qmui_selectedRange, NSMakeRange(0, 0)); if (selfObject.qmui_respondsToDeleteActionAtLeading && deletingAtLeading) { QMUILog(@"UITextField (QMUI)", @"光标已在输入框开头的情况下依然按下删除按键。"); if ([selfObject.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) { [selfObject.delegate textField:selfObject shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@""]; } } // call super void (*originSelectorIMP)(id, SEL); originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); originSelectorIMP(selfObject, originCMD); }; }); } oncePerIdentifier:@"UITextField (QMUI) delete"]; } - (BOOL)qmui_respondsToDeleteActionAtLeading { return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_respondsToDeleteActionAtLeading)) boolValue]; } @end ================================================ FILE: QMUIKit/UIKitExtensions/UITextInputTraits+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UITextInputTraits+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UITextView+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 2017/3/29. // #import #import 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 跳到最后一个字符,导致在中间修改文字后光标会跳到末尾,所以设置前要保存一下,设置后恢复过来 */ - (void)qmui_setTextKeepingSelectedRange:(NSString *)text; /** * 设置 attributedText 会让 selectedTextRange 跳到最后一个字符,导致在中间修改文字后光标会跳到末尾,所以设置前要保存一下,设置后恢复过来 */ - (void)qmui_setAttributedTextKeepingSelectedRange:(NSAttributedString *)attributedText; /** [UITextView scrollRangeToVisible:] 并不会考虑 textContainerInset.bottom,所以使用这个方法来代替 @param range 要滚动到的文字区域,如果 range 非法则什么都不做 */ - (void)qmui_scrollRangeToVisible:(NSRange)range; /** * 将光标滚到可视区域 */ - (void)qmui_scrollCaretVisibleAnimated:(BOOL)animated; @end NS_ASSUME_NONNULL_END ================================================ FILE: QMUIKit/UIKitExtensions/UITextView+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 2017/3/29. // #import "UITextView+QMUI.h" #import "QMUICore.h" #import "UIScrollView+QMUI.h" @implementation UITextView (QMUI) #ifdef IOS17_SDK_ALLOWED + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // UIScrollView.clipsToBounds 默认值为 YES,但如果是 Xcode 15 编译的包,UITextView.scrollEnabled = NO 时会强制把 clipsToBounds 置为 NO,导致 UITextView 设置了 backgroundColor 和 cornerRadius 时会看不到圆角(因为背景色溢出了),所以这里统一改回去 clipsToBounds = YES if (@available(iOS 17.0, *)) { OverrideImplementation([UITextView class], @selector(setScrollEnabled:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UITextView *selfObject, BOOL firstArgv) { // call super void (*originSelectorIMP)(id, SEL, BOOL); originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); originSelectorIMP(selfObject, originCMD, firstArgv); if (!firstArgv) { selfObject.clipsToBounds = YES; } }; }); } }); } #endif - (void)qmui_updateContentSize { SEL selector = NSSelectorFromString([NSString qmui_stringByConcat:@"_", @"updateContentSize", nil]); if ([self respondsToSelector:selector]) { BeginIgnorePerformSelectorLeaksWarning [self performSelector:selector]; EndIgnorePerformSelectorLeaksWarning } } - (NSRange)qmui_selectedRange { return [self qmui_convertNSRangeFromUITextRange:self.selectedTextRange]; } - (NSRange)qmui_convertNSRangeFromUITextRange:(UITextRange *)textRange { NSInteger location = [self offsetFromPosition:self.beginningOfDocument toPosition:textRange.start]; NSInteger length = [self offsetFromPosition:textRange.start toPosition:textRange.end]; 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; self.selectedTextRange = selectedTextRange; } - (void)qmui_setAttributedTextKeepingSelectedRange:(NSAttributedString *)attributedText { UITextRange *selectedTextRange = self.selectedTextRange; self.attributedText = attributedText; self.selectedTextRange = selectedTextRange; } - (void)qmui_scrollRangeToVisible:(NSRange)range { if (CGRectIsEmpty(self.bounds)) return; 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); } } } if (!CGRectIsEmpty(rect)) { rect = [self convertRect:rect fromView:self.textInputView]; [self _scrollRectToVisible:rect animated:YES]; } } - (void)qmui_scrollCaretVisibleAnimated:(BOOL)animated { if (CGRectIsEmpty(self.bounds)) return; CGRect caretRect = [self caretRectForPosition:self.selectedTextRange.end]; [self _scrollRectToVisible:caretRect animated:animated]; } - (void)_scrollRectToVisible:(CGRect)rect animated:(BOOL)animated { // scrollEnabled 为 NO 时可能产生不合法的 rect 值 https://github.com/Tencent/QMUI_iOS/issues/205 if (!CGRectIsValidated(rect)) { return; } 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIToolbar+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIToolbar+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UITraitCollection+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UITraitCollection+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIView+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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) @param size 初始化时的 size @return 初始化得到的实例 */ - (instancetype)qmui_initWithSize:(CGSize)size; /** 将要设置的 frame 用 CGRectApplyAffineTransformWithAnchorPoint 处理后再设置 注意这个方式会导致 self.bounds 也受 transform 的影响(系统默认行为是 frame 受 transform 影响,center 和 bounds 不会),如果有需要访问 self.bounds 的情况,请避免使用这个方式。 */ @property(nonatomic, assign) CGRect qmui_frameApplyTransform; /** 在 iOS 11 及之后的版本,此属性将返回系统已有的 self.safeAreaInsets。在之前的版本此属性返回 UIEdgeInsetsZero */ @property(nonatomic, assign, readonly) UIEdgeInsets qmui_safeAreaInsets DEPRECATED_MSG_ATTRIBUTE("请使用系统的 UIView.safeAreaInsets,QMUI 4.4.0 已不再支持 iOS 10,没必要提供该兼容性之的接口了,后续会删除。"); /** 有修改过 tintColor,则不会再受 superview.tintColor 的影响 */ @property(nonatomic, assign, readonly) BOOL qmui_tintColorCustomized; /// 响应区域需要改变的大小,负值表示往外扩大,正值表示往内缩小。 /// 特别地,如果对 UISlider 使用,则扩大的是圆点的区域。 /// 当你引入了 QMUINavigationButton,它会使 UIBarButtonItem.customView 也可使用 qmui_outsideEdge(默认不可以,因为 customView 的父容器和 customView 一样大,所以 UINavigationBar 感知不到 customView 有 qmui_outsideEdge)。 @property(nonatomic,assign) UIEdgeInsets qmui_outsideEdge; /** 移除当前所有 subviews */ - (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) /** 在 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 值不相等时才会被调用 */ @property(nullable, nonatomic, copy) CGRect (^qmui_frameWillChangeBlock)(__kindof UIView *view, CGRect followingFrame); /** 在 UIView 的 frame 变化后会调用这个 block,变化途径包括 setFrame:、setBounds:、setCenter:、setTransform:,可用于监听布局的变化,或者在不方便重写 layoutSubviews 时使用这个 block 代替。 @param view 当前的 view 本身,方便使用,省去 weak 操作 @param precedingFrame 修改前的 frame 值 */ @property(nullable, nonatomic, copy) void (^qmui_frameDidChangeBlock)(__kindof UIView *view, CGRect precedingFrame); /** 在 - [UIView layoutSubviews] 调用后就调用的 block @param view 当前的 view 本身,方便使用,省去 weak 操作 */ @property(nullable, nonatomic, copy) void (^qmui_layoutSubviewsBlock)(__kindof UIView *view); /** 在 UIView 的 sizeThatFits: 调用后就调用的 block,可返回一个修改后的值来作为原方法的返回值 @param view 当前的 view 本身,方便使用,省去 weak 操作 @param size sizeThatFits: 方法被调用时传进来的参数 size @param superResult 原本的 sizeThatFits: 方法的返回值 */ @property(nullable, nonatomic, copy) CGSize (^qmui_sizeThatFitsBlock)(__kindof UIView *view, CGSize size, CGSize superResult); /** 当 tintColorDidChange 被调用的时候会调用这个 block,就不用重写方法了 @param view 当前的 view 本身,方便使用,省去 weak 操作 */ @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); @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) /** * 判断当前类是否有重写某个指定的 UIView 的方法 * @param selector 要判断的方法 * @return YES 表示当前类重写了指定的方法,NO 表示没有重写,使用的是 UIView 默认的实现 */ - (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector; @end /** * 方便地将某个 UIView 截图并转成一个 UIImage,注意如果这个 UIView 本身做了 transform,也不会在截图上反映出来,截图始终都是原始 UIView 的截图。 */ @interface UIView (QMUI_Snapshotting) - (UIImage *)qmui_snapshotLayerImage; - (UIImage *)qmui_snapshotImageAfterScreenUpdates:(BOOL)afterScreenUpdates; @end /** 当某个 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; /** * 对 view.frame 操作的简便封装,注意 view 与 view 之间互相计算时,需要保证处于同一个坐标系内。 */ @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; /// 保持其他三个边缘的位置不变的情况下,将底边缘拓展到某个指定的位置,注意高度会跟随变化。 @property(nonatomic, assign) CGFloat qmui_extendToBottom; /// 保持其他三个边缘的位置不变的情况下,将右边缘拓展到某个指定的位置,注意宽度会跟随变化。 @property(nonatomic, assign) CGFloat qmui_extendToRight; /// 获取当前 view 在 superview 内水平居中时的 left @property(nonatomic, assign, readonly) CGFloat qmui_leftWhenCenterInSuperview; /// 获取当前 view 在 superview 内垂直居中时的 top @property(nonatomic, assign, readonly) CGFloat qmui_topWhenCenterInSuperview; @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 /** * Debug UIView 的时候用,对某个 view 的 subviews 都添加一个半透明的背景色,方面查看 view 的布局情况 */ @interface UIView (QMUI_Debug) /// 是否需要添加debug背景色,默认NO @property(nonatomic, assign) BOOL qmui_shouldShowDebugColor; /// 是否每个view的背景色随机,如果不随机则统一使用半透明红色,默认NO @property(nonatomic, assign) BOOL qmui_needsDifferentDebugColor; @end NS_ASSUME_NONNULL_END ================================================ FILE: QMUIKit/UIKitExtensions/UIView+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 15/7/20. // #import "UIView+QMUI.h" #import "QMUICore.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" @implementation UIView (QMUI) QMUISynthesizeBOOLProperty(qmui_tintColorCustomized, setQmui_tintColorCustomized) QMUISynthesizeIdCopyProperty(qmui_frameWillChangeBlock, setQmui_frameWillChangeBlock) QMUISynthesizeIdCopyProperty(qmui_frameDidChangeBlock, setQmui_frameDidChangeBlock) + (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)setQmui_frameApplyTransform:(CGRect)qmui_frameApplyTransform { self.frame = CGRectApplyAffineTransformWithAnchorPoint(qmui_frameApplyTransform, self.transform, self.layer.anchorPoint); } - (CGRect)qmui_frameApplyTransform { return self.frame; } - (UIEdgeInsets)qmui_safeAreaInsets { return self.safeAreaInsets; } - (void)qmui_removeAllSubviews { [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; } static char kAssociatedObjectKey_outsideEdge; - (void)setQmui_outsideEdge:(UIEdgeInsets)qmui_outsideEdge { objc_setAssociatedObject(self, &kAssociatedObjectKey_outsideEdge, @(qmui_outsideEdge), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (!UIEdgeInsetsEqualToEdgeInsets(qmui_outsideEdge, UIEdgeInsetsZero)) { [QMUIHelper executeBlock:^{ OverrideImplementation([UIView class], @selector(pointInside:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^BOOL(UIControl *selfObject, CGPoint point, UIEvent *event) { if (!UIEdgeInsetsEqualToEdgeInsets(selfObject.qmui_outsideEdge, UIEdgeInsetsZero) && selfObject.alpha > 0.01 && !selfObject.hidden && !CGRectIsEmpty(selfObject.frame)) { CGRect rect = UIEdgeInsetsInsetRect(selfObject.bounds, selfObject.qmui_outsideEdge); BOOL result = CGRectContainsPoint(rect, point); return result; } // call super BOOL (*originSelectorIMP)(id, SEL, CGPoint, UIEvent *); originSelectorIMP = (BOOL (*)(id, SEL, CGPoint, UIEvent *))originalIMPProvider(); BOOL result = originSelectorIMP(selfObject, originCMD, point, event); return result; }; }); } oncePerIdentifier:@"UIView (QMUI) outsideEdge"]; if ([self isKindOfClass:UISlider.class]) { [QMUIHelper executeBlock:^{ if (@available(iOS 14.0, *)) { // -[_UISlideriOSVisualElement thumbHitEdgeInsets] OverrideImplementation(NSClassFromString([NSString qmui_stringByConcat:@"_", @"UISlider", @"iOS", @"VisualElement", nil]), NSSelectorFromString(@"thumbHitEdgeInsets"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UIEdgeInsets(UIView *selfObject) { // call super UIEdgeInsets (*originSelectorIMP)(id, SEL); originSelectorIMP = (UIEdgeInsets (*)(id, SEL))originalIMPProvider(); UIEdgeInsets result = originSelectorIMP(selfObject, originCMD); UISlider *slider = (UISlider *)selfObject.superview; if ([slider isKindOfClass:UISlider.class] && !UIEdgeInsetsEqualToEdgeInsets(slider.qmui_outsideEdge, UIEdgeInsetsZero)) { result = UIEdgeInsetsConcat(result, slider.qmui_outsideEdge); } return result; }; }); } else { // -[UISlider _thumbHitEdgeInsets] OverrideImplementation([UISlider class], NSSelectorFromString(@"_thumbHitEdgeInsets"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UIEdgeInsets(UISlider *selfObject) { // call super UIEdgeInsets (*originSelectorIMP)(id, SEL); originSelectorIMP = (UIEdgeInsets (*)(id, SEL))originalIMPProvider(); UIEdgeInsets result = originSelectorIMP(selfObject, originCMD); if (!UIEdgeInsetsEqualToEdgeInsets(selfObject.qmui_outsideEdge, UIEdgeInsetsZero)) { result = UIEdgeInsetsConcat(result, selfObject.qmui_outsideEdge); } return result; }; }); } } oncePerIdentifier:@"UIView (QMUI) outsideEdge slider"]; } } } - (UIEdgeInsets)qmui_outsideEdge { return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_outsideEdge)) UIEdgeInsetsValue]; } static char kAssociatedObjectKey_tintColorDidChangeBlock; - (void)setQmui_tintColorDidChangeBlock:(void (^)(__kindof UIView * _Nonnull))qmui_tintColorDidChangeBlock { objc_setAssociatedObject(self, &kAssociatedObjectKey_tintColorDidChangeBlock, qmui_tintColorDidChangeBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); if (qmui_tintColorDidChangeBlock) { [QMUIHelper executeBlock:^{ ExtendImplementationOfVoidMethodWithoutArguments([UIView class], @selector(tintColorDidChange), ^(UIView *selfObject) { if (selfObject.qmui_tintColorDidChangeBlock) { selfObject.qmui_tintColorDidChangeBlock(selfObject); } }); } oncePerIdentifier:@"UIView (QMUI) tintColorDidChangeBlock"]; } } - (void (^)(__kindof UIView * _Nonnull))qmui_tintColorDidChangeBlock { return (void (^)(__kindof UIView * _Nonnull))objc_getAssociatedObject(self, &kAssociatedObjectKey_tintColorDidChangeBlock); } static char kAssociatedObjectKey_hitTestBlock; - (void)setQmui_hitTestBlock:(__kindof UIView * _Nullable (^)(CGPoint, UIEvent * _Nullable, __kindof UIView * _Nullable))qmui_hitTestBlock { objc_setAssociatedObject(self, &kAssociatedObjectKey_hitTestBlock, qmui_hitTestBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); [QMUIHelper executeBlock:^{ ExtendImplementationOfNonVoidMethodWithTwoArguments([UIView class], @selector(hitTest:withEvent:), CGPoint, UIEvent *, UIView *, ^UIView *(UIView *selfObject, CGPoint point, UIEvent *event, UIView *originReturnValue) { if (selfObject.qmui_hitTestBlock) { UIView *view = selfObject.qmui_hitTestBlock(point, event, originReturnValue); return view; } return originReturnValue; }); } oncePerIdentifier:@"UIView (QMUI) hitTestBlock"]; } - (__kindof UIView * _Nonnull (^)(CGPoint, UIEvent * _Nonnull, __kindof UIView * _Nonnull))qmui_hitTestBlock { return (__kindof UIView * _Nonnull (^)(CGPoint, UIEvent * _Nonnull, __kindof UIView * _Nonnull))objc_getAssociatedObject(self, &kAssociatedObjectKey_hitTestBlock); } - (CGPoint)qmui_convertPoint:(CGPoint)point toView:(nullable UIView *)view { if (view) { return [view qmui_convertPoint:point fromView:view]; } return [self convertPoint:point toView:view]; } - (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 { if (animations) { animations(); } if (completion) { completion(YES); } } } + (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 { if (animations) { animations(); } if (completion) { completion(YES); } } } + (void)qmui_animateWithAnimated:(BOOL)animated duration:(NSTimeInterval)duration animations:(void (^ __nullable)(void))animations { if (animated) { [UIView animateWithDuration:duration animations:animations]; } else { if (animations) { 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 { 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 @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], [UITextField class], [UISlider class], [UISwitch class], [UIActivityIndicatorView class], [UIProgressView class], [UIPageControl class], [UIStepper class], [UITableView class], [UITableViewCell class], [UIImageView class], [UICollectionView class], [UICollectionViewCell class], [UICollectionReusableView class], [UITextView class], [UIScrollView class], [UIDatePicker class], [UIPickerView class], [UIVisualEffectView class], // Apple 不再接受使用了 UIWebView 的 App 提交,所以这里去掉 UIWebView // https://github.com/Tencent/QMUI_iOS/issues/741 // [UIWebView class], [UIWindow class], [UINavigationBar class], [UIToolbar class], [UITabBar class], [UISearchBar class], [UIControl class], [UIView class], nil]; for (NSInteger i = 0, l = viewSuperclasses.count; i < l; i++) { Class superclass = viewSuperclasses[i]; if ([self qmui_hasOverrideMethod:selector ofSuperclass:superclass]) { return YES; } } return NO; } @end const CGFloat QMUIViewSelfSizingHeight = INFINITY; const CGSize QMUIViewFixedSizeNone = {-1, -1}; @implementation UIView (QMUI_Layout) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ OverrideImplementation([UIView class], @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIView *selfObject, CGRect frame) { 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); } }; }); }); } - (CGFloat)qmui_top { return CGRectGetMinY(self.frame); } - (void)setQmui_top:(CGFloat)top { self.frame = CGRectSetY(self.frame, top); } - (CGFloat)qmui_left { return CGRectGetMinX(self.frame); } - (void)setQmui_left:(CGFloat)left { self.frame = CGRectSetX(self.frame, left); } - (CGFloat)qmui_bottom { return CGRectGetMaxY(self.frame); } - (void)setQmui_bottom:(CGFloat)bottom { self.frame = CGRectSetY(self.frame, bottom - CGRectGetHeight(self.frame)); } - (CGFloat)qmui_right { return CGRectGetMaxX(self.frame); } - (void)setQmui_right:(CGFloat)right { self.frame = CGRectSetX(self.frame, right - CGRectGetWidth(self.frame)); } - (CGPoint)qmui_origin { return self.frame.origin; } - (void)setQmui_origin:(CGPoint)qmui_origin { self.center = CGPointMake(qmui_origin.x + CGRectGetWidth(self.frame) / 2, qmui_origin.y + CGRectGetHeight(self.frame) / 2); } - (CGFloat)qmui_width { return CGRectGetWidth(self.frame); } - (void)setQmui_width:(CGFloat)width { self.frame = CGRectSetWidth(self.frame, width); } - (CGFloat)qmui_height { return CGRectGetHeight(self.frame); } - (void)setQmui_height:(CGFloat)height { self.frame = CGRectSetHeight(self.frame, height); } - (CGSize)qmui_size { return self.frame.size; } - (void)setQmui_size:(CGSize)qmui_size { self.frame = CGRectSetSize(self.frame, qmui_size); } static char kAssociatedObjectKey_fixedSize; - (void)setQmui_fixedSize:(CGSize)qmui_fixedSize { objc_setAssociatedObject(self, &kAssociatedObjectKey_fixedSize, @(qmui_fixedSize), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (!CGSizeEqualToSize(qmui_fixedSize, QMUIViewFixedSizeNone)) { self.qmui_sizeThatFitsBlock = ^CGSize(__kindof UIView * _Nonnull view, CGSize size, CGSize superResult) { if (!CGSizeEqualToSize(view.qmui_fixedSize, QMUIViewFixedSizeNone)) { return view.qmui_fixedSize; } return superResult; }; self.qmui_size = qmui_fixedSize; } else { self.qmui_sizeThatFitsBlock = nil; } } - (CGSize)qmui_fixedSize { NSNumber *result = objc_getAssociatedObject(self, &kAssociatedObjectKey_fixedSize); if (!result) { return QMUIViewFixedSizeNone; } return result.CGSizeValue; } - (CGFloat)qmui_extendToTop { return self.qmui_top; } - (void)setQmui_extendToTop:(CGFloat)qmui_extendToTop { self.qmui_height = self.qmui_bottom - qmui_extendToTop; self.qmui_top = qmui_extendToTop; } - (CGFloat)qmui_extendToLeft { return self.qmui_left; } - (void)setQmui_extendToLeft:(CGFloat)qmui_extendToLeft { self.qmui_width = self.qmui_right - qmui_extendToLeft; self.qmui_left = qmui_extendToLeft; } - (CGFloat)qmui_extendToBottom { return self.qmui_bottom; } - (void)setQmui_extendToBottom:(CGFloat)qmui_extendToBottom { self.qmui_height = qmui_extendToBottom - self.qmui_top; self.qmui_bottom = qmui_extendToBottom; } - (CGFloat)qmui_extendToRight { return self.qmui_right; } - (void)setQmui_extendToRight:(CGFloat)qmui_extendToRight { self.qmui_width = qmui_extendToRight - self.qmui_left; self.qmui_right = qmui_extendToRight; } - (CGFloat)qmui_leftWhenCenterInSuperview { return CGFloatGetCenter(CGRectGetWidth(self.superview.bounds), CGRectGetWidth(self.frame)); } - (CGFloat)qmui_topWhenCenterInSuperview { return CGFloatGetCenter(CGRectGetHeight(self.superview.bounds), CGRectGetHeight(self.frame)); } @end @implementation UIView (CGAffineTransform) - (CGFloat)qmui_scaleX { return self.transform.a; } - (CGFloat)qmui_scaleY { return self.transform.d; } - (CGFloat)qmui_translationX { return self.transform.tx; } - (CGFloat)qmui_translationY { return self.transform.ty; } @end @implementation UIView (QMUI_Snapshotting) - (UIImage *)qmui_snapshotLayerImage { return [UIImage qmui_imageWithView:self]; } - (UIImage *)qmui_snapshotImageAfterScreenUpdates:(BOOL)afterScreenUpdates { return [UIImage qmui_imageWithView:self afterScreenUpdates: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 ================================================ FILE: QMUIKit/UIKitExtensions/UIView+QMUIBorder.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIView+QMUIBorder.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIViewController+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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 }; @interface UIViewController (QMUI) /// 当前 UIViewController.class 是否为系统默认的几个 container viewController(也即 UINavigationController、UITabBarController、UISplitViewController)。 @property(class, nonatomic, assign, readonly) BOOL qmui_isSystemContainerViewController; /// 当前 UIViewController 是否为系统默认的几个 container viewController(也即 UINavigationController、UITabBarController、UISplitViewController)。 @property(nonatomic, assign, readonly) BOOL qmui_isSystemContainerViewController; /** 获取和自身处于同一个UINavigationController里的上一个UIViewController */ @property(nullable, nonatomic, weak, readonly) UIViewController *qmui_previousViewController; /** 获取上一个UIViewController的title,可用于设置自定义返回按钮的文字 */ @property(nullable, nonatomic, copy, readonly) NSString *qmui_previousViewControllerTitle; /** * 获取当前controller里的最高层可见viewController(可见的意思是还会判断self.view.window是否存在) * * @see 如果要获取当前App里的可见viewController,请使用 [QMUIHelper visibleViewController] * * @return 当前controller里的最高层可见viewController */ - (nullable UIViewController *)qmui_visibleViewControllerIfExist; /** * 当前 viewController 是否是被以 present 的方式显示的,是则返回 YES,否则返回 NO * @warning 对于被放在 UINavigationController 里显示的 UIViewController,如果 self 是 self.navigationController 的第一个 viewController,则如果 self.navigationController 是被 present 起来的,那么 self.qmui_isPresented = self.navigationController.qmui_isPresented = YES。利用这个特性,可以方便地给 navigationController 的第一个界面的左上角添加关闭按钮。 */ - (BOOL)qmui_isPresented; /** * 是否应该响应一些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) /** * 判断当前类是否有重写某个指定的 UIViewController 的方法 * @param selector 要判断的方法 * @return YES 表示当前类重写了指定的方法,NO 表示没有重写,使用的是 UIViewController 默认的实现 */ - (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 ================================================ FILE: QMUIKit/UIKitExtensions/UIViewController+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 16/1/12. // #import "UIViewController+QMUI.h" #import "UINavigationController+QMUI.h" #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) 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, ^{ 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 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); }; }); } // iOS 11 及以后不 override prefersStatusBarHidden 而是通过私有方法来实现,是因为系统会先通过 +[UIViewController doesOverrideViewControllerMethod:inBaseClass:] 方法来判断当前的 UIViewController 有没有重写 prefersStatusBarHidden 方法,有的话才会去调用 prefersStatusBarHidden,而如果我们用 swizzle 的方式去重写 prefersStatusBarHidden,系统依然会认为你没有重写该方法,于是不会调用,于是 block 无效。对于 iOS 10 及以前的系统没有这种逻辑,所以没问题。 // 特别的,只有 hidden 操作有这种逻辑,而 style、animation 等操作不管在哪个 iOS 版本里都是没有这种逻辑的 OverrideImplementation([UIViewController class], NSSelectorFromString(@"_preferredStatusBarVisibility"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^NSInteger(UIViewController *selfObject) { // 为了保证重写 prefersStatusBarHidden 的优先级比 block 高,这里要判断一下 qmui_hasOverrideUIKitMethod 的值 if (![selfObject qmui_hasOverrideUIKitMethod:@selector(prefersStatusBarHidden)] && selfObject.qmui_prefersStatusBarHiddenBlock) { return selfObject.qmui_prefersStatusBarHiddenBlock() ? 1 : 2;// 系统返回的 1 表示隐藏,2 表示显示,0 不清楚含义 } // call super NSInteger (*originSelectorIMP)(id, SEL); originSelectorIMP = (NSInteger (*)(id, SEL))originalIMPProvider(); NSInteger result = originSelectorIMP(selfObject, originCMD); return result; }; }); OverrideImplementation([UIViewController class], @selector(preferredStatusBarStyle), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UIStatusBarStyle(UIViewController *selfObject) { if (selfObject.qmui_preferredStatusBarStyleBlock) { return selfObject.qmui_preferredStatusBarStyleBlock(); } // call super UIStatusBarStyle (*originSelectorIMP)(id, SEL); originSelectorIMP = (UIStatusBarStyle (*)(id, SEL))originalIMPProvider(); UIStatusBarStyle result = originSelectorIMP(selfObject, originCMD); return result; }; }); OverrideImplementation([UIViewController class], @selector(preferredStatusBarUpdateAnimation), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^UIStatusBarAnimation(UIViewController *selfObject) { if (selfObject.qmui_preferredStatusBarUpdateAnimationBlock) { return selfObject.qmui_preferredStatusBarUpdateAnimationBlock(); } // call super UIStatusBarAnimation (*originSelectorIMP)(id, SEL); originSelectorIMP = (UIStatusBarAnimation (*)(id, SEL))originalIMPProvider(); UIStatusBarAnimation result = originSelectorIMP(selfObject, originCMD); return result; }; }); OverrideImplementation([UIViewController class], @selector(prefersHomeIndicatorAutoHidden), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^BOOL(UIViewController *selfObject) { if (selfObject.qmui_prefersHomeIndicatorAutoHiddenBlock) { return selfObject.qmui_prefersHomeIndicatorAutoHiddenBlock(); } // call super BOOL (*originSelectorIMP)(id, SEL); originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); BOOL result = originSelectorIMP(selfObject, originCMD); return result; }; }); }); } - (NSString *)qmuivc_description { 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:@"; 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:@"; viewControllers(%@): %@; selectedViewController(%@): %@", @(tabBarController.viewControllers.count), [self descriptionWithViewControllers:tabBarController.viewControllers], @(tabBarController.selectedIndex), [tabBarController.selectedViewController qmuivc_description]]; result = [result stringByAppendingString:tabBarDescription]; } return result; } - (NSString *)descriptionWithViewControllers:(NSArray *)viewControllers { NSMutableString *string = [[NSMutableString alloc] init]; [string appendString:@"( "]; for (NSInteger i = 0, l = viewControllers.count; i < l; i++) { [string appendFormat:@"[%@]%@%@", @(i), [viewControllers[i] qmuivc_description], i < l - 1 ? @"," : @""]; } [string appendString:@" )"]; return [string copy]; } + (BOOL)qmui_isSystemContainerViewController { for (Class clz in @[UINavigationController.class, UITabBarController.class, UISplitViewController.class]) { if ([self isSubclassOfClass:clz]) { return YES; } } return NO; } - (BOOL)qmui_isSystemContainerViewController { return self.class.qmui_isSystemContainerViewController; } static char kAssociatedObjectKey_visibleState; - (void)setQmui_visibleState:(QMUIViewControllerVisibleState)qmui_visibleState { BOOL valueChanged = self.qmui_visibleState != qmui_visibleState; objc_setAssociatedObject(self, &kAssociatedObjectKey_visibleState, @(qmui_visibleState), OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (valueChanged && self.qmui_visibleStateDidChangeBlock) { self.qmui_visibleStateDidChangeBlock(self, qmui_visibleState); } } - (QMUIViewControllerVisibleState)qmui_visibleState { return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_visibleState)) unsignedIntegerValue]; } - (UIViewController *)qmui_previousViewController { UIViewController *previousViewController = nil; NSArray *viewControllers = self.navigationController.viewControllers; NSUInteger index = [viewControllers indexOfObject:self]; if (index != NSNotFound && index > 0) { previousViewController = viewControllers[index - 1]; // 系统 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 previousViewController; } - (NSString *)qmui_previousViewControllerTitle { UIViewController *previousViewController = [self qmui_previousViewController]; if (previousViewController) { return previousViewController.title ?: previousViewController.navigationItem.title; } return nil; } - (BOOL)qmui_isPresented { UIViewController *viewController = self; if (self.navigationController) { if (self.navigationController.qmui_rootViewController != self) { return NO; } viewController = self.navigationController; } BOOL result = viewController.presentingViewController.presentedViewController == viewController; return result; } - (UIViewController *)qmui_visibleViewControllerIfExist { if (self.presentedViewController) { return [self.presentedViewController qmui_visibleViewControllerIfExist]; } if ([self isKindOfClass:[UINavigationController class]]) { return [((UINavigationController *)self).visibleViewController qmui_visibleViewControllerIfExist]; } if ([self isKindOfClass:[UITabBarController class]]) { return [((UITabBarController *)self).selectedViewController qmui_visibleViewControllerIfExist]; } if ([self qmui_isViewLoadedAndVisible]) { return self; } else { QMUILog(@"UIViewController (QMUI)", @"qmui_visibleViewControllerIfExist:,找不到可见的viewController。self = %@, self.view.window = %@", self, [self isViewLoaded] ? self.view.window : nil); return nil; } } - (BOOL)qmui_isViewLoadedAndVisible { return self.isViewLoaded && self.view.qmui_visible; } - (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 @implementation UIViewController (Runtime) - (BOOL)qmui_hasOverrideUIKitMethod:(SEL)selector { // 排序依照 Xcode Interface Builder 里的控件排序,但保证子类在父类前面 NSMutableArray *viewControllerSuperclasses = [[NSMutableArray alloc] initWithObjects: [UIImagePickerController class], [UINavigationController class], [UITableViewController class], [UICollectionViewController class], [UITabBarController class], [UISplitViewController class], [UIPageViewController class], [UIViewController class], nil]; if (NSClassFromString(@"UIAlertController")) { [viewControllerSuperclasses addObject:[UIAlertController class]]; } if (NSClassFromString(@"UISearchController")) { [viewControllerSuperclasses addObject:[UISearchController class]]; } for (NSInteger i = 0, l = viewControllerSuperclasses.count; i < l; i++) { Class superclass = viewControllerSuperclasses[i]; if ([self qmui_hasOverrideMethod:selector ofSuperclass:superclass]) { return YES; } } return NO; } @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 ================================================ FILE: QMUIKit/UIKitExtensions/UIVisualEffectView+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIVisualEffectView+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIWindow+QMUI.h ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 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 ================================================ FILE: QMUIKit/UIKitExtensions/UIWindow+QMUI.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 QMUI Team on 16/7/21. // #import "UIWindow+QMUI.h" #import "QMUICore.h" @implementation UIWindow (QMUI) QMUISynthesizeBOOLProperty(qmui_capturesStatusBarAppearance, setQmui_capturesStatusBarAppearance) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // -[UIWindow initWithFrame:] ExtendImplementationOfNonVoidMethodWithSingleArgument([UIWindow class], @selector(initWithFrame:), CGRect, UIWindow *, ^UIWindow *(UIWindow *selfObject, CGRect frame, UIWindow *originReturnValue) { selfObject.qmui_capturesStatusBarAppearance = YES; return originReturnValue; }); // -[UIWindow initWithWindowScene:] ExtendImplementationOfNonVoidMethodWithSingleArgument([UIWindow class], @selector(initWithWindowScene:), UIWindowScene *, UIWindow *, ^UIWindow *(UIWindow *selfObject, UIWindowScene *windowScene, UIWindow *originReturnValue) { selfObject.qmui_capturesStatusBarAppearance = YES; return originReturnValue; }); // -[UIWindow _canAffectStatusBarAppearance] OverrideImplementation([UIWindow class], NSSelectorFromString([NSString stringWithFormat:@"_%@%@%@", @"canAffect", @"StatusBar", @"Appearance"]), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^BOOL(UIWindow *selfObject) { if (selfObject.qmui_capturesStatusBarAppearance) { // call super BOOL (*originSelectorIMP)(id, SEL); originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); BOOL result = originSelectorIMP(selfObject, originCMD); return result; } return NO; }; }); }); } static char kAssociatedObjectKey_canBecomeKeyWindow; - (void)setQmui_canBecomeKeyWindow:(BOOL)qmui_canBecomeKeyWindow { [self qmuiw_hookIfNeeded]; objc_setAssociatedObject(self, &kAssociatedObjectKey_canBecomeKeyWindow, @(qmui_canBecomeKeyWindow), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)qmui_canBecomeKeyWindow { NSNumber *value = objc_getAssociatedObject(self, &kAssociatedObjectKey_canBecomeKeyWindow); if (!value) { return YES; } return value.boolValue; } static char kAssociatedObjectKey_canResignKeyWindowBlock; - (void)setQmui_canResignKeyWindowBlock:(BOOL (^)(UIWindow *, UIWindow *))qmui_canResignKeyWindowBlock { [self qmuiw_hookIfNeeded]; objc_setAssociatedObject(self, &kAssociatedObjectKey_canResignKeyWindowBlock, qmui_canResignKeyWindowBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (BOOL (^)(UIWindow *, UIWindow *))qmui_canResignKeyWindowBlock { return (BOOL (^)(UIWindow *, UIWindow *))objc_getAssociatedObject(self, &kAssociatedObjectKey_canResignKeyWindowBlock); } - (void)qmuiw_hookIfNeeded { [QMUIHelper executeBlock:^{ // - [UIWindow canBecomeKeyWindow] SEL sel1 = @selector(canBecomeKeyWindow); // - [UIWindow _canBecomeKeyWindow] SEL sel2 = NSSelectorFromString([NSString stringWithFormat:@"_%@", NSStringFromSelector(sel1)]); SEL sel = [self respondsToSelector:sel1] ? sel1 : ([self respondsToSelector:sel2] ? sel2 : nil); if (sel) { OverrideImplementation([UIWindow class], sel, ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^BOOL(UIWindow *selfObject) { // call super BOOL (*originSelectorIMP)(id, SEL); originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider(); BOOL result = originSelectorIMP(selfObject, originCMD); BOOL hasSet = !!objc_getAssociatedObject(selfObject, &kAssociatedObjectKey_canBecomeKeyWindow); if (hasSet) { result = selfObject.qmui_canBecomeKeyWindow; } 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 ================================================ FILE: QMUIKit.podspec ================================================ Pod::Spec.new do |s| s.name = "QMUIKit" s.version = "4.8.0" s.summary = "致力于提高项目 UI 开发效率的解决方案" s.description = <<-DESC QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设计目的是用于辅助快速搭建一个具备基本设计还原效果的 iOS 项目,同时利用自身提供的丰富控件及兼容处理, 让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。 DESC s.homepage = "https://github.com/Tencent/QMUI_iOS" s.license = 'MIT' s.author = {"qmuiteam" => "contact@qmuiteam.com"} s.source = {:git => "https://github.com/Tencent/QMUI_iOS.git", :tag => s.version.to_s} #s.source = {:git => "https://github.com/Tencent/QMUI_iOS.git", :branch => 'master'} s.social_media_url = 'https://github.com/Tencent/QMUI_iOS' s.requires_arc = true s.documentation_url = 'https://github.com/Tencent/QMUI_iOS' s.screenshot = 'https://cloud.githubusercontent.com/assets/1190261/26751376/63f96538-486a-11e7-81cf-5bc83a945207.png' s.platform = :ios, '13.0' s.frameworks = 'Foundation', 'UIKit', 'CoreGraphics' s.preserve_paths = 'QMUIConfigurationTemplate/*' s.source_files = 'QMUIKit/QMUIKit.h' s.resource_bundles = {'QMUIKit' => ['QMUIKit/PrivacyInfo.xcprivacy']} s.subspec 'QMUICore' do |ss| ss.source_files = 'QMUIKit/QMUIKit.h', 'QMUIKit/QMUICore', 'QMUIKit/UIKitExtensions', '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 ================================================ FILE: QMUIKitTests/Components/QMUIThemeTests.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKitTests/Core/QMUICommonDefinesTests.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKitTests/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleVersion 1 ================================================ FILE: QMUIKitTests/UIKitExtensions/NSObjectTests.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKitTests/UIKitExtensions/NSStringTests.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKitTests/UIKitExtensions/UIButtonTests.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: QMUIKitTests/UIKitExtensions/UIColorTests.m ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ================================================ FILE: README.md ================================================ # QMUI 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 配置 只需要修改一份配置表就可以调整 App 的全局样式,包括颜色、导航栏、输入框、列表等。一处修改,全局生效。 ### UIKit 拓展及版本兼容 拓展多个 UIKit 的组件,提供更加丰富的特性和功能,提高开发效率;解决不同 iOS 版本常见的兼容性问题。 ### 丰富的 UI 控件 提供丰富且常用的 UI 控件,使用方便灵活,并且支持自定义控件的样式。 ### 高效的工具方法及宏 提供高效的工具方法,包括设备信息、动态字体、键盘管理、状态栏管理等,可以解决各种常见场景并大幅度提升开发效率。 ## 支持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+。 ## 使用方法 ``` 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) ## 注意事项 - 关于 AutoLayout:通常可以配合 Masonry 等常见的 AutoLayout 框架使用,若遇到不兼容的个案请提 issue。 - 关于 xib / storyboard:现已全面支持。 - 关于 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) ================================================ FILE: add_license.py ================================================ # -*- 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 = [] ================================================ FILE: new_license_content.txt ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ ================================================ FILE: old_license_content.txt ================================================ /** * Tencent is pleased to support the open source community by making QMUI_iOS available. * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at * http://opensource.org/licenses/MIT * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ ================================================ FILE: qmui.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 56; objects = { /* Begin PBXBuildFile section */ 08230CEC233D285B00BF9CB1 /* UISearchController+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 08230CEA233D285B00BF9CB1 /* UISearchController+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; 08230CED233D285B00BF9CB1 /* UISearchController+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 08230CEB233D285B00BF9CB1 /* UISearchController+QMUI.m */; }; 083551A92438C0D000B8FEAB /* CALayer+QMUIViewAnimation.h in Headers */ = {isa = PBXBuildFile; fileRef = 083551A72438C0D000B8FEAB /* CALayer+QMUIViewAnimation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 083551AA2438C0D000B8FEAB /* CALayer+QMUIViewAnimation.m in Sources */ = {isa = PBXBuildFile; fileRef = 083551A82438C0D000B8FEAB /* CALayer+QMUIViewAnimation.m */; }; 08B399C922E18A3B000A8A45 /* UITraitCollection+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 08B399C722E18A3B000A8A45 /* UITraitCollection+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; 08B399CA22E18A3B000A8A45 /* UITraitCollection+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 08B399C822E18A3B000A8A45 /* UITraitCollection+QMUI.m */; }; 1178D5692198258700AA30E5 /* NSURL+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 1178D5672198258700AA30E5 /* NSURL+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1178D56A2198258700AA30E5 /* NSURL+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 1178D5682198258700AA30E5 /* NSURL+QMUI.m */; }; 3CB960C42BB40725005626A6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3CB960C32BB40725005626A6 /* PrivacyInfo.xcprivacy */; }; AA8860BA2107455C005E4054 /* QMUIWeakObjectContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = AA8860B82107455C005E4054 /* QMUIWeakObjectContainer.h */; settings = {ATTRIBUTES = (Public, ); }; }; AA8860BB2107455C005E4054 /* QMUIWeakObjectContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = AA8860B92107455C005E4054 /* QMUIWeakObjectContainer.m */; }; CD046C412018668900092035 /* QMUILogItem.h in Headers */ = {isa = PBXBuildFile; fileRef = CD046C3F2018668900092035 /* QMUILogItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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, ); }; }; CD84F31F1E52DBEA00546111 /* UITabBar+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD84F31C1E52DBEA00546111 /* UITabBar+QMUI.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, ); }; }; CDB8CB511DCC870700769DF0 /* CALayer+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA761DCC870700769DF0 /* CALayer+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDB8CB541DCC870700769DF0 /* CALayer+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA771DCC870700769DF0 /* CALayer+QMUI.m */; }; CDB8CB551DCC870700769DF0 /* NSAttributedString+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA781DCC870700769DF0 /* NSAttributedString+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; CDB8CB641DCC870700769DF0 /* NSString+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CA7F1DCC870700769DF0 /* NSString+QMUI.m */; }; CDB8CB991DCC870700769DF0 /* UIActivityIndicatorView+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CA9A1DCC870700769DF0 /* UIActivityIndicatorView+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; CDB8CBC01DCC870800769DF0 /* UILabel+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CAAD1DCC870700769DF0 /* UILabel+QMUI.m */; }; CDB8CBC51DCC870800769DF0 /* UINavigationController+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CAB01DCC870700769DF0 /* UINavigationController+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; 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, ); }; }; CDB8CBE01DCC870800769DF0 /* UIViewController+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CABD1DCC870700769DF0 /* UIViewController+QMUI.m */; }; CDB8CBE11DCC870800769DF0 /* UIWindow+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CDB8CABE1DCC870700769DF0 /* UIWindow+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDB8CBE41DCC870800769DF0 /* UIWindow+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CDB8CABF1DCC870700769DF0 /* UIWindow+QMUI.m */; }; CDC006E522A804D800A81771 /* NSObjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC006E422A804D800A81771 /* NSObjectTests.m */; }; CDC163C6204D441000E4CC13 /* QMUILogManagerViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC163C4204D441000E4CC13 /* QMUILogManagerViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDC163C7204D441000E4CC13 /* QMUILogManagerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CDC163C5204D441000E4CC13 /* QMUILogManagerViewController.m */; }; CDC86FBD1F68D617000E8829 /* QMUIAsset.h in Headers */ = {isa = PBXBuildFile; fileRef = CDC86F411F68D5F9000E8829 /* QMUIAsset.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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 */; }; FE1FBCAF1E8BA79000C6C01A /* UITextView+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = FE1FBCAD1E8BA79000C6C01A /* UITextView+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; FE1FBCB11E8BA79000C6C01A /* UITextView+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = FE1FBCAE1E8BA79000C6C01A /* UITextView+QMUI.m */; }; FE8710FD22E499EC00DF1354 /* UIMenuController+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = FE8710FB22E499EB00DF1354 /* UIMenuController+QMUI.m */; }; FE8710FE22E499EC00DF1354 /* UIMenuController+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = FE8710FC22E499EC00DF1354 /* UIMenuController+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; FECD352322BBC3BB00DC69DE /* QMUIAnimationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = FECD351D22BBC3BB00DC69DE /* QMUIAnimationHelper.m */; }; FECD352422BBC3BB00DC69DE /* QMUIEasings.h in Headers */ = {isa = PBXBuildFile; fileRef = FECD351E22BBC3BB00DC69DE /* QMUIEasings.h */; settings = {ATTRIBUTES = (Public, ); }; }; FECD352522BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.h in Headers */ = {isa = PBXBuildFile; fileRef = FECD351F22BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.h */; settings = {ATTRIBUTES = (Public, ); }; }; FECD352622BBC3BB00DC69DE /* QMUIAnimationHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = FECD352022BBC3BB00DC69DE /* QMUIAnimationHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; FECD352722BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.m in Sources */ = {isa = PBXBuildFile; fileRef = FECD352122BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.m */; }; FECD352B22BBC93500DC69DE /* QMUIWindowSizeMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = FECD352922BBC93400DC69DE /* QMUIWindowSizeMonitor.m */; }; FECD352C22BBC93500DC69DE /* QMUIWindowSizeMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = FECD352A22BBC93500DC69DE /* QMUIWindowSizeMonitor.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ CD4EA577228C401E00A55066 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = CD44C1BD1956D5970098D0A2 /* Project object */; proxyType = 1; remoteGlobalIDString = FE0AFAD01D82B9D8000D21D9; remoteInfo = QMUIKit; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 08230CEA233D285B00BF9CB1 /* UISearchController+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UISearchController+QMUI.h"; sourceTree = ""; }; 08230CEB233D285B00BF9CB1 /* UISearchController+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UISearchController+QMUI.m"; sourceTree = ""; }; 083551A72438C0D000B8FEAB /* CALayer+QMUIViewAnimation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CALayer+QMUIViewAnimation.h"; sourceTree = ""; }; 083551A82438C0D000B8FEAB /* CALayer+QMUIViewAnimation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CALayer+QMUIViewAnimation.m"; sourceTree = ""; }; 089F1E4A2322F6D50063061E /* UIView+QMUITheme.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+QMUITheme.h"; sourceTree = ""; }; 08B399C722E18A3B000A8A45 /* UITraitCollection+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITraitCollection+QMUI.h"; sourceTree = ""; }; 08B399C822E18A3B000A8A45 /* UITraitCollection+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITraitCollection+QMUI.m"; sourceTree = ""; }; 1178D5672198258700AA30E5 /* NSURL+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSURL+QMUI.h"; sourceTree = ""; }; 1178D5682198258700AA30E5 /* NSURL+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSURL+QMUI.m"; sourceTree = ""; }; 3CB960C32BB40725005626A6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 6D03A56D1B53895D003BDDE4 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; AA8860B82107455C005E4054 /* QMUIWeakObjectContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIWeakObjectContainer.h; sourceTree = ""; }; AA8860B92107455C005E4054 /* QMUIWeakObjectContainer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIWeakObjectContainer.m; sourceTree = ""; }; 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; }; CD44C1E11956D5970098D0A2 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; CD4DA9C01E8E3B0500836A1A /* QMUIConfigurationTemplate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIConfigurationTemplate.h; sourceTree = ""; }; CD4DA9C11E8E3B0500836A1A /* QMUIConfigurationTemplate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIConfigurationTemplate.m; sourceTree = ""; }; CD4EA4BD2275FA0100A55066 /* NSMethodSignature+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMethodSignature+QMUI.h"; sourceTree = ""; }; CD4EA4BE2275FA0100A55066 /* NSMethodSignature+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMethodSignature+QMUI.m"; sourceTree = ""; }; CD4EA571228C401E00A55066 /* QMUIKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QMUIKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; CD4EA575228C401E00A55066 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CD4EA57D228C443B00A55066 /* UIColorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIColorTests.m; sourceTree = ""; }; CD513E25283527AA004A549D /* QMUIBarProtocolPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIBarProtocolPrivate.h; sourceTree = ""; }; CD513E26283527AA004A549D /* QMUIBarProtocolPrivate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIBarProtocolPrivate.m; sourceTree = ""; }; CD513E27283527AA004A549D /* QMUIBarProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIBarProtocol.h; sourceTree = ""; }; CD513E2B283527CE004A549D /* UITabBar+QMUIBarProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITabBar+QMUIBarProtocol.h"; sourceTree = ""; }; CD513E2C283527CE004A549D /* UITabBar+QMUIBarProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITabBar+QMUIBarProtocol.m"; sourceTree = ""; }; CD513E2F283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UINavigationBar+QMUIBarProtocol.h"; sourceTree = ""; }; CD513E30283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UINavigationBar+QMUIBarProtocol.m"; sourceTree = ""; }; CD5E431F2B85F71F0030CFDA /* NSRegularExpression+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSRegularExpression+QMUI.h"; sourceTree = ""; }; CD5E43202B85F71F0030CFDA /* NSRegularExpression+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSRegularExpression+QMUI.m"; sourceTree = ""; }; CD60DB4F2C5BC5D1005109B3 /* QMUICheckbox.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUICheckbox.h; sourceTree = ""; }; CD60DB502C5BC5D1005109B3 /* QMUICheckbox.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUICheckbox.m; sourceTree = ""; }; CD6631D91FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUITableViewHeaderFooterView.h; sourceTree = ""; }; CD6631DA1FD929F4004DF7E8 /* QMUITableViewHeaderFooterView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUITableViewHeaderFooterView.m; sourceTree = ""; }; CD669A0B25F79DA40036D6B2 /* UICollectionViewCell+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UICollectionViewCell+QMUI.h"; sourceTree = ""; }; CD669A0C25F79DA40036D6B2 /* UICollectionViewCell+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UICollectionViewCell+QMUI.m"; sourceTree = ""; }; CD6BE14C2058C64E00BE093E /* QMUICellHeightKeyCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUICellHeightKeyCache.h; sourceTree = ""; }; CD6BE14D2058C64E00BE093E /* QMUICellHeightKeyCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUICellHeightKeyCache.m; sourceTree = ""; }; CD6BE1542058C73600BE093E /* UITableView+QMUICellHeightKeyCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITableView+QMUICellHeightKeyCache.h"; sourceTree = ""; }; CD6BE1552058C73600BE093E /* UITableView+QMUICellHeightKeyCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITableView+QMUICellHeightKeyCache.m"; sourceTree = ""; }; CD70C438276340B300D212F5 /* UISlider+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UISlider+QMUI.h"; sourceTree = ""; }; CD70C439276340B300D212F5 /* UISlider+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UISlider+QMUI.m"; sourceTree = ""; }; CD72E7BF2B440DF000AC528A /* QMUILayouterItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILayouterItem.h; sourceTree = ""; }; CD72E7C02B440DF000AC528A /* QMUILayouterItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUILayouterItem.m; sourceTree = ""; }; CD72E7C52B44AF2F00AC528A /* QMUILayouterLinearHorizontal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILayouterLinearHorizontal.h; sourceTree = ""; }; CD72E7C62B44AF2F00AC528A /* QMUILayouterLinearHorizontal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUILayouterLinearHorizontal.m; sourceTree = ""; }; CD72E7C92B44AF8800AC528A /* QMUILayouterLinearVertical.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUILayouterLinearVertical.h; sourceTree = ""; }; CD72E7CA2B44AF8800AC528A /* QMUILayouterLinearVertical.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUILayouterLinearVertical.m; sourceTree = ""; }; CD745E2821CA5B8E006EC132 /* QMUIImagePreviewView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIImagePreviewView.h; sourceTree = ""; }; CD745E2921CA5B8E006EC132 /* QMUIImagePreviewViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIImagePreviewViewController.m; sourceTree = ""; }; CD745E2A21CA5B8E006EC132 /* QMUIImagePreviewViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIImagePreviewViewController.h; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; 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 = ""; }; CDB8CA791DCC870700769DF0 /* NSAttributedString+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributedString+QMUI.m"; sourceTree = ""; }; CDB8CA7A1DCC870700769DF0 /* NSObject+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+QMUI.h"; sourceTree = ""; }; CDB8CA7B1DCC870700769DF0 /* NSObject+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+QMUI.m"; sourceTree = ""; }; CDB8CA7C1DCC870700769DF0 /* NSParagraphStyle+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSParagraphStyle+QMUI.h"; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; CDB8CA9D1DCC870700769DF0 /* UIBezierPath+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIBezierPath+QMUI.m"; sourceTree = ""; }; CDB8CA9E1DCC870700769DF0 /* UIButton+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIButton+QMUI.h"; sourceTree = ""; }; CDB8CA9F1DCC870700769DF0 /* UIButton+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIButton+QMUI.m"; sourceTree = ""; }; CDB8CAA01DCC870700769DF0 /* UICollectionView+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionView+QMUI.h"; sourceTree = ""; }; CDB8CAA11DCC870700769DF0 /* UICollectionView+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UICollectionView+QMUI.m"; sourceTree = ""; }; CDB8CAA21DCC870700769DF0 /* UIColor+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+QMUI.h"; sourceTree = ""; }; CDB8CAA31DCC870700769DF0 /* UIColor+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+QMUI.m"; sourceTree = ""; }; CDB8CAA41DCC870700769DF0 /* UIControl+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIControl+QMUI.h"; sourceTree = ""; }; CDB8CAA51DCC870700769DF0 /* UIControl+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIControl+QMUI.m"; sourceTree = ""; }; CDB8CAA61DCC870700769DF0 /* UIFont+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIFont+QMUI.h"; sourceTree = ""; }; CDB8CAA71DCC870700769DF0 /* UIFont+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIFont+QMUI.m"; sourceTree = ""; }; CDB8CAA81DCC870700769DF0 /* UIImage+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+QMUI.h"; sourceTree = ""; }; CDB8CAA91DCC870700769DF0 /* UIImage+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+QMUI.m"; sourceTree = ""; }; CDB8CAAA1DCC870700769DF0 /* UIImageView+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImageView+QMUI.h"; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; CDB8CAB31DCC870700769DF0 /* UIScrollView+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+QMUI.m"; sourceTree = ""; }; CDB8CAB41DCC870700769DF0 /* UISearchBar+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UISearchBar+QMUI.h"; sourceTree = ""; }; CDB8CAB51DCC870700769DF0 /* UISearchBar+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UISearchBar+QMUI.m"; sourceTree = ""; }; CDB8CAB61DCC870700769DF0 /* UITabBarItem+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITabBarItem+QMUI.h"; sourceTree = ""; }; CDB8CAB71DCC870700769DF0 /* UITabBarItem+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UITabBarItem+QMUI.m"; sourceTree = ""; }; CDB8CAB81DCC870700769DF0 /* UITableView+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UITableView+QMUI.h"; sourceTree = ""; }; CDB8CAB91DCC870700769DF0 /* UITableView+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UITableView+QMUI.m"; sourceTree = ""; }; CDB8CABA1DCC870700769DF0 /* UIView+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+QMUI.h"; sourceTree = ""; }; CDB8CABB1DCC870700769DF0 /* UIView+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+QMUI.m"; sourceTree = ""; }; CDB8CABC1DCC870700769DF0 /* UIViewController+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+QMUI.h"; sourceTree = ""; }; CDB8CABD1DCC870700769DF0 /* UIViewController+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+QMUI.m"; sourceTree = ""; }; CDB8CABE1DCC870700769DF0 /* UIWindow+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIWindow+QMUI.h"; sourceTree = ""; }; CDB8CABF1DCC870700769DF0 /* UIWindow+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIWindow+QMUI.m"; sourceTree = ""; }; CDC006E422A804D800A81771 /* NSObjectTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSObjectTests.m; sourceTree = ""; }; CDC163C4204D441000E4CC13 /* QMUILogManagerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUILogManagerViewController.h; sourceTree = ""; }; CDC163C5204D441000E4CC13 /* QMUILogManagerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUILogManagerViewController.m; sourceTree = ""; }; 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 = ""; }; FE8710FB22E499EB00DF1354 /* UIMenuController+QMUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIMenuController+QMUI.m"; sourceTree = ""; }; FE8710FC22E499EC00DF1354 /* UIMenuController+QMUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIMenuController+QMUI.h"; sourceTree = ""; }; FECD351D22BBC3BB00DC69DE /* QMUIAnimationHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIAnimationHelper.m; sourceTree = ""; }; FECD351E22BBC3BB00DC69DE /* QMUIEasings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIEasings.h; sourceTree = ""; }; FECD351F22BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIDisplayLinkAnimation.h; sourceTree = ""; }; FECD352022BBC3BB00DC69DE /* QMUIAnimationHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIAnimationHelper.h; sourceTree = ""; }; FECD352122BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIDisplayLinkAnimation.m; sourceTree = ""; }; FECD352922BBC93400DC69DE /* QMUIWindowSizeMonitor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIWindowSizeMonitor.m; sourceTree = ""; }; FECD352A22BBC93500DC69DE /* QMUIWindowSizeMonitor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QMUIWindowSizeMonitor.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ CD4EA56E228C401E00A55066 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CD4EA576228C401E00A55066 /* QMUIKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; FE0AFACD1D82B9D8000D21D9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CDFF5FB62369926300B63B92 /* Photos.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 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 = ( CD43CB15207B98A10090346B /* QMUIButton.h */, CD43CB16207B98A10090346B /* QMUIButton.m */, CD43CB19207B98B60090346B /* QMUINavigationButton.h */, CD43CB1A207B98B60090346B /* QMUINavigationButton.m */, CD43CB1D207B9A510090346B /* QMUIToolbarButton.h */, CD43CB1E207B9A510090346B /* QMUIToolbarButton.m */, ); path = QMUIButton; sourceTree = ""; }; CD44C1BC1956D5970098D0A2 = { isa = PBXGroup; children = ( CD4EA572228C401E00A55066 /* QMUIKitTests */, CD44C1C71956D5970098D0A2 /* Frameworks */, CD44C1C61956D5970098D0A2 /* Products */, CD4DA9BF1E8E3B0500836A1A /* QMUIConfigurationTemplate */, CDB8CA2D1DCC870700769DF0 /* QMUIKit */, ); sourceTree = ""; }; CD44C1C61956D5970098D0A2 /* Products */ = { isa = PBXGroup; children = ( FE0AFAD11D82B9D8000D21D9 /* QMUIKit.framework */, CD4EA571228C401E00A55066 /* QMUIKitTests.xctest */, ); name = Products; sourceTree = ""; }; CD44C1C71956D5970098D0A2 /* Frameworks */ = { isa = PBXGroup; children = ( 6D03A56D1B53895D003BDDE4 /* Photos.framework */, CDFF5FB52369926300B63B92 /* Photos.framework */, CD44C1C81956D5970098D0A2 /* Foundation.framework */, CD44C1CA1956D5970098D0A2 /* CoreGraphics.framework */, CD44C1CC1956D5970098D0A2 /* UIKit.framework */, CD44C1E11956D5970098D0A2 /* XCTest.framework */, ); name = Frameworks; sourceTree = ""; }; CD4DA9BF1E8E3B0500836A1A /* QMUIConfigurationTemplate */ = { isa = PBXGroup; children = ( CD4DA9C01E8E3B0500836A1A /* QMUIConfigurationTemplate.h */, CD4DA9C11E8E3B0500836A1A /* QMUIConfigurationTemplate.m */, ); path = QMUIConfigurationTemplate; sourceTree = ""; }; CD4EA572228C401E00A55066 /* QMUIKitTests */ = { isa = PBXGroup; children = ( D0F0C7C0246A91EF00927A1A /* Core */, CD7A9A0B22C4AA2F0093DAB4 /* Components */, CD4EA57C228C43FB00A55066 /* UIKitExtensions */, CD4EA575228C401E00A55066 /* Info.plist */, ); path = QMUIKitTests; sourceTree = ""; }; CD4EA57C228C43FB00A55066 /* UIKitExtensions */ = { isa = PBXGroup; children = ( CDC006E422A804D800A81771 /* NSObjectTests.m */, D0ECA053261513230067BCC6 /* NSStringTests.m */, CD4EA57D228C443B00A55066 /* UIColorTests.m */, D00881752677B5870061CABF /* UIButtonTests.m */, ); path = UIKitExtensions; sourceTree = ""; }; CD513E24283527AA004A549D /* QMUIBarProtocol */ = { isa = PBXGroup; children = ( CD513E27283527AA004A549D /* QMUIBarProtocol.h */, CD513E25283527AA004A549D /* QMUIBarProtocolPrivate.h */, CD513E26283527AA004A549D /* QMUIBarProtocolPrivate.m */, CD513E2F283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h */, CD513E30283527DB004A549D /* UINavigationBar+QMUIBarProtocol.m */, CD513E2B283527CE004A549D /* UITabBar+QMUIBarProtocol.h */, CD513E2C283527CE004A549D /* UITabBar+QMUIBarProtocol.m */, ); path = QMUIBarProtocol; sourceTree = ""; }; CD6BE1472058C61000BE093E /* QMUICellHeightKeyCache */ = { isa = PBXGroup; children = ( CD6BE14C2058C64E00BE093E /* QMUICellHeightKeyCache.h */, CD6BE14D2058C64E00BE093E /* QMUICellHeightKeyCache.m */, CD6BE1542058C73600BE093E /* UITableView+QMUICellHeightKeyCache.h */, CD6BE1552058C73600BE093E /* UITableView+QMUICellHeightKeyCache.m */, ); path = QMUICellHeightKeyCache; sourceTree = ""; }; CD745E2721CA5B8E006EC132 /* QMUIImagePreviewView */ = { isa = PBXGroup; children = ( CD745E2821CA5B8E006EC132 /* QMUIImagePreviewView.h */, CD745E2B21CA5B8E006EC132 /* QMUIImagePreviewView.m */, CD745E2A21CA5B8E006EC132 /* QMUIImagePreviewViewController.h */, CD745E2921CA5B8E006EC132 /* QMUIImagePreviewViewController.m */, CD745E3021CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.h */, CD745E3121CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.m */, ); path = QMUIImagePreviewView; sourceTree = ""; }; CD7A9A0B22C4AA2F0093DAB4 /* Components */ = { isa = PBXGroup; children = ( CD7A9A0C22C4AA2F0093DAB4 /* QMUIThemeTests.m */, ); path = Components; sourceTree = ""; }; CD82C0A42069EB850046EED2 /* QMUIMultipleDelegates */ = { isa = PBXGroup; children = ( CD82C0B1206A82520046EED2 /* NSObject+QMUIMultipleDelegates.h */, CD82C0B2206A82520046EED2 /* NSObject+QMUIMultipleDelegates.m */, CD82C0AD206A2C3D0046EED2 /* QMUIMultipleDelegates.h */, CD82C0AE206A2C3D0046EED2 /* QMUIMultipleDelegates.m */, ); 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 */ = { isa = PBXGroup; 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 */, 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 */, 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 = ""; }; 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 = ( D0F0C7C1246A926600927A1A /* QMUICommonDefinesTests.m */, ); path = Core; sourceTree = ""; }; FECD351C22BBC3BB00DC69DE /* QMUIAnimation */ = { isa = PBXGroup; children = ( FECD352022BBC3BB00DC69DE /* QMUIAnimationHelper.h */, FECD351D22BBC3BB00DC69DE /* QMUIAnimationHelper.m */, FECD351F22BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.h */, FECD352122BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.m */, FECD351E22BBC3BB00DC69DE /* QMUIEasings.h */, ); path = QMUIAnimation; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ FE0AFACE1D82B9D8000D21D9 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( CD60DB512C5BC5D1005109B3 /* QMUICheckbox.h in Headers */, CD40021E2C1F6E1C003D2127 /* QMUIPopupMenuItemViewProtocol.h in Headers */, CD4002222C1F81CE003D2127 /* QMUIPopupMenuItemView.h in Headers */, CD40021C2C1F6BB0003D2127 /* QMUIPopupMenuItem.h in Headers */, CD81252A2A69CB5900132EC7 /* NSDictionary+QMUI.h in Headers */, CD96A2B928C74CCA00E87728 /* NSShadow+QMUI.h in Headers */, CD513E2D283527CE004A549D /* UITabBar+QMUIBarProtocol.h in Headers */, CD513E31283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h in Headers */, CD513E2A283527AA004A549D /* QMUIBarProtocol.h in Headers */, CD70C43A276340B300D212F5 /* UISlider+QMUI.h in Headers */, CDE77517274FB9430066A767 /* UIBlurEffect+QMUI.h in Headers */, CDE77513274E93CE0066A767 /* UIToolbar+QMUI.h in Headers */, D02096B226DD2B180029BA78 /* UIApplication+QMUI.h in Headers */, CD669A0D25F79DA40036D6B2 /* UICollectionViewCell+QMUI.h in Headers */, D033BC0F2549A32D00674526 /* UINavigationItem+QMUI.h in Headers */, D09D4BDB24BF1561002D29FF /* UIVisualEffectView+QMUI.h in Headers */, D03102B524A8CB410095C232 /* UIView+QMUIBorder.h in Headers */, D032060E2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.h in Headers */, D0BEFA97247D42510006D1B9 /* UIView+QMUIBadge.h in Headers */, D0BEFA9A247D427A0006D1B9 /* QMUIBadgeProtocol.h in Headers */, 083551A92438C0D000B8FEAB /* CALayer+QMUIViewAnimation.h in Headers */, D00B6521242A67D7002C27AB /* QMUIAppearance.h in Headers */, CDAB2D262357481700C96B31 /* UITextInputTraits+QMUI.h in Headers */, 08230CEC233D285B00BF9CB1 /* UISearchController+QMUI.h in Headers */, CD0BD676233B9888005E47CE /* UIView+QMUITheme.h in Headers */, D0193BE822E3449D00FB76F5 /* UIVisualEffect+QMUITheme.h in Headers */, 08B399C922E18A3B000A8A45 /* UITraitCollection+QMUI.h in Headers */, CD8CB8C222DE10F200B0C9F8 /* UIImage+QMUITheme.h in Headers */, D02FDB6E22D880F800DB7E13 /* UISwitch+QMUI.h in Headers */, D031843B22C287EA00B43520 /* UIViewController+QMUITheme.h in Headers */, CDD759A922BBE68900BC8F36 /* CAAnimation+QMUI.h in Headers */, CDD7599D22BBE11200BC8F36 /* QMUITheme.h in Headers */, CDAA653622BBC1240004C6BB /* UIColor+QMUITheme.h in Headers */, CDAA653A22BBC3340004C6BB /* QMUIThemeManager.h in Headers */, CD4EA4BF2275FA0100A55066 /* NSMethodSignature+QMUI.h in Headers */, FECD352C22BBC93500DC69DE /* QMUIWindowSizeMonitor.h in Headers */, CD8AA7B321E8C0F300BA7369 /* QMUIConsoleViewController.h in Headers */, CD8AA7C221EDE06800BA7369 /* QMUILog+QMUIConsole.h in Headers */, CD046C452018670900092035 /* QMUILogNameManager.h in Headers */, CD046C412018668900092035 /* QMUILogItem.h in Headers */, CD046C492018688F00092035 /* QMUILogger.h in Headers */, CD5E43212B85F7200030CFDA /* NSRegularExpression+QMUI.h in Headers */, CD8AA7AF21E8BF0B00BA7369 /* QMUIConsoleToolbar.h in Headers */, CD8AA7AB21E8B9D600BA7369 /* QMUIConsole.h in Headers */, CD19F4D821E4AB3900BD4687 /* QMUILab.h in Headers */, 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 */, CDB8CB9D1DCC870700769DF0 /* UIBezierPath+QMUI.h in Headers */, CDB8CB591DCC870700769DF0 /* NSObject+QMUI.h in Headers */, CDB8CBA91DCC870700769DF0 /* UIColor+QMUI.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 */, CDB8CB5D1DCC870700769DF0 /* NSParagraphStyle+QMUI.h in Headers */, CDB8CB991DCC870700769DF0 /* UIActivityIndicatorView+QMUI.h in Headers */, CDB8CBA51DCC870700769DF0 /* UICollectionView+QMUI.h in Headers */, CDB8CBAD1DCC870800769DF0 /* UIControl+QMUI.h in Headers */, CDB8CBD51DCC870800769DF0 /* UITableView+QMUI.h in Headers */, CDB8CBDD1DCC870800769DF0 /* UIViewController+QMUI.h in Headers */, FECD352622BBC3BB00DC69DE /* QMUIAnimationHelper.h in Headers */, FECD352422BBC3BB00DC69DE /* QMUIEasings.h in Headers */, FECD352522BBC3BB00DC69DE /* QMUIDisplayLinkAnimation.h in Headers */, FE8710FE22E499EC00DF1354 /* UIMenuController+QMUI.h in Headers */, CD7D402F231FA2900007DF6C /* QMUIThemeManagerCenter.h in Headers */, CDB8CBB11DCC870800769DF0 /* UIFont+QMUI.h in Headers */, AA8860BA2107455C005E4054 /* QMUIWeakObjectContainer.h in Headers */, CDC86FC51F68D617000E8829 /* UINavigationBar+Transition.h in Headers */, CDB8CACF1DCC870700769DF0 /* QMUIKit.h in Headers */, CD9F48AA22C3985200F5C5C2 /* QMUIThemePrivate.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ CD4EA570228C401E00A55066 /* QMUIKitTests */ = { isa = PBXNativeTarget; buildConfigurationList = CD4EA57B228C401E00A55066 /* Build configuration list for PBXNativeTarget "QMUIKitTests" */; buildPhases = ( CD4EA56D228C401E00A55066 /* Sources */, CD4EA56E228C401E00A55066 /* Frameworks */, CD4EA56F228C401E00A55066 /* Resources */, ); buildRules = ( ); dependencies = ( CD4EA578228C401E00A55066 /* PBXTargetDependency */, ); name = QMUIKitTests; productName = QMUIKitTests; productReference = CD4EA571228C401E00A55066 /* QMUIKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; FE0AFAD01D82B9D8000D21D9 /* QMUIKit */ = { isa = PBXNativeTarget; buildConfigurationList = FE0AFAE81D82B9D8000D21D9 /* Build configuration list for PBXNativeTarget "QMUIKit" */; buildPhases = ( FE0AFACE1D82B9D8000D21D9 /* Headers */, FE0AFACC1D82B9D8000D21D9 /* Sources */, FE0AFACD1D82B9D8000D21D9 /* Frameworks */, FE0AFACF1D82B9D8000D21D9 /* Resources */, CDE38A6F1F70F745001ACF2C /* Create Umbrella Header File */, ); buildRules = ( ); dependencies = ( ); name = QMUIKit; productName = QMUIKit; productReference = FE0AFAD11D82B9D8000D21D9 /* QMUIKit.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ CD44C1BD1956D5970098D0A2 /* Project object */ = { isa = PBXProject; attributes = { CLASSPREFIX = QMUI; LastUpgradeCheck = 0940; ORGANIZATIONNAME = "QMUI Team"; TargetAttributes = { CD4EA570228C401E00A55066 = { CreatedOnToolsVersion = 10.2.1; }; FE0AFAD01D82B9D8000D21D9 = { CreatedOnToolsVersion = 8.0; DevelopmentTeam = XKMGNG3AWZ; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = CD44C1C01956D5970098D0A2 /* Build configuration list for PBXProject "qmui" */; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, "zh-Hant", "zh-Hans", ); mainGroup = CD44C1BC1956D5970098D0A2; productRefGroup = CD44C1C61956D5970098D0A2 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( FE0AFAD01D82B9D8000D21D9 /* QMUIKit */, CD4EA570228C401E00A55066 /* QMUIKitTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ CD4EA56F228C401E00A55066 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; FE0AFACF1D82B9D8000D21D9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 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 */ CD4EA56D228C401E00A55066 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( D0ECA054261513230067BCC6 /* NSStringTests.m in Sources */, D00881762677B5870061CABF /* UIButtonTests.m in Sources */, CD4EA57E228C443B00A55066 /* UIColorTests.m in Sources */, D0F0C7C2246A926600927A1A /* QMUICommonDefinesTests.m in Sources */, CDC006E522A804D800A81771 /* NSObjectTests.m in Sources */, CD7A9A0D22C4AA2F0093DAB4 /* QMUIThemeTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; FE0AFACC1D82B9D8000D21D9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 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 */, CDB8CBB81DCC870800769DF0 /* UIImage+QMUI.m in Sources */, CD81252B2A69CB5900132EC7 /* NSDictionary+QMUI.m in Sources */, CD72E7C82B44AF2F00AC528A /* QMUILayouterLinearHorizontal.m in Sources */, CDB8CB581DCC870700769DF0 /* NSAttributedString+QMUI.m in Sources */, CDB8CB5C1DCC870700769DF0 /* NSObject+QMUI.m in Sources */, CDB8CBA41DCC870700769DF0 /* UIButton+QMUI.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 */, 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 */, CDB8CBA81DCC870700769DF0 /* UICollectionView+QMUI.m in Sources */, CD979996213F934700C00FDC /* QMUIRuntime.m in Sources */, CD70C43B276340B300D212F5 /* UISlider+QMUI.m in Sources */, CDB8CBD41DCC870800769DF0 /* UITabBarItem+QMUI.m in Sources */, CD82C0B4206A82520046EED2 /* NSObject+QMUIMultipleDelegates.m in Sources */, CDB8CBC01DCC870800769DF0 /* UILabel+QMUI.m in Sources */, CDEA6D0A1F4B07E700F627AF /* UIGestureRecognizer+QMUI.m in Sources */, CDB8CBB41DCC870800769DF0 /* UIFont+QMUI.m in Sources */, CDB8CBD01DCC870800769DF0 /* UISearchBar+QMUI.m in Sources */, CD4002212C1F81CE003D2127 /* QMUIPopupMenuItemView.m in Sources */, CD84F31F1E52DBEA00546111 /* UITabBar+QMUI.m in Sources */, FE1FBCB11E8BA79000C6C01A /* UITextView+QMUI.m in Sources */, CD745E3321CA5BBB006EC132 /* QMUIImagePreviewViewTransitionAnimator.m in Sources */, CDB8CBAC1DCC870800769DF0 /* UIColor+QMUI.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 */, D032060F2488E38900BB28E7 /* UITableViewHeaderFooterView+QMUI.m in Sources */, D021DE38205E809500FFA408 /* UICollectionView+QMUICellSizeKeyCache.m in Sources */, CDB8CBE01DCC870800769DF0 /* UIViewController+QMUI.m in Sources */, FE1FBCAA1E8BA61300C6C01A /* UITextField+QMUI.m in Sources */, D02FDB6F22D880F800DB7E13 /* UISwitch+QMUI.m in Sources */, CDD071FE2060F82700343AB6 /* QMUICellHeightCache.m in Sources */, 1178D56A2198258700AA30E5 /* NSURL+QMUI.m in Sources */, D021DE3C205E80EB00FFA408 /* QMUICellSizeKeyCache.m in Sources */, CD8AA7AC21E8B9D600BA7369 /* QMUIConsole.m in Sources */, CDB8CB541DCC870700769DF0 /* CALayer+QMUI.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 PBXTargetDependency section */ CD4EA578228C401E00A55066 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = FE0AFAD01D82B9D8000D21D9 /* QMUIKit */; targetProxy = CD4EA577228C401E00A55066 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 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; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "DEBUG=1", "IS_QMUI_PROJECT=1", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; 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 = 13.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; name = Debug; }; CD44C1F01956D5970098D0A2 /* Release */ = { 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; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = "IS_QMUI_PROJECT=1"; 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 = 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 = { CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; 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 = 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)"; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; WARNING_CFLAGS = ""; }; name = Debug; }; FE0AFAEA1D82B9D8000D21D9 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NONNULL = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; 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 = 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)"; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; WARNING_CFLAGS = ""; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ CD44C1C01956D5970098D0A2 /* Build configuration list for PBXProject "qmui" */ = { isa = XCConfigurationList; buildConfigurations = ( CD44C1EF1956D5970098D0A2 /* Debug */, CD44C1F01956D5970098D0A2 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CD4EA57B228C401E00A55066 /* Build configuration list for PBXNativeTarget "QMUIKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( CD4EA579228C401E00A55066 /* Debug */, CD4EA57A228C401E00A55066 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; FE0AFAE81D82B9D8000D21D9 /* Build configuration list for PBXNativeTarget "QMUIKit" */ = { isa = XCConfigurationList; buildConfigurations = ( FE0AFAE91D82B9D8000D21D9 /* Debug */, FE0AFAEA1D82B9D8000D21D9 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = CD44C1BD1956D5970098D0A2 /* Project object */; } ================================================ FILE: qmui.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: qmui.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: qmui.xcodeproj/xcshareddata/xcschemes/QMUIKit.xcscheme ================================================ ================================================ FILE: qmui.xcodeproj/xcshareddata/xcschemes/QMUIKitTests.xcscheme ================================================ ================================================ FILE: umbrellaHeaderFileCreator.py ================================================ #!/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()