Selaa lähdekoodia

v2.2.7 适配 iOS 18.1

openlockPPP 6 kuukautta sitten
vanhempi
commit
a36636b8f8
100 muutettua tiedostoa jossa 9658 lisäystä ja 8600 poistoa
  1. 2 2
      Podfile
  2. 145 125
      Podfile.lock
  3. 145 125
      Pods/Manifest.lock
  4. 5649 5600
      Pods/Pods.xcodeproj/project.pbxproj
  5. 5 0
      Pods/Pods.xcodeproj/xcuserdata/mimasigeling.xcuserdatad/xcschemes/xcschememanagement.plist
  6. 4 16
      Pods/QMUIKit/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m
  7. 10 4
      Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m
  8. 6 0
      Pods/QMUIKit/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h
  9. 72 22
      Pods/QMUIKit/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m
  10. 15 48
      Pods/QMUIKit/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m
  11. 3 3
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAlertController.h
  12. 23 23
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAlertController.m
  13. 18 14
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeProtocol.h
  14. 16 58
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m
  15. 72 238
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m
  16. 5 0
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h
  17. 155 352
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m
  18. 48 1
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m
  19. 3 3
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellHeightCache.m
  20. 17 17
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m
  21. 1 6
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m
  22. 53 60
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIKeyboardManager.m
  23. 1 2
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILabel.m
  24. 20 20
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILogManagerViewController.m
  25. 6 0
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h
  26. 6 18
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m
  27. 10 1
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUINavigationTitleView.h
  28. 27 7
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUINavigationTitleView.m
  29. 43 3
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupContainerView.h
  30. 255 119
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupContainerView.m
  31. 94 17
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h
  32. 481 182
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.m
  33. 30 7
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUISearchController.h
  34. 225 111
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUISearchController.m
  35. 1 1
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.m
  36. 17 5
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITextField.m
  37. 5 0
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITextView.h
  38. 29 0
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITextView.m
  39. 5 11
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m
  40. 35 97
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m
  41. 26 26
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.m
  42. 1 4
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.h
  43. 40 45
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.m
  44. 12 25
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m
  45. 11 10
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m
  46. 1 1
      Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIZoomImageView.m
  47. 1 1
      Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastView.h
  48. 4 4
      Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastView.m
  49. 35 12
      Pods/QMUIKit/QMUIKit/QMUICore/QMUICommonDefines.h
  50. 0 6
      Pods/QMUIKit/QMUIKit/QMUICore/QMUIConfiguration.h
  51. 68 205
      Pods/QMUIKit/QMUIKit/QMUICore/QMUIConfiguration.m
  52. 1 7
      Pods/QMUIKit/QMUIKit/QMUICore/QMUIConfigurationMacros.h
  53. 34 11
      Pods/QMUIKit/QMUIKit/QMUICore/QMUIHelper.h
  54. 243 60
      Pods/QMUIKit/QMUIKit/QMUICore/QMUIHelper.m
  55. 19 1
      Pods/QMUIKit/QMUIKit/QMUICore/QMUIRuntime.m
  56. 47 7
      Pods/QMUIKit/QMUIKit/QMUIKit.h
  57. 2 2
      Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m
  58. 11 22
      Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUINavigationController.m
  59. 3 3
      Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/Contents.json
  60. 12 0
      Pods/QMUIKit/QMUIKit/UIKitExtensions/CALayer+QMUI.h
  61. 59 0
      Pods/QMUIKit/QMUIKit/UIKitExtensions/CALayer+QMUI.m
  62. 7 2
      Pods/QMUIKit/QMUIKit/UIKitExtensions/NSArray+QMUI.h
  63. 16 2
      Pods/QMUIKit/QMUIKit/UIKitExtensions/NSArray+QMUI.m
  64. 20 0
      Pods/QMUIKit/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.h
  65. 36 0
      Pods/QMUIKit/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.m
  66. 30 36
      Pods/QMUIKit/QMUIKit/UIKitExtensions/NSObject+QMUI.m
  67. 3 3
      Pods/QMUIKit/QMUIKit/UIKitExtensions/NSString+QMUI.h
  68. 8 7
      Pods/QMUIKit/QMUIKit/UIKitExtensions/NSString+QMUI.m
  69. 8 19
      Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.m
  70. 8 19
      Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.m
  71. 89 6
      Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIStringPrivate.m
  72. 8 11
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h
  73. 62 10
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m
  74. 2 2
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m
  75. 6 8
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIButton+QMUI.m
  76. 30 32
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIColor+QMUI.m
  77. 4 3
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIControl+QMUI.m
  78. 11 1
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.m
  79. 3 0
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIImage+QMUI.h
  80. 36 1
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIImage+QMUI.m
  81. 1 1
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIInterface+QMUI.h
  82. 7 0
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIInterface+QMUI.m
  83. 12 0
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UILabel+QMUI.h
  84. 37 12
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UILabel+QMUI.m
  85. 91 53
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIMenuController+QMUI.m
  86. 73 69
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m
  87. 7 0
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h
  88. 94 36
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m
  89. 11 1
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIScrollView+QMUI.h
  90. 38 23
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UIScrollView+QMUI.m
  91. 7 2
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchBar+QMUI.h
  92. 139 147
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m
  93. 19 0
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchController+QMUI.h
  94. 229 2
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchController+QMUI.m
  95. 1 1
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UISlider+QMUI.m
  96. 10 14
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UISwitch+QMUI.m
  97. 100 282
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UITabBar+QMUI.m
  98. 2 2
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.h
  99. 1 4
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m
  100. 5 19
      Pods/QMUIKit/QMUIKit/UIKitExtensions/UITableView+QMUI.h

+ 2 - 2
Podfile

@@ -9,7 +9,7 @@ source 'https://gitee.com/huazi-rongchuang/cxhadsdk-ios-repo.git'
 #source 'https://github.com/ScareCrowInMountain/RQPrivateSpec.git'
 
 # 说明平台是ios,版本是11.0
-platform:ios,'11.0'
+platform:ios,'13.0'
 
 # 忽略引入库的所有警告(强迫症者的福音啊)
 inhibit_all_warnings!
@@ -29,7 +29,7 @@ target  'jiaPei'  do
     pod 'MBProgressHUD'
     pod 'MJExtension'
     pod 'MJRefresh'
-    pod 'QMUIKit'                                     # 腾讯UI控件
+    pod 'QMUIKit', '~> 4.8.0'                               # 腾讯UI控件
     pod 'ReactiveObjC'
     pod 'RealReachability'
     pod 'SDCycleScrollView'

+ 145 - 125
Podfile.lock

@@ -202,74 +202,77 @@ PODS:
   - QMLineSDK (4.4.0):
     - QMBaseLib
     - SocketRocket (~> 0.6.0)
-  - QMUIKit (4.6.0):
-    - QMUIKit/QMUIComponents (= 4.6.0)
-    - QMUIKit/QMUICore (= 4.6.0)
-    - QMUIKit/QMUILog (= 4.6.0)
-    - QMUIKit/QMUIMainFrame (= 4.6.0)
-    - QMUIKit/QMUIResources (= 4.6.0)
-    - QMUIKit/QMUIWeakObjectContainer (= 4.6.0)
-  - QMUIKit/QMUIComponents (4.6.0):
-    - QMUIKit/QMUIComponents/NavigationBarTransition (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIAlertController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIAnimation (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIAppearance (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIAssetLibrary (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIBadge (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIButton (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICAAnimationExtension (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICALayerExtension (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICellHeightCache (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICellHeightKeyCache (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICellSizeKeyCache (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIConsole (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIDialogViewController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIEmotionInputManager (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIEmotionView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIEmptyView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIFloatLayoutView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIGridView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIImagePickerLibrary (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIImagePreviewView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIKeyboardManager (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUILabel (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUILogManagerViewController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUILogWithConfigurationSupported (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIMarqueeLabel (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIModalPresentationViewController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIMoreOperationController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIMultipleDelegates (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUINavigationButton (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUINavigationTitleView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIOrderedDictionary (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIPieProgressView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIPopupContainerView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIPopupMenuView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIScrollAnimator (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUISearchBar (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUISearchController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUISegmentedControl (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIStaticTableView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITableView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITableViewCell (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITableViewProtocols (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITestView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITextField (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITextView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITheme (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITips (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIToastView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIToolbarButton (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIWindowSizeMonitor (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIZoomImageView (= 4.6.0)
-    - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/NavigationBarTransition (4.6.0):
+  - QMUIKit (4.8.0):
+    - QMUIKit/QMUIComponents (= 4.8.0)
+    - QMUIKit/QMUICore (= 4.8.0)
+    - QMUIKit/QMUILog (= 4.8.0)
+    - QMUIKit/QMUIMainFrame (= 4.8.0)
+    - QMUIKit/QMUIResources (= 4.8.0)
+    - QMUIKit/QMUIWeakObjectContainer (= 4.8.0)
+  - QMUIKit/QMUIComponents (4.8.0):
+    - QMUIKit/QMUIComponents/NavigationBarTransition (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIAlertController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIAnimation (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIAppearance (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIAssetLibrary (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIBadge (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIButton (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICAAnimationExtension (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICALayerExtension (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICellHeightCache (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICellHeightKeyCache (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICellSizeKeyCache (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICheckbox (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIConsole (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIDialogViewController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIEmotionInputManager (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIEmotionView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIEmptyView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIFloatLayoutView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIGridView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIImagePickerLibrary (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIImagePreviewView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIKeyboardManager (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUILabel (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUILayouter (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUILogManagerViewController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUILogWithConfigurationSupported (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIMarqueeLabel (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIModalPresentationViewController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIMoreOperationController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIMultipleDelegates (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUINavigationButton (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUINavigationTitleView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIOrderedDictionary (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIPieProgressView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIPopupContainerView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIPopupMenuView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIScrollAnimator (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUISearchBar (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUISearchController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUISegmentedControl (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUISheetPresentation (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIStaticTableView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITableView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITableViewCell (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITableViewProtocols (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITestView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITextField (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITextView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITheme (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITips (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIToastView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIToolbarButton (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIWindowSizeMonitor (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIZoomImageView (= 4.8.0)
+    - QMUIKit/QMUICore
+  - QMUIKit/QMUIComponents/NavigationBarTransition (4.8.0):
     - QMUIKit/QMUIComponents/QMUINavigationTitleView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUIAlertController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIAlertController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUIKeyboardManager
@@ -277,36 +280,41 @@ PODS:
     - QMUIKit/QMUIComponents/QMUIModalPresentationViewController
     - QMUIKit/QMUIComponents/QMUITextField
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIAnimation (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIAnimation (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIAppearance (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIAppearance (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIAssetLibrary (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIAssetLibrary (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIBadge (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIBadge (4.8.0):
     - QMUIKit/QMUIComponents/QMUILabel
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIButton (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIButton (4.8.0):
+    - QMUIKit/QMUIComponents/QMUILayouter
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICAAnimationExtension (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICAAnimationExtension (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICALayerExtension (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICALayerExtension (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICellHeightCache (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICellHeightCache (4.8.0):
     - QMUIKit/QMUIComponents/QMUITableViewProtocols
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICellHeightKeyCache (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICellHeightKeyCache (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUIComponents/QMUITableViewProtocols
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICellSizeKeyCache (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICellSizeKeyCache (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICheckbox (4.8.0):
+    - QMUIKit/QMUIComponents/QMUIButton
+    - QMUIKit/QMUICore
+    - QMUIKit/QMUIResources
+  - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIConsole (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIConsole (4.8.0):
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUICAAnimationExtension
     - QMUIKit/QMUIComponents/QMUICellHeightKeyCache
@@ -317,7 +325,7 @@ PODS:
     - QMUIKit/QMUIComponents/QMUITextView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIResources
-  - QMUIKit/QMUIComponents/QMUIDialogViewController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIDialogViewController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUILabel
@@ -328,22 +336,22 @@ PODS:
     - QMUIKit/QMUIComponents/QMUITextField
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUIEmotionInputManager (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIEmotionInputManager (4.8.0):
     - QMUIKit/QMUIComponents/QMUIEmotionView
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIEmotionView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIEmotionView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUICore
     - QMUIKit/QMUIResources
-  - QMUIKit/QMUIComponents/QMUIEmptyView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIEmptyView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIFloatLayoutView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIFloatLayoutView (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIGridView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIGridView (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIImagePickerLibrary (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIImagePickerLibrary (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAlertController
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIAssetLibrary
@@ -356,7 +364,7 @@ PODS:
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
     - QMUIKit/QMUIResources
-  - QMUIKit/QMUIComponents/QMUIImagePreviewView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIImagePreviewView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout
@@ -365,89 +373,101 @@ PODS:
     - QMUIKit/QMUIComponents/QMUIZoomImageView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUIKeyboardManager (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIKeyboardManager (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUILabel (4.6.0):
+  - QMUIKit/QMUIComponents/QMUILabel (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUILogManagerViewController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUILayouter (4.8.0):
+    - QMUIKit/QMUICore
+  - QMUIKit/QMUIComponents/QMUILogManagerViewController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIPopupMenuView
     - QMUIKit/QMUIComponents/QMUISearchController
     - QMUIKit/QMUIComponents/QMUIStaticTableView
     - QMUIKit/QMUIComponents/QMUITableView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUILogWithConfigurationSupported (4.6.0):
+  - QMUIKit/QMUIComponents/QMUILogWithConfigurationSupported (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIMarqueeLabel (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIMarqueeLabel (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIModalPresentationViewController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIModalPresentationViewController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIKeyboardManager
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIMoreOperationController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIMoreOperationController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUIModalPresentationViewController
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIMultipleDelegates (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIMultipleDelegates (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUINavigationButton (4.6.0):
+  - QMUIKit/QMUIComponents/QMUINavigationButton (4.8.0):
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUINavigationTitleView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUINavigationTitleView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIOrderedDictionary (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIOrderedDictionary (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIPieProgressView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIPieProgressView (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIPopupContainerView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIPopupContainerView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUIPopupMenuView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIPopupMenuView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIButton
+    - QMUIKit/QMUIComponents/QMUICheckbox
+    - QMUIKit/QMUIComponents/QMUILabel
+    - QMUIKit/QMUIComponents/QMUILayouter
     - QMUIKit/QMUIComponents/QMUIPopupContainerView
+    - QMUIKit/QMUIComponents/QMUITableView
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIScrollAnimator (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIScrollAnimator (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUISearchBar (4.6.0):
+  - QMUIKit/QMUIComponents/QMUISearchBar (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUISearchController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUISearchController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIEmptyView
     - QMUIKit/QMUIComponents/QMUISearchBar
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUISegmentedControl (4.6.0):
+  - QMUIKit/QMUIComponents/QMUISegmentedControl (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIStaticTableView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUISheetPresentation (4.8.0):
+    - QMUIKit/QMUIComponents/QMUIButton
+    - QMUIKit/QMUIComponents/QMUIMultipleDelegates
+    - QMUIKit/QMUIComponents/QMUINavigationButton
+    - QMUIKit/QMUICore
+    - QMUIKit/QMUIMainFrame
+  - QMUIKit/QMUIComponents/QMUIStaticTableView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUIComponents/QMUITableViewCell
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITableView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITableView (4.8.0):
     - QMUIKit/QMUIComponents/QMUITableViewProtocols
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITableViewCell (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITableViewCell (4.8.0):
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITableViewProtocols (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITableViewProtocols (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITestView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITestView (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITextField (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITextField (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITextView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITextView (4.8.0):
     - QMUIKit/QMUIComponents/QMUILabel
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITheme (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITheme (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAlertController
     - QMUIKit/QMUIComponents/QMUIBadge
     - QMUIKit/QMUIComponents/QMUIButton
@@ -465,29 +485,29 @@ PODS:
     - QMUIKit/QMUIComponents/QMUITextView
     - QMUIKit/QMUIComponents/QMUIToastView
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITips (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITips (4.8.0):
     - QMUIKit/QMUIComponents/QMUIToastView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIResources
-  - QMUIKit/QMUIComponents/QMUIToastView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIToastView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIKeyboardManager
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIToolbarButton (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIToolbarButton (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIWindowSizeMonitor (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIWindowSizeMonitor (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIZoomImageView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIZoomImageView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAssetLibrary
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUIEmptyView
     - QMUIKit/QMUIComponents/QMUIPieProgressView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIResources
-  - QMUIKit/QMUICore (4.6.0):
+  - QMUIKit/QMUICore (4.8.0):
     - QMUIKit/QMUILog
     - QMUIKit/QMUIWeakObjectContainer
-  - QMUIKit/QMUILog (4.6.0)
-  - QMUIKit/QMUIMainFrame (4.6.0):
+  - QMUIKit/QMUILog (4.8.0)
+  - QMUIKit/QMUIMainFrame (4.8.0):
     - QMUIKit/QMUIComponents/QMUIEmptyView
     - QMUIKit/QMUIComponents/QMUIKeyboardManager
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
@@ -496,8 +516,8 @@ PODS:
     - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView
     - QMUIKit/QMUICore
     - QMUIKit/QMUILog
-  - QMUIKit/QMUIResources (4.6.0)
-  - QMUIKit/QMUIWeakObjectContainer (4.6.0)
+  - QMUIKit/QMUIResources (4.8.0)
+  - QMUIKit/QMUIWeakObjectContainer (4.8.0)
   - ReactiveObjC (3.1.1)
   - RealReachability (1.4.1):
     - RealReachability/Connection (= 1.4.1)
@@ -596,7 +616,7 @@ DEPENDENCIES:
   - MLEmojiLabel
   - OpenSSL-Universal
   - QMLineSDK (~> 4.4.0)
-  - QMUIKit
+  - QMUIKit (~> 4.8.0)
   - ReactiveObjC
   - RealReachability
   - SAMKeychain
@@ -764,7 +784,7 @@ SPEC CHECKSUMS:
   Qiniu: 601dc3247d9c033ab80b2b216517ec484770b5ed
   QMBaseLib: f623741cc351ab084387a8a67d73d11a44051819
   QMLineSDK: 34188bac5127be874e7883a89563d82926c80183
-  QMUIKit: 6321cf1124623d686a9ec0a79a7cc59d0d64a52c
+  QMUIKit: 3c622fe3c55c0dccfad750634bd9ee5e7ade9a53
   ReactiveObjC: 011caa393aa0383245f2dcf9bf02e86b80b36040
   RealReachability: 828e74e3a1e89ba8f54395344637689cf4065c36
   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
@@ -792,6 +812,6 @@ SPEC CHECKSUMS:
   ZFPlayer: 5cf39e8d9f0c2394a014b0db4767b5b5a6bffe13
   ZXSDK: 184f25c5ffdab3222b1941d1dc4864c30341a942
 
-PODFILE CHECKSUM: 723246a8b63ef1e326573cfd9fe751ae1c487ff2
+PODFILE CHECKSUM: 0a3a0145910f1a37fb882075ad1b067062667866
 
 COCOAPODS: 1.15.2

+ 145 - 125
Pods/Manifest.lock

@@ -202,74 +202,77 @@ PODS:
   - QMLineSDK (4.4.0):
     - QMBaseLib
     - SocketRocket (~> 0.6.0)
-  - QMUIKit (4.6.0):
-    - QMUIKit/QMUIComponents (= 4.6.0)
-    - QMUIKit/QMUICore (= 4.6.0)
-    - QMUIKit/QMUILog (= 4.6.0)
-    - QMUIKit/QMUIMainFrame (= 4.6.0)
-    - QMUIKit/QMUIResources (= 4.6.0)
-    - QMUIKit/QMUIWeakObjectContainer (= 4.6.0)
-  - QMUIKit/QMUIComponents (4.6.0):
-    - QMUIKit/QMUIComponents/NavigationBarTransition (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIAlertController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIAnimation (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIAppearance (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIAssetLibrary (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIBadge (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIButton (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICAAnimationExtension (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICALayerExtension (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICellHeightCache (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICellHeightKeyCache (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICellSizeKeyCache (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIConsole (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIDialogViewController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIEmotionInputManager (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIEmotionView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIEmptyView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIFloatLayoutView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIGridView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIImagePickerLibrary (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIImagePreviewView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIKeyboardManager (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUILabel (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUILogManagerViewController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUILogWithConfigurationSupported (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIMarqueeLabel (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIModalPresentationViewController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIMoreOperationController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIMultipleDelegates (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUINavigationButton (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUINavigationTitleView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIOrderedDictionary (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIPieProgressView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIPopupContainerView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIPopupMenuView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIScrollAnimator (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUISearchBar (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUISearchController (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUISegmentedControl (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIStaticTableView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITableView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITableViewCell (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITableViewProtocols (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITestView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITextField (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITextView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITheme (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUITips (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIToastView (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIToolbarButton (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIWindowSizeMonitor (= 4.6.0)
-    - QMUIKit/QMUIComponents/QMUIZoomImageView (= 4.6.0)
-    - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/NavigationBarTransition (4.6.0):
+  - QMUIKit (4.8.0):
+    - QMUIKit/QMUIComponents (= 4.8.0)
+    - QMUIKit/QMUICore (= 4.8.0)
+    - QMUIKit/QMUILog (= 4.8.0)
+    - QMUIKit/QMUIMainFrame (= 4.8.0)
+    - QMUIKit/QMUIResources (= 4.8.0)
+    - QMUIKit/QMUIWeakObjectContainer (= 4.8.0)
+  - QMUIKit/QMUIComponents (4.8.0):
+    - QMUIKit/QMUIComponents/NavigationBarTransition (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIAlertController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIAnimation (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIAppearance (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIAssetLibrary (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIBadge (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIButton (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICAAnimationExtension (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICALayerExtension (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICellHeightCache (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICellHeightKeyCache (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICellSizeKeyCache (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICheckbox (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIConsole (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIDialogViewController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIEmotionInputManager (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIEmotionView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIEmptyView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIFloatLayoutView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIGridView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIImagePickerLibrary (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIImagePreviewView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIKeyboardManager (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUILabel (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUILayouter (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUILogManagerViewController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUILogWithConfigurationSupported (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIMarqueeLabel (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIModalPresentationViewController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIMoreOperationController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIMultipleDelegates (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUINavigationButton (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUINavigationTitleView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIOrderedDictionary (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIPieProgressView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIPopupContainerView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIPopupMenuView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIScrollAnimator (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUISearchBar (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUISearchController (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUISegmentedControl (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUISheetPresentation (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIStaticTableView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITableView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITableViewCell (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITableViewProtocols (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITestView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITextField (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITextView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITheme (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUITips (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIToastView (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIToolbarButton (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIWindowSizeMonitor (= 4.8.0)
+    - QMUIKit/QMUIComponents/QMUIZoomImageView (= 4.8.0)
+    - QMUIKit/QMUICore
+  - QMUIKit/QMUIComponents/NavigationBarTransition (4.8.0):
     - QMUIKit/QMUIComponents/QMUINavigationTitleView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUIAlertController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIAlertController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUIKeyboardManager
@@ -277,36 +280,41 @@ PODS:
     - QMUIKit/QMUIComponents/QMUIModalPresentationViewController
     - QMUIKit/QMUIComponents/QMUITextField
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIAnimation (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIAnimation (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIAppearance (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIAppearance (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIAssetLibrary (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIAssetLibrary (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIBadge (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIBadge (4.8.0):
     - QMUIKit/QMUIComponents/QMUILabel
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIButton (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIButton (4.8.0):
+    - QMUIKit/QMUIComponents/QMUILayouter
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICAAnimationExtension (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICAAnimationExtension (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICALayerExtension (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICALayerExtension (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICellHeightCache (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICellHeightCache (4.8.0):
     - QMUIKit/QMUIComponents/QMUITableViewProtocols
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICellHeightKeyCache (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICellHeightKeyCache (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUIComponents/QMUITableViewProtocols
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICellSizeKeyCache (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICellSizeKeyCache (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout (4.6.0):
+  - QMUIKit/QMUIComponents/QMUICheckbox (4.8.0):
+    - QMUIKit/QMUIComponents/QMUIButton
+    - QMUIKit/QMUICore
+    - QMUIKit/QMUIResources
+  - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIConsole (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIConsole (4.8.0):
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUICAAnimationExtension
     - QMUIKit/QMUIComponents/QMUICellHeightKeyCache
@@ -317,7 +325,7 @@ PODS:
     - QMUIKit/QMUIComponents/QMUITextView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIResources
-  - QMUIKit/QMUIComponents/QMUIDialogViewController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIDialogViewController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUILabel
@@ -328,22 +336,22 @@ PODS:
     - QMUIKit/QMUIComponents/QMUITextField
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUIEmotionInputManager (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIEmotionInputManager (4.8.0):
     - QMUIKit/QMUIComponents/QMUIEmotionView
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIEmotionView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIEmotionView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUICore
     - QMUIKit/QMUIResources
-  - QMUIKit/QMUIComponents/QMUIEmptyView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIEmptyView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIFloatLayoutView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIFloatLayoutView (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIGridView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIGridView (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIImagePickerLibrary (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIImagePickerLibrary (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAlertController
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIAssetLibrary
@@ -356,7 +364,7 @@ PODS:
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
     - QMUIKit/QMUIResources
-  - QMUIKit/QMUIComponents/QMUIImagePreviewView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIImagePreviewView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUICollectionViewPagingLayout
@@ -365,89 +373,101 @@ PODS:
     - QMUIKit/QMUIComponents/QMUIZoomImageView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUIKeyboardManager (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIKeyboardManager (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUILabel (4.6.0):
+  - QMUIKit/QMUIComponents/QMUILabel (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUILogManagerViewController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUILayouter (4.8.0):
+    - QMUIKit/QMUICore
+  - QMUIKit/QMUIComponents/QMUILogManagerViewController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIPopupMenuView
     - QMUIKit/QMUIComponents/QMUISearchController
     - QMUIKit/QMUIComponents/QMUIStaticTableView
     - QMUIKit/QMUIComponents/QMUITableView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUILogWithConfigurationSupported (4.6.0):
+  - QMUIKit/QMUIComponents/QMUILogWithConfigurationSupported (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIMarqueeLabel (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIMarqueeLabel (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIModalPresentationViewController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIModalPresentationViewController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIKeyboardManager
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIMoreOperationController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIMoreOperationController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUIModalPresentationViewController
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIMultipleDelegates (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIMultipleDelegates (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUINavigationButton (4.6.0):
+  - QMUIKit/QMUIComponents/QMUINavigationButton (4.8.0):
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUINavigationTitleView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUINavigationTitleView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIOrderedDictionary (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIOrderedDictionary (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIPieProgressView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIPieProgressView (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIPopupContainerView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIPopupContainerView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAppearance
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUIPopupMenuView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIPopupMenuView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIButton
+    - QMUIKit/QMUIComponents/QMUICheckbox
+    - QMUIKit/QMUIComponents/QMUILabel
+    - QMUIKit/QMUIComponents/QMUILayouter
     - QMUIKit/QMUIComponents/QMUIPopupContainerView
+    - QMUIKit/QMUIComponents/QMUITableView
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIScrollAnimator (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIScrollAnimator (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUISearchBar (4.6.0):
+  - QMUIKit/QMUIComponents/QMUISearchBar (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUISearchController (4.6.0):
+  - QMUIKit/QMUIComponents/QMUISearchController (4.8.0):
     - QMUIKit/QMUIComponents/QMUIEmptyView
     - QMUIKit/QMUIComponents/QMUISearchBar
     - QMUIKit/QMUICore
     - QMUIKit/QMUIMainFrame
-  - QMUIKit/QMUIComponents/QMUISegmentedControl (4.6.0):
+  - QMUIKit/QMUIComponents/QMUISegmentedControl (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIStaticTableView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUISheetPresentation (4.8.0):
+    - QMUIKit/QMUIComponents/QMUIButton
+    - QMUIKit/QMUIComponents/QMUIMultipleDelegates
+    - QMUIKit/QMUIComponents/QMUINavigationButton
+    - QMUIKit/QMUICore
+    - QMUIKit/QMUIMainFrame
+  - QMUIKit/QMUIComponents/QMUIStaticTableView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUIComponents/QMUITableViewCell
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITableView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITableView (4.8.0):
     - QMUIKit/QMUIComponents/QMUITableViewProtocols
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITableViewCell (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITableViewCell (4.8.0):
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITableViewProtocols (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITableViewProtocols (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITestView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITestView (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITextField (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITextField (4.8.0):
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITextView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITextView (4.8.0):
     - QMUIKit/QMUIComponents/QMUILabel
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITheme (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITheme (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAlertController
     - QMUIKit/QMUIComponents/QMUIBadge
     - QMUIKit/QMUIComponents/QMUIButton
@@ -465,29 +485,29 @@ PODS:
     - QMUIKit/QMUIComponents/QMUITextView
     - QMUIKit/QMUIComponents/QMUIToastView
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUITips (4.6.0):
+  - QMUIKit/QMUIComponents/QMUITips (4.8.0):
     - QMUIKit/QMUIComponents/QMUIToastView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIResources
-  - QMUIKit/QMUIComponents/QMUIToastView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIToastView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIKeyboardManager
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIToolbarButton (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIToolbarButton (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIWindowSizeMonitor (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIWindowSizeMonitor (4.8.0):
     - QMUIKit/QMUICore
-  - QMUIKit/QMUIComponents/QMUIZoomImageView (4.6.0):
+  - QMUIKit/QMUIComponents/QMUIZoomImageView (4.8.0):
     - QMUIKit/QMUIComponents/QMUIAssetLibrary
     - QMUIKit/QMUIComponents/QMUIButton
     - QMUIKit/QMUIComponents/QMUIEmptyView
     - QMUIKit/QMUIComponents/QMUIPieProgressView
     - QMUIKit/QMUICore
     - QMUIKit/QMUIResources
-  - QMUIKit/QMUICore (4.6.0):
+  - QMUIKit/QMUICore (4.8.0):
     - QMUIKit/QMUILog
     - QMUIKit/QMUIWeakObjectContainer
-  - QMUIKit/QMUILog (4.6.0)
-  - QMUIKit/QMUIMainFrame (4.6.0):
+  - QMUIKit/QMUILog (4.8.0)
+  - QMUIKit/QMUIMainFrame (4.8.0):
     - QMUIKit/QMUIComponents/QMUIEmptyView
     - QMUIKit/QMUIComponents/QMUIKeyboardManager
     - QMUIKit/QMUIComponents/QMUIMultipleDelegates
@@ -496,8 +516,8 @@ PODS:
     - QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView
     - QMUIKit/QMUICore
     - QMUIKit/QMUILog
-  - QMUIKit/QMUIResources (4.6.0)
-  - QMUIKit/QMUIWeakObjectContainer (4.6.0)
+  - QMUIKit/QMUIResources (4.8.0)
+  - QMUIKit/QMUIWeakObjectContainer (4.8.0)
   - ReactiveObjC (3.1.1)
   - RealReachability (1.4.1):
     - RealReachability/Connection (= 1.4.1)
@@ -596,7 +616,7 @@ DEPENDENCIES:
   - MLEmojiLabel
   - OpenSSL-Universal
   - QMLineSDK (~> 4.4.0)
-  - QMUIKit
+  - QMUIKit (~> 4.8.0)
   - ReactiveObjC
   - RealReachability
   - SAMKeychain
@@ -764,7 +784,7 @@ SPEC CHECKSUMS:
   Qiniu: 601dc3247d9c033ab80b2b216517ec484770b5ed
   QMBaseLib: f623741cc351ab084387a8a67d73d11a44051819
   QMLineSDK: 34188bac5127be874e7883a89563d82926c80183
-  QMUIKit: 6321cf1124623d686a9ec0a79a7cc59d0d64a52c
+  QMUIKit: 3c622fe3c55c0dccfad750634bd9ee5e7ade9a53
   ReactiveObjC: 011caa393aa0383245f2dcf9bf02e86b80b36040
   RealReachability: 828e74e3a1e89ba8f54395344637689cf4065c36
   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
@@ -792,6 +812,6 @@ SPEC CHECKSUMS:
   ZFPlayer: 5cf39e8d9f0c2394a014b0db4767b5b5a6bffe13
   ZXSDK: 184f25c5ffdab3222b1941d1dc4864c30341a942
 
-PODFILE CHECKSUM: 723246a8b63ef1e326573cfd9fe751ae1c487ff2
+PODFILE CHECKSUM: 0a3a0145910f1a37fb882075ad1b067062667866
 
 COCOAPODS: 1.15.2

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 5649 - 5600
Pods/Pods.xcodeproj/project.pbxproj


+ 5 - 0
Pods/Pods.xcodeproj/xcuserdata/mimasigeling.xcuserdatad/xcschemes/xcschememanagement.plist

@@ -284,6 +284,11 @@
 			<key>isShown</key>
 			<false/>
 		</dict>
+		<key>QMUIKit-QMUIKit.xcscheme</key>
+		<dict>
+			<key>isShown</key>
+			<false/>
+		</dict>
 		<key>QMUIKit-QMUIResources.xcscheme</key>
 		<dict>
 			<key>isShown</key>

+ 4 - 16
Pods/QMUIKit/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m

@@ -54,7 +54,7 @@
     #pragma mark - QMUILog
     QMUICMI.shouldPrintDefaultLog = YES;                                        // ShouldPrintDefaultLog : 是否允许输出 QMUILogLevelDefault 级别的 log
     QMUICMI.shouldPrintInfoLog = YES;                                           // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelInfo 级别的 log
-    QMUICMI.shouldPrintWarnLog = YES;                                           // ShouldPrintInfoLog : 是否允许输出 QMUILogLevelWarn 级别的 log
+    QMUICMI.shouldPrintWarnLog = YES;                                           // ShouldPrintWarnLog : 是否允许输出 QMUILogLevelWarn 级别的 log
     QMUICMI.shouldPrintQMUIWarnLogToConsole = NO;                              // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上
     
     #pragma mark - UIControl
@@ -257,40 +257,28 @@
     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 右上角的偏移
-    BeginIgnoreDeprecatedWarning
-    QMUICMI.badgeCenterOffset = CGPointMake(14, -10);                           // BadgeCenterOffset : QMUIBadge 未读数相对于目标 view 中心的偏移
-    QMUICMI.badgeCenterOffsetLandscape = CGPointMake(16, -7);                   // BadgeCenterOffsetLandscape : QMUIBadge 未读数在横屏下相对于目标 view 中心的偏移
-    EndIgnoreDeprecatedWarning
     
     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 右上角的偏移
-    BeginIgnoreDeprecatedWarning
-    QMUICMI.updatesIndicatorCenterOffset = CGPointMake(14, -10);                // UpdatesIndicatorCenterOffset : QMUIBadge 未读红点相对于目标 view 中心的偏移
-    QMUICMI.updatesIndicatorCenterOffsetLandscape = CGPointMake(14, -10);       // UpdatesIndicatorCenterOffsetLandscape : QMUIBadge 未读红点在横屏下相对于目标 view 中心点的偏移
-    EndIgnoreDeprecatedWarning
     
     #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,固定黑色则设置为 QMUIStatusBarStyleDarkContent。
+    QMUICMI.defaultStatusBarStyle = UIStatusBarStyleDefault;                    // DefaultStatusBarStyle : 默认的状态栏样式,默认值为 UIStatusBarStyleDefault,也即在 iOS 12 及以前是黑色文字,iOS 13 及以后会自动根据当前 App 是否处于 Dark Mode 切换颜色。如果你希望固定为白色,请设置为 UIStatusBarStyleLightContent,固定黑色则设置为 UIStatusBarStyleDarkContent。
     QMUICMI.needsBackBarButtonItemTitle = YES;                                  // NeedsBackBarButtonItemTitle : 全局是否需要返回按钮的 title,不需要则只显示一个返回image
     QMUICMI.hidesBottomBarWhenPushedInitially = NO;                             // HidesBottomBarWhenPushedInitially : QMUICommonViewController.hidesBottomBarWhenPushed 的初始值,默认为 NO,以保持与系统默认值一致,但通常建议改为 YES,因为一般只有 tabBar 首页那几个界面要求为 NO
     QMUICMI.preventConcurrentNavigationControllerTransitions = YES;             // PreventConcurrentNavigationControllerTransitions : 自动保护 QMUINavigationController 在上一次 push/pop 尚未结束的时候就进行下一次 push/pop 的行为,避免产生 crash
     QMUICMI.navigationBarHiddenInitially = NO;                                  // NavigationBarHiddenInitially : QMUINavigationControllerDelegate preferredNavigationBarHidden 的初始值,默认为NO
-    QMUICMI.shouldFixTabBarTransitionBugInIPhoneX = NO;                         // ShouldFixTabBarTransitionBugInIPhoneX : 是否需要自动修复 iOS 11 下,iPhone X 的设备在 push 界面时,tabBar 会瞬间往上跳的 bug
     QMUICMI.shouldFixTabBarSafeAreaInsetsBug = NO;                              // ShouldFixTabBarSafeAreaInsetsBug : 是否要对 iOS 11 及以后的版本修复当存在 UITabBar 时,UIScrollView 的 inset.bottom 可能错误的 bug(issue #218 #934),默认为 YES
     QMUICMI.shouldFixSearchBarMaskViewLayoutBug = NO;                           // ShouldFixSearchBarMaskViewLayoutBug : 是否自动修复 UISearchController.searchBar 被当作 tableHeaderView 使用时可能出现的布局 bug(issue #950)
     QMUICMI.shouldPrintQMUIWarnLogToConsole = IS_DEBUG;                         // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上
-    QMUICMI.sendAnalyticsToQMUITeam = YES;                                      // SendAnalyticsToQMUITeam : 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用
     QMUICMI.dynamicPreferredValueForIPad = NO;                                  // DynamicPreferredValueForIPad : 当 iPad 处于 Slide Over 或 Split View 分屏模式下,宏 `PreferredValueForXXX` 是否把 iPad 视为某种屏幕宽度近似的 iPhone 来取值。
-    if (@available(iOS 13.0, *)) {
-        QMUICMI.ignoreKVCAccessProhibited = NO;                                     // IgnoreKVCAccessProhibited : 是否全局忽略 iOS 13 对 KVC 访问 UIKit 私有属性的限制
-        QMUICMI.adjustScrollIndicatorInsetsByContentInsetAdjustment = NO;           // AdjustScrollIndicatorInsetsByContentInsetAdjustment : 当将 UIScrollView.contentInsetAdjustmentBehavior 设为 UIScrollViewContentInsetAdjustmentNever 时,是否自动将 UIScrollView.automaticallyAdjustsScrollIndicatorInsets 设为 NO,以保证原本在 iOS 12 下的代码不用修改就能在 iOS 13 下正常控制滚动条的位置。
-    }
+    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。

+ 10 - 4
Pods/QMUIKit/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m

@@ -122,6 +122,16 @@ static NSString * const kImageOrUnknownCellIdentifier = @"imageorunknown";
     [self.collectionView reloadData];
 }
 
+- (void)viewWillDisappear:(BOOL)animated {
+    [super viewWillDisappear:animated];
+    
+    // 在 pop 回相簿列表时重置标志位以使下次进来 picker 时 collection 可以滚动到正确的初始位置
+    // 但不能影响从 picker 进入大图的路径
+    if (self.navigationController && ![self.navigationController.viewControllers containsObject:self]) {
+        self.hasScrollToInitialPosition = NO;
+    }
+}
+
 - (void)showEmptyView {
     [super showEmptyView];
     self.emptyView.backgroundColor = self.view.backgroundColor; // 为了盖住背后的 collectionView,这里加个背景色(不盖住的话会看到 collectionView 先滚到列表顶部然后跳到列表底部)
@@ -244,10 +254,6 @@ static NSString * const kImageOrUnknownCellIdentifier = @"imageorunknown";
     }
 }
 
-- (void)willPopInNavigationControllerWithAnimated:(BOOL)animated {
-    self.hasScrollToInitialPosition = NO;
-}
-
 #pragma mark - Getters & Setters
 
 @synthesize collectionViewLayout = _collectionViewLayout;

+ 6 - 0
Pods/QMUIKit/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h

@@ -23,7 +23,13 @@
 
 @interface _QMUITransitionNavigationBar : UINavigationBar
 
+@property(nonatomic, weak) UIViewController *parentViewController;
+
 // 建立假 bar 到真 bar 的关系,内部会通过 qmuinb_copyStylesToBar 同时设置真 bar 到假 bar 的关系
 @property(nonatomic, weak) UINavigationBar *originalNavigationBar;
+
 @property(nonatomic, assign) BOOL shouldPreventAppearance;
+
+// 根据当前的系统导航栏布局,刷新自身在 vc.view 上的布局
+- (void)updateLayout;
 @end

+ 72 - 22
Pods/QMUIKit/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m

@@ -26,32 +26,63 @@
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
         
-#ifdef IOS15_SDK_ALLOWED
         if (@available(iOS 15.0, *)) {
-            ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setStandardAppearance:), UINavigationBarAppearance *, ^(UINavigationBar *selfObject, UINavigationBarAppearance *appearance) {
-                if (selfObject.qmuinb_copyStylesToBar) {
-                    selfObject.qmuinb_copyStylesToBar.standardAppearance = appearance;
-                }
+            
+            OverrideImplementation([UINavigationBar class], @selector(setStandardAppearance:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+                return ^(UINavigationBar *selfObject, UINavigationBarAppearance *appearance) {
+                    
+                    // call super
+                    void (*originSelectorIMP)(id, SEL, UINavigationBarAppearance *);
+                    originSelectorIMP = (void (*)(id, SEL, UINavigationBarAppearance *))originalIMPProvider();
+                    originSelectorIMP(selfObject, originCMD, appearance);
+                    
+                    if (selfObject.qmuinb_copyStylesToBar) {
+                        selfObject.qmuinb_copyStylesToBar.standardAppearance = appearance;
+                    }
+                };
             });
             
-            ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setScrollEdgeAppearance:), UINavigationBarAppearance *, ^(UINavigationBar *selfObject, UINavigationBarAppearance *appearance) {
-                if (selfObject.qmuinb_copyStylesToBar) {
-                    selfObject.qmuinb_copyStylesToBar.scrollEdgeAppearance = appearance;
-                }
+            OverrideImplementation([UINavigationBar class], @selector(setScrollEdgeAppearance:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+                return ^(UINavigationBar *selfObject, UINavigationBarAppearance *appearance) {
+                    
+                    // call super
+                    void (*originSelectorIMP)(id, SEL, UINavigationBarAppearance *);
+                    originSelectorIMP = (void (*)(id, SEL, UINavigationBarAppearance *))originalIMPProvider();
+                    originSelectorIMP(selfObject, originCMD, appearance);
+                    
+                    if (selfObject.qmuinb_copyStylesToBar) {
+                        selfObject.qmuinb_copyStylesToBar.standardAppearance = appearance;
+                    }
+                };
             });
         }
-#endif
         
-        ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setBarStyle:), UIBarStyle, ^(UINavigationBar *selfObject, UIBarStyle barStyle) {
-            if (selfObject.qmuinb_copyStylesToBar && selfObject.qmuinb_copyStylesToBar.barStyle != barStyle) {
-                selfObject.qmuinb_copyStylesToBar.barStyle = barStyle;
-            }
+        OverrideImplementation([UINavigationBar class], @selector(setBarStyle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+            return ^(UINavigationBar *selfObject, UIBarStyle barStyle) {
+                
+                // call super
+                void (*originSelectorIMP)(id, SEL, UIBarStyle);
+                originSelectorIMP = (void (*)(id, SEL, UIBarStyle))originalIMPProvider();
+                originSelectorIMP(selfObject, originCMD, barStyle);
+                
+                if (selfObject.qmuinb_copyStylesToBar && selfObject.qmuinb_copyStylesToBar.barStyle != barStyle) {
+                    selfObject.qmuinb_copyStylesToBar.barStyle = barStyle;
+                }
+            };
         });
         
-        ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setBarTintColor:), UIColor *, ^(UINavigationBar *selfObject, UIColor *barTintColor) {
-            if (selfObject.qmuinb_copyStylesToBar && ![selfObject.qmuinb_copyStylesToBar.barTintColor isEqual:barTintColor]) {
-                selfObject.qmuinb_copyStylesToBar.barTintColor = barTintColor;
-            }
+        OverrideImplementation([UINavigationBar class], @selector(setBarTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+            return ^(UINavigationBar *selfObject, UIColor *barTintColor) {
+                
+                // call super
+                void (*originSelectorIMP)(id, SEL, UIColor *);
+                originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider();
+                originSelectorIMP(selfObject, originCMD, barTintColor);
+                
+                if (selfObject.qmuinb_copyStylesToBar && ![selfObject.qmuinb_copyStylesToBar.barTintColor isEqual:barTintColor]) {
+                    selfObject.qmuinb_copyStylesToBar.barTintColor = barTintColor;
+                }
+            };
         });
         
         OverrideImplementation([UINavigationBar class], @selector(setBackgroundImage:forBarMetrics:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
@@ -68,10 +99,18 @@
             };
         });
         
-        ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setShadowImage:), UIImage *, ^(UINavigationBar *selfObject, UIImage *firstArgv) {
-            if (selfObject.qmuinb_copyStylesToBar) {
-                selfObject.qmuinb_copyStylesToBar.shadowImage = firstArgv;
-            }
+        OverrideImplementation([UINavigationBar class], @selector(setShadowImage:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
+            return ^(UINavigationBar *selfObject, UIImage *shadowImage) {
+                
+                // call super
+                void (*originSelectorIMP)(id, SEL, UIImage *);
+                originSelectorIMP = (void (*)(id, SEL, UIImage *))originalIMPProvider();
+                originSelectorIMP(selfObject, originCMD, shadowImage);
+                
+                if (selfObject.qmuinb_copyStylesToBar) {
+                    selfObject.qmuinb_copyStylesToBar.shadowImage = shadowImage;
+                }
+            };
         });
         
         OverrideImplementation([UINavigationBar class], @selector(setQmui_effect:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
@@ -211,6 +250,8 @@ static char kAssociatedObjectKey_copyStylesToBar;
     // 只复制当前 originBar 的样式,所以复制完立马就清空
     originBar.qmuinb_copyStylesToBar = self;
     originBar.qmuinb_copyStylesToBar = nil;
+    
+    [self updateLayout];
 }
 
 - (void)layoutSubviews {
@@ -228,4 +269,13 @@ static char kAssociatedObjectKey_copyStylesToBar;
     }
 }
 
+- (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

+ 15 - 48
Pods/QMUIKit/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m

@@ -113,7 +113,7 @@ QMUISynthesizeIdStrongProperty(qmui_specifiedTextColor, setQmui_specifiedTextCol
                 switch (action) {
                     case QMUINavigationActionDidPush:
                     case QMUINavigationActionWillPop:
-                    case QMUINavigationActionWillSet: {
+                    case QMUINavigationActionDidSet: {
                         BOOL shouldCustomNavigationBarTransition =
                         [weakNavigationController shouldCustomTransitionAutomaticallyForOperation:UINavigationControllerOperationPush firstViewController:disappearingViewController secondViewController:appearingViewController];
                         if (shouldCustomNavigationBarTransition) {
@@ -134,22 +134,6 @@ QMUISynthesizeIdStrongProperty(qmui_specifiedTextColor, setQmui_specifiedTextCol
                         weakNavigationController.navigationBar.qmuinb_copyStylesToBar = nil;
                     }
                         break;
-                    case QMUINavigationActionDidPop: {
-                        
-                        if (@available(iOS 13.0, *)) {
-                        } else {
-                            // iOS 12 及以下系统,在不使用自定义 titleView 的情况下,在 viewWillAppear 时通过修改 navigationBar.titleTextAttributes 来设置新界面的导航栏标题样式,push 时是生效的,但 pop 时右边界面的样式会覆盖左边界面的样式,所以 pop 时的 titleTextAttributes 改为在 did pop 时处理
-                            // 如果用自定义 titleView 则没这种问题,只是为了代码简单,时机的选择不区分是否自定义 title
-                            [appearingViewController renderNavigationBarTitleAppearanceAnimated:animated];
-                            [weakNavigationController qmui_animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
-                                // 这里要重新获取 topViewController,因为触发 pop 有两种:1. 普通完整的 pop;2.手势返回又取消。后者在 completion 里拿到的 topViewController 已经不是 completion 外面那个 appearingViewController 了,只有重新获取的 topViewController 才能代表最终可视的那个界面
-                                // https://github.com/Tencent/QMUI_iOS/issues/1210
-                                [weakNavigationController.topViewController renderNavigationBarTitleAppearanceAnimated:animated];
-                            }];
-                        }
-                    }
-                        break;
-                        
                     default:
                         break;
                 }
@@ -189,9 +173,7 @@ QMUISynthesizeIdStrongProperty(qmui_specifiedTextColor, setQmui_specifiedTextCol
         
         OverrideImplementation([UIViewController class], @selector(viewWillLayoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
             return ^(UIViewController *selfObject) {
-                if (selfObject.transitionNavigationBar) {
-                    [selfObject layoutTransitionNavigationBar];
-                }
+                [selfObject.transitionNavigationBar updateLayout];
                 
                 // call super
                 void (*originSelectorIMP)(id, SEL);
@@ -249,24 +231,26 @@ QMUISynthesizeIdStrongProperty(qmui_specifiedTextColor, setQmui_specifiedTextCol
     }
     
     _QMUITransitionNavigationBar *customBar = [[_QMUITransitionNavigationBar alloc] init];
+    customBar.parentViewController = self;
     self.transitionNavigationBar = customBar;
     
     // iOS 15 里,假 bar 在 add 到界面上时会被强制同步为 UIAppearance 的值,不管你之前是否设置过自己的样式。而且在那个 runloop 内不管你后续怎么更新 standardAppearance,都会呈现出 UIAppearance 里的统一的值的样式。所以这里一方面屏蔽 didMoveToWindow,从而避免在这时候应用 UIAppearance,另一方面要保证先 add 到界面上再同步当前导航栏的样式。
-    // 经测试只有 push 时需要这么处理,pop 没问题
+    // 经测试只有 push 或 push 动画的 set 需要这么处理,pop 及 pop 动画的 set 没问题
     // iOS 14 及以下没这种问题。
-#ifdef IOS15_SDK_ALLOWED
+    // https://github.com/Tencent/QMUI_iOS/issues/1501
     if (@available(iOS 15.0, *)) {
-        if (self.navigationController.qmui_navigationAction == QMUINavigationActionDidPush) {
+        BOOL isPush = self.navigationController.qmui_navigationAction == QMUINavigationActionDidPush;
+        BOOL isSet = self.navigationController.qmui_navigationAction == QMUINavigationActionDidSet;
+        BOOL isPopAnimation = isSet && self.navigationController.qmui_lastOperation == UINavigationControllerOperationPop;
+        if (isPush || (isSet && !isPopAnimation)) {
             customBar.shouldPreventAppearance = YES;
         }
     }
-#endif
     [self.view addSubview:customBar];
     customBar.originalNavigationBar = self.navigationController.navigationBar;// 注意这里内部不会保留真 bar 和假 bar 的 copy 关系
     if (shouldBind) {
         self.navigationController.navigationBar.qmuinb_copyStylesToBar = customBar;
     }
-    [self layoutTransitionNavigationBar];
 }
 
 - (void)removeTransitionNavigationBar {
@@ -280,15 +264,6 @@ QMUISynthesizeIdStrongProperty(qmui_specifiedTextColor, setQmui_specifiedTextCol
     }
 }
 
-- (void)layoutTransitionNavigationBar {
-    if (self.isViewLoaded && self.navigationController) {
-        UIView *backgroundView = self.navigationController.navigationBar.qmui_backgroundView;
-        CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.view];
-        self.transitionNavigationBar.frame = CGRectSetX(rect, 0);// push/pop 过程中系统的导航栏转换过来的 x 可能是 112、-112
-        [self.view bringSubviewToFront:self.transitionNavigationBar];// 避免在后续被其他 subviews 盖住
-    }
-}
-
 #pragma mark - 工具方法
 
 // 根据当前的viewController,统一处理导航栏的显隐、样式
@@ -404,16 +379,7 @@ QMUISynthesizeIdStrongProperty(qmui_specifiedTextColor, setQmui_specifiedTextCol
     // iOS 13 及以上,title 的更新只在 viewWillAppear 这里进行就可以了,但 iOS 12 及以下还要靠 popViewController 那边
     // iOS 12 及以下系统,在不使用自定义 titleView 的情况下,在 viewWillAppear 时通过修改 navigationBar.titleTextAttributes 来设置新界面的导航栏标题样式,push 时是生效的,但 pop 时右边界面的样式会覆盖左边界面的样式,所以 pop 时的 titleTextAttributes 改为在 did pop 时处理
     // 如果用自定义 titleView 则没这种问题,只是为了代码简单,时机的选择不区分是否自定义 title
-    BOOL shouldRenderTitle = YES;
-    if (@available(iOS 13.0, *)) {
-    } else {
-        // push/pop 时如果 animated 为 NO,那么走到这里时 push/pop 已经结束了,action 处于 unknown 状态,所以这里要把 unknown 也包含进去
-        // https://github.com/Tencent/QMUI_iOS/issues/1190
-        shouldRenderTitle = navigationController.qmui_navigationAction >= QMUINavigationActionUnknow && navigationController.qmui_navigationAction <= QMUINavigationActionPushCompleted;
-    }
-    if (shouldRenderTitle) {
-        [vc renderNavigationBarTitleAppearanceAnimated:animated];
-    }
+    [vc renderNavigationBarTitleAppearanceAnimated:animated];
 }
 
 // 仅处理导航栏标题
@@ -435,8 +401,8 @@ QMUISynthesizeIdStrongProperty(qmui_specifiedTextColor, setQmui_specifiedTextCol
     // 导航栏title的颜色
     if ([vc respondsToSelector:@selector(qmui_titleViewTintColor)]) {
         UIColor *tintColor = [vc qmui_titleViewTintColor];
-        if ([vc.navigationItem.titleView isKindOfClass:QMUINavigationTitleView.class]) {
-            ((QMUINavigationTitleView *)vc.navigationItem.titleView).tintColor = tintColor;
+        if (vc.navigationItem.titleView.qmui_useAsNavigationTitleView) {
+            vc.navigationItem.titleView.tintColor = tintColor;
         } else if (!vc.navigationItem.titleView) {
             NSMutableDictionary<NSAttributedStringKey, id> *titleTextAttributes = (navigationController.navigationBar.titleTextAttributes ?: @{}).mutableCopy;
             titleTextAttributes[NSForegroundColorAttributeName] = tintColor;
@@ -446,8 +412,8 @@ QMUISynthesizeIdStrongProperty(qmui_specifiedTextColor, setQmui_specifiedTextCol
         }
     } else if (QMUICMIActivated) {
         UIColor *tintColor = NavBarTitleColor;
-        if ([vc.navigationItem.titleView isKindOfClass:QMUINavigationTitleView.class]) {
-            ((QMUINavigationTitleView *)vc.navigationItem.titleView).tintColor = tintColor;
+        if (vc.navigationItem.titleView.qmui_useAsNavigationTitleView) {
+            vc.navigationItem.titleView.tintColor = tintColor;
         } else if (!vc.navigationItem.titleView) {
             NSMutableDictionary<NSAttributedStringKey, id> *titleTextAttributes = (navigationController.navigationBar.titleTextAttributes ?: @{}).mutableCopy;
             titleTextAttributes[NSForegroundColorAttributeName] = tintColor;
@@ -503,6 +469,7 @@ QMUISynthesizeIdStrongProperty(qmui_specifiedTextColor, setQmui_specifiedTextCol
     return NO;
 }
 
+// 对于有一个界面隐藏了导航栏的情况,我们也要做自定义的动画去干预,因为如果左右两个界面导航栏样式不同,你不去干预的话,push/pop 瞬间导航栏会变成即将显示的那个界面的样式,这不符合预期
 - (BOOL)shouldCustomTransitionAutomaticallyForOperation:(UINavigationControllerOperation)operation firstViewController:(UIViewController *)viewController1 secondViewController:(UIViewController *)viewController2 {
     
     UIViewController<QMUINavigationControllerDelegate> *vc1 = (UIViewController<QMUINavigationControllerDelegate> *)viewController1;

+ 3 - 3
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAlertController.h

@@ -93,7 +93,7 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) {
     UIView          *_scrollWrapView;   // 包含上下两个 scrollView 的容器
     UIScrollView    *_headerScrollView; // 上半部分的内容的 scrollView,例如 title、message
     UIScrollView    *_buttonScrollView; // 所有按钮的容器,特别的,actionSheet 下的取消按钮不放在这里面,因为它不参与滚动
-    UIControl       *_maskView;         // 背后占满整个屏幕的半透明黑色遮罩
+    UIControl       *_dimmingView;      // 背后占满整个屏幕的半透明黑色遮罩
 }
 
 /// alert距离屏幕四边的间距,默认UIEdgeInsetsMake(0, 0, 0, 0)。alert的宽度最终是通过屏幕宽度减去水平的 alertContentMargin 和 alertContentMaximumWidth 决定的。
@@ -278,8 +278,8 @@ typedef NS_ENUM(NSInteger, QMUIAlertControllerStyle) {
  */
 @property(nonatomic, assign) BOOL orderActionsByAddedOrdered;
 
-/// maskView是否响应点击,alert默认为NO,sheet默认为YES
-@property(nonatomic, assign) BOOL shouldRespondMaskViewTouch;
+/// dimmingView 是否响应点击,alert 默认为NO,sheet 默认为YES
+@property(nonatomic, assign) BOOL shouldRespondDimmingViewTouch;
 
 /// 在 iPhoneX 机器上是否延伸底部背景色。因为在 iPhoneX 上我们会把整个面板往上移动 safeArea 的距离,如果你的面板本来就配置成撑满全屏的样式,那么就会露出底部的空隙,isExtendBottomLayout 可以帮助你把空暇填补上。默认为NO。
 /// @warning: 只对 sheet 类型有效

+ 23 - 23
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIAlertController.m

@@ -180,7 +180,7 @@ static NSUInteger alertControllerCount = 0;
 
 @property(nonatomic, strong) UIView *containerView;
 
-@property(nonatomic, strong) UIControl *maskView;
+@property(nonatomic, strong) UIControl *dimmingView;
 
 @property(nonatomic, strong) UIView *scrollWrapView;
 @property(nonatomic, strong) UIScrollView *headerScrollView;
@@ -498,7 +498,7 @@ static NSUInteger alertControllerCount = 0;
         
         self.preferredStyle = preferredStyle;
     
-        self.shouldRespondMaskViewTouch = preferredStyle == QMUIAlertControllerStyleActionSheet;
+        self.shouldRespondDimmingViewTouch = preferredStyle == QMUIAlertControllerStyleActionSheet;
         
         self.alertActions = [[NSMutableArray alloc] init];
         self.alertTextFields = [[NSMutableArray alloc] init];
@@ -520,7 +520,7 @@ static NSUInteger alertControllerCount = 0;
 - (void)viewDidLoad {
     [super viewDidLoad];
     
-    [self.view addSubview:self.maskView];
+    [self.view addSubview:self.dimmingView];
     [self.view addSubview:self.containerView];
     [self.containerView addSubview:self.scrollWrapView];
     [self.scrollWrapView addSubview:self.headerScrollView];
@@ -539,7 +539,7 @@ static NSUInteger alertControllerCount = 0;
     BOOL shouldShowSeparatorAtTopOfButtonAtFirstLine = hasTitle || hasMessage || hasCustomView;
     CGFloat contentOriginY = 0;
     
-    self.maskView.frame = self.view.bounds;
+    self.dimmingView.frame = self.view.bounds;
     
     if (self.preferredStyle == QMUIAlertControllerStyleAlert) {
         
@@ -631,7 +631,7 @@ static NSUInteger alertControllerCount = 0;
         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);
+        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);
@@ -655,7 +655,7 @@ static NSUInteger alertControllerCount = 0;
         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 - self.keyboardHeight) / 2, CGRectGetWidth(self.containerView.frame), CGRectGetHeight(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) {
@@ -759,7 +759,7 @@ static NSUInteger alertControllerCount = 0;
         }
         // 把上下的margin都加上用于跟整个屏幕的高度做比较
         CGFloat contentHeight = contentOriginY + UIEdgeInsetsGetVerticalValue(self.sheetContentMargin);
-        CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds);
+        CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds) - SafeAreaInsetsConstantForDeviceWithNotch.top - (self.isExtendBottomLayout ? 0 : SafeAreaInsetsConstantForDeviceWithNotch.bottom);
         if (contentHeight > screenSpaceHeight) {
             CGFloat cancelButtonAreaHeight = (self.cancelAction ? (CGRectGetHeight(self.cancelAction.button.bounds) + self.sheetCancelButtonMarginTop) : 0);
             screenSpaceHeight = screenSpaceHeight - cancelButtonAreaHeight - UIEdgeInsetsGetVerticalValue(self.sheetContentMargin);
@@ -789,7 +789,7 @@ static NSUInteger alertControllerCount = 0;
             contentHeight -= self.sheetContentMargin.top;
         }
         
-        self.containerView.qmui_frameApplyTransform = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.frame)) / 2, screenSpaceHeight - contentHeight - SafeAreaInsetsConstantForDeviceWithNotch.bottom, CGRectGetWidth(self.containerView.frame), contentHeight + (self.isExtendBottomLayout ? SafeAreaInsetsConstantForDeviceWithNotch.bottom : 0));
+        self.containerView.qmui_frameApplyTransform = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.frame)) / 2, SafeAreaInsetsConstantForDeviceWithNotch.top + screenSpaceHeight - contentHeight, CGRectGetWidth(self.containerView.frame), contentHeight + (self.isExtendBottomLayout ? SafeAreaInsetsConstantForDeviceWithNotch.bottom : 0));
         
         self.extendLayer.frame = CGRectFlatMake(0, CGRectGetHeight(self.containerView.bounds) - SafeAreaInsetsConstantForDeviceWithNotch.bottom - 1, CGRectGetWidth(self.containerView.bounds), SafeAreaInsetsConstantForDeviceWithNotch.bottom + 1);
     }
@@ -845,7 +845,7 @@ static NSUInteger alertControllerCount = 0;
             weakSelf.containerView.alpha = 0;
             weakSelf.containerView.layer.transform = CATransform3DMakeScale(1.2, 1.2, 1.0);
             [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{
-                weakSelf.maskView.alpha = 1;
+                weakSelf.dimmingView.alpha = 1;
                 weakSelf.containerView.alpha = 1;
                 weakSelf.containerView.layer.transform = CATransform3DMakeScale(1.0, 1.0, 1.0);
             } completion:^(BOOL finished) {
@@ -856,7 +856,7 @@ static NSUInteger alertControllerCount = 0;
         } else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) {
             weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, CGRectGetHeight(weakSelf.view.bounds) - CGRectGetMinY(weakSelf.containerView.frame), 0);
             [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{
-                weakSelf.maskView.alpha = 1;
+                weakSelf.dimmingView.alpha = 1;
                 weakSelf.containerView.layer.transform = CATransform3DIdentity;
             } completion:^(BOOL finished) {
                 if (completion) {
@@ -869,7 +869,7 @@ static NSUInteger alertControllerCount = 0;
     self.modalPresentationViewController.hidingAnimation = ^(UIView *dimmingView, CGRect containerBounds, CGFloat keyboardHeight, void(^completion)(BOOL finished)) {
         if (self.preferredStyle == QMUIAlertControllerStyleAlert) {
             [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{
-                weakSelf.maskView.alpha = 0;
+                weakSelf.dimmingView.alpha = 0;
                 weakSelf.containerView.alpha = 0;
             } completion:^(BOOL finished) {
                 weakSelf.containerView.alpha = 1;
@@ -879,7 +879,7 @@ static NSUInteger alertControllerCount = 0;
             }];
         } else if (self.preferredStyle == QMUIAlertControllerStyleActionSheet) {
             [UIView animateWithDuration:0.25f delay:0 options:QMUIViewAnimationOptionsCurveOut animations:^{
-                weakSelf.maskView.alpha = 0;
+                weakSelf.dimmingView.alpha = 0;
                 weakSelf.containerView.layer.transform = CATransform3DMakeTranslation(0, CGRectGetHeight(weakSelf.view.bounds) - CGRectGetMinY(weakSelf.containerView.frame), 0);
             } completion:^(BOOL finished) {
                 if (completion) {
@@ -919,7 +919,7 @@ static NSUInteger alertControllerCount = 0;
     __weak __typeof(self)weakSelf = self;
     
     [self.modalPresentationViewController showWithAnimated:animated completion:^(BOOL finished) {
-        weakSelf.maskView.alpha = 1;
+        weakSelf.dimmingView.alpha = 1;
         weakSelf.willShow = NO;
         weakSelf.showing = YES;
         if (weakSelf.isNeedsHideAfterAlertShowed) {
@@ -963,7 +963,7 @@ static NSUInteger alertControllerCount = 0;
         weakSelf.modalPresentationViewController = nil;
         weakSelf.willShow = NO;
         weakSelf.showing = NO;
-        weakSelf.maskView.alpha = 0;
+        weakSelf.dimmingView.alpha = 0;
         if (self.preferredStyle == QMUIAlertControllerStyleAlert) {
             weakSelf.containerView.alpha = 0;
         } else {
@@ -1166,22 +1166,22 @@ static NSUInteger alertControllerCount = 0;
     return [self.alertTextFields copy];
 }
 
-- (void)handleMaskViewEvent:(id)sender {
-    if (_shouldRespondMaskViewTouch) {
+- (void)handleDimmingViewEvent:(id)sender {
+    if (_shouldRespondDimmingViewTouch) {
         [self hideWithAnimated:YES completion:NULL];
     }
 }
 
 #pragma mark - Getters & Setters
 
-- (UIControl *)maskView {
-    if (!_maskView) {
-        _maskView = [[UIControl alloc] init];
-        _maskView.alpha = 0;
-        _maskView.backgroundColor = UIColorMask;
-        [_maskView addTarget:self action:@selector(handleMaskViewEvent:) forControlEvents:UIControlEventTouchUpInside];
+- (UIControl *)dimmingView {
+    if (!_dimmingView) {
+        _dimmingView = [[UIControl alloc] init];
+        _dimmingView.alpha = 0;
+        _dimmingView.backgroundColor = UIColorMask;
+        [_dimmingView addTarget:self action:@selector(handleDimmingViewEvent:) forControlEvents:UIControlEventTouchUpInside];
     }
-    return _maskView;
+    return _dimmingView;
 }
 
 - (UIView *)containerView {

+ 18 - 14
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIBadge/QMUIBadgeProtocol.h

@@ -16,28 +16,32 @@
 #import <Foundation/Foundation.h>
 #import <UIKit/UIKit.h>
 
-// TODO: molice 等废弃 qmui_badgeCenterOffset 系列接口后再删除
-#import "QMUICore.h"
-
 NS_ASSUME_NONNULL_BEGIN
 
-@class QMUILabel;
-
 @protocol QMUIBadgeProtocol <NSObject>
 
 #pragma mark - Badge
 
-/// 用数字设置未读数,0表示不显示未读数
+/// 用数字设置未读数,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 正值表示向下。
@@ -47,11 +51,11 @@ NS_ASSUME_NONNULL_BEGIN
 /// 横屏下使用,其他同 @c qmui_badgeOffset 。
 @property(nonatomic, assign) CGPoint qmui_badgeOffsetLandscape;
 
-/// 在这两个属性被删除之前,如果不主动设置 @c qmui_badgeOffset 和 @c qmui_badgeOffsetLandscape ,则依然使用旧的逻辑,一旦设置过两个新属性,则旧属性会失效。
-@property(nonatomic, assign) CGPoint qmui_badgeCenterOffset DEPRECATED_MSG_ATTRIBUTE("QMUIBadge 不再以中心为布局参考点,请改为使用 qmui_badgeOffset");
-@property(nonatomic, assign) CGPoint qmui_badgeCenterOffsetLandscape DEPRECATED_MSG_ATTRIBUTE("QMUIBadge 不再以中心为布局参考点,请改为使用 qmui_badgeOffsetLandscape");
+/// 未读数的 view,默认是 QMUIBadgeLabel,也可设置为自定义的 view。自定义 view 如果是 UILabel 类型则内部会自动为其设置 text、textColor,但如果是其他类型的 view 则需要业务自行处理。
+@property(nonatomic, strong, nullable) __kindof UIView *qmui_badgeView;
 
-@property(nonatomic, strong, readonly, nullable) QMUILabel *qmui_badgeLabel;
+/// badgeView 布局完成后的回调。因为 badgeView 必定在当前 view 的 layoutSubviews 执行完之后才布局,所以业务很难在自己的 layoutSubviews 里重新调整 badgeView 的布局,所以提供一个 block。
+@property(nonatomic, copy, nullable) void (^qmui_badgeViewDidLayoutBlock)(__kindof UIView *aView, __kindof UIView *aBadgeView);
 
 
 #pragma mark - UpdatesIndicator
@@ -68,11 +72,11 @@ NS_ASSUME_NONNULL_BEGIN
 /// 横屏下使用,其他同 @c qmui_updatesIndicatorOffset 。
 @property(nonatomic, assign) CGPoint qmui_updatesIndicatorOffsetLandscape;
 
-/// 在这两个属性被删除之前,如果不主动设置 @c qmui_updatesIndicatorOffset 和 @c qmui_updatesIndicatorOffsetLandscape ,则依然使用旧的逻辑,一旦设置过两个新属性,则旧属性会失效。
-@property(nonatomic, assign) CGPoint qmui_updatesIndicatorCenterOffset DEPRECATED_MSG_ATTRIBUTE("QMUIBadge 不再以中心为布局参考点,请改为使用 qmui_updatesIndicatorOffset");
-@property(nonatomic, assign) CGPoint qmui_updatesIndicatorCenterOffsetLandscape DEPRECATED_MSG_ATTRIBUTE("QMUIBadge 不再以中心为布局参考点,请改为使用 qmui_updatesIndicatorOffsetLandscape");
+/// 未读红点的 view,支持设置为自定义 view。
+@property(nonatomic, strong, nullable) __kindof UIView *qmui_updatesIndicatorView;
 
-@property(nonatomic, strong, readonly, nullable) UIView *qmui_updatesIndicatorView;
+/// updatesIndicatorView 布局完成后的回调。因为 updatesIndicatorView 必定在当前 view 的 layoutSubviews 执行完之后才布局,所以业务很难在自己的 layoutSubviews 里重新调整 updatesIndicatorView 的布局,所以提供一个 block。
+@property(nonatomic, copy, nullable) void (^qmui_updatesIndicatorViewDidLayoutBlock)(__kindof UIView *aView, __kindof UIView *aUpdatesIndicatorView);
 
 @end
 

+ 16 - 58
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIBadge/UIBarItem+QMUIBadge.m

@@ -65,13 +65,6 @@
         self.qmui_updatesIndicatorSize = UpdatesIndicatorSize;
         self.qmui_updatesIndicatorOffset = UpdatesIndicatorOffset;
         self.qmui_updatesIndicatorOffsetLandscape = UpdatesIndicatorOffsetLandscape;
-        
-        BeginIgnoreDeprecatedWarning
-        self.qmui_badgeCenterOffset = BadgeCenterOffset;
-        self.qmui_badgeCenterOffsetLandscape = BadgeCenterOffsetLandscape;
-        self.qmui_updatesIndicatorCenterOffset = UpdatesIndicatorCenterOffset;
-        self.qmui_updatesIndicatorCenterOffsetLandscape = UpdatesIndicatorCenterOffsetLandscape;
-        EndIgnoreClangWarning
     }
 }
 
@@ -160,34 +153,20 @@ static char kAssociatedObjectKey_badgeOffsetLandscape;
     return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape)) CGPointValue];
 }
 
-BeginIgnoreDeprecatedWarning
-BeginIgnoreClangWarning(-Wdeprecated-implementations)
-
-static char kAssociatedObjectKey_badgeCenterOffset;
-- (void)setQmui_badgeCenterOffset:(CGPoint)qmui_badgeCenterOffset {
-    objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffset, [NSValue valueWithCGPoint:qmui_badgeCenterOffset], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    self.qmui_view.qmui_badgeCenterOffset = qmui_badgeCenterOffset;
+- (void)setQmui_badgeView:(__kindof UIView *)qmui_badgeView {
+    self.qmui_view.qmui_badgeView = qmui_badgeView;
 }
 
-- (CGPoint)qmui_badgeCenterOffset {
-    return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffset)) CGPointValue];
+- (__kindof UIView *)qmui_badgeView {
+    return self.qmui_view.qmui_badgeView;
 }
 
-static char kAssociatedObjectKey_badgeCenterOffsetLandscape;
-- (void)setQmui_badgeCenterOffsetLandscape:(CGPoint)qmui_badgeCenterOffsetLandscape {
-    objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffsetLandscape, [NSValue valueWithCGPoint:qmui_badgeCenterOffsetLandscape], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    self.qmui_view.qmui_badgeCenterOffsetLandscape = qmui_badgeCenterOffsetLandscape;
+- (void)setQmui_badgeViewDidLayoutBlock:(void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_badgeViewDidLayoutBlock {
+    self.qmui_view.qmui_badgeViewDidLayoutBlock = qmui_badgeViewDidLayoutBlock;
 }
 
-- (CGPoint)qmui_badgeCenterOffsetLandscape {
-    return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffsetLandscape)) CGPointValue];
-}
-
-EndIgnoreClangWarning
-EndIgnoreDeprecatedWarning
-
-- (QMUILabel *)qmui_badgeLabel {
-    return self.qmui_view.qmui_badgeLabel;
+- (void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_badgeViewDidLayoutBlock {
+    return self.qmui_view.qmui_badgeViewDidLayoutBlock;
 }
 
 #pragma mark - UpdatesIndicator
@@ -245,34 +224,20 @@ static char kAssociatedObjectKey_updatesIndicatorOffsetLandscape;
     return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape)) CGPointValue];
 }
 
-BeginIgnoreDeprecatedWarning
-BeginIgnoreClangWarning(-Wdeprecated-implementations)
-
-static char kAssociatedObjectKey_updatesIndicatorCenterOffset;
-- (void)setQmui_updatesIndicatorCenterOffset:(CGPoint)qmui_updatesIndicatorCenterOffset {
-    objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffset, [NSValue valueWithCGPoint:qmui_updatesIndicatorCenterOffset], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    self.qmui_view.qmui_updatesIndicatorCenterOffset = qmui_updatesIndicatorCenterOffset;
-}
-
-- (CGPoint)qmui_updatesIndicatorCenterOffset {
-    return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffset)) CGPointValue];
+- (void)setQmui_updatesIndicatorView:(__kindof UIView *)qmui_updatesIndicatorView {
+    self.qmui_view.qmui_updatesIndicatorView = qmui_updatesIndicatorView;
 }
 
-static char kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape;
-- (void)setQmui_updatesIndicatorCenterOffsetLandscape:(CGPoint)qmui_updatesIndicatorCenterOffsetLandscape {
-    objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape, [NSValue valueWithCGPoint:qmui_updatesIndicatorCenterOffsetLandscape], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    self.qmui_view.qmui_updatesIndicatorCenterOffsetLandscape = qmui_updatesIndicatorCenterOffsetLandscape;
+- (UIView *)qmui_updatesIndicatorView {
+    return self.qmui_view.qmui_updatesIndicatorView;
 }
 
-- (CGPoint)qmui_updatesIndicatorCenterOffsetLandscape {
-    return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape)) CGPointValue];
+- (void)setQmui_updatesIndicatorViewDidLayoutBlock:(void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_updatesIndicatorViewDidLayoutBlock {
+    self.qmui_view.qmui_updatesIndicatorViewDidLayoutBlock = qmui_updatesIndicatorViewDidLayoutBlock;
 }
 
-EndIgnoreClangWarning
-EndIgnoreDeprecatedWarning
-
-- (UIView *)qmui_updatesIndicatorView {
-    return self.qmui_view.qmui_updatesIndicatorView;
+- (void (^)(__kindof UIView * _Nonnull, __kindof UIView * _Nonnull))qmui_updatesIndicatorViewDidLayoutBlock {
+    return self.qmui_view.qmui_updatesIndicatorViewDidLayoutBlock;
 }
 
 #pragma mark - Common
@@ -292,13 +257,6 @@ EndIgnoreDeprecatedWarning
             view.qmui_updatesIndicatorOffset = item.qmui_updatesIndicatorOffset;
             view.qmui_updatesIndicatorOffsetLandscape = item.qmui_updatesIndicatorOffsetLandscape;
             
-            BeginIgnoreDeprecatedWarning
-            view.qmui_badgeCenterOffset = item.qmui_badgeCenterOffset;
-            view.qmui_badgeCenterOffsetLandscape = item.qmui_badgeCenterOffsetLandscape;
-            view.qmui_updatesIndicatorCenterOffset = item.qmui_updatesIndicatorCenterOffset;
-            view.qmui_updatesIndicatorCenterOffsetLandscape = item.qmui_updatesIndicatorCenterOffsetLandscape;
-            EndIgnoreDeprecatedWarning
-            
             view.qmui_badgeString = item.qmui_badgeString;
             view.qmui_shouldShowUpdatesIndicator = item.qmui_shouldShowUpdatesIndicator;
         };

+ 72 - 238
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m

@@ -18,34 +18,17 @@
 #import "QMUILabel.h"
 #import "UIView+QMUI.h"
 #import "UITabBarItem+QMUI.h"
-
-@protocol _QMUIBadgeViewProtocol <NSObject>
-
-@required
-
-@property(nonatomic, assign) CGPoint offset;
-@property(nonatomic, assign) CGPoint offsetLandscape;
-@property(nonatomic, assign) CGPoint centerOffset;
-@property(nonatomic, assign) CGPoint centerOffsetLandscape;
-
-@end
-
-@interface _QMUIBadgeLabel : QMUILabel <_QMUIBadgeViewProtocol>
-@end
-
-@interface _QMUIUpdatesIndicatorView : UIView <_QMUIBadgeViewProtocol>
-@end
+#import "QMUIBadgeLabel.h"
 
 @interface UIView ()
-
-@property(nonatomic, strong, readwrite) _QMUIBadgeLabel *qmui_badgeLabel;
-@property(nonatomic, strong, readwrite) _QMUIUpdatesIndicatorView *qmui_updatesIndicatorView;
 @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;
@@ -93,13 +76,6 @@ QMUISynthesizeIdStrongProperty(qmuibdg_layoutSubviewsBlock, setQmuibdg_layoutSub
         self.qmui_updatesIndicatorSize = UpdatesIndicatorSize;
         self.qmui_updatesIndicatorOffset = UpdatesIndicatorOffset;
         self.qmui_updatesIndicatorOffsetLandscape = UpdatesIndicatorOffsetLandscape;
-        
-        BeginIgnoreDeprecatedWarning
-        self.qmui_badgeCenterOffset = BadgeCenterOffset;
-        self.qmui_badgeCenterOffsetLandscape = BadgeCenterOffsetLandscape;
-        self.qmui_updatesIndicatorCenterOffset = UpdatesIndicatorCenterOffset;
-        self.qmui_updatesIndicatorCenterOffsetLandscape = UpdatesIndicatorCenterOffsetLandscape;
-        EndIgnoreDeprecatedWarning
     }
 }
 
@@ -119,30 +95,23 @@ 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_badgeLabel) {
-            self.qmui_badgeLabel = [[_QMUIBadgeLabel alloc] init];
-            self.qmui_badgeLabel.clipsToBounds = YES;
-            self.qmui_badgeLabel.textAlignment = NSTextAlignmentCenter;
-            self.qmui_badgeLabel.backgroundColor = self.qmui_badgeBackgroundColor;
-            self.qmui_badgeLabel.textColor = self.qmui_badgeTextColor;
-            self.qmui_badgeLabel.font = self.qmui_badgeFont;
-            self.qmui_badgeLabel.contentEdgeInsets = self.qmui_badgeContentEdgeInsets;
-            self.qmui_badgeLabel.offset = self.qmui_badgeOffset;
-            self.qmui_badgeLabel.offsetLandscape = self.qmui_badgeOffsetLandscape;
-            BeginIgnoreDeprecatedWarning
-            self.qmui_badgeLabel.centerOffset = self.qmui_badgeCenterOffset;
-            self.qmui_badgeLabel.centerOffsetLandscape = self.qmui_badgeCenterOffsetLandscape;
-            EndIgnoreDeprecatedWarning
-            [self addSubview:self.qmui_badgeLabel];
-            
-            [self updateLayoutSubviewsBlockIfNeeded];
+        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_badgeLabel.text = qmui_badgeString;
-        self.qmui_badgeLabel.hidden = NO;
+        self.qmui_badgeView.hidden = NO;
         [self setNeedsUpdateBadgeLabelLayout];
+        QMUIAssert(!self.clipsToBounds, @"QMUIBadge", @"clipsToBounds should be NO when showing badgeString");
         self.clipsToBounds = NO;
     } else {
-        self.qmui_badgeLabel.hidden = YES;
+        self.qmui_badgeView.hidden = YES;
     }
 }
 
@@ -153,7 +122,7 @@ static char 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_badgeLabel.backgroundColor = qmui_badgeBackgroundColor;
+    self.qmui_badgeView.backgroundColor = qmui_badgeBackgroundColor;
 }
 
 - (UIColor *)qmui_badgeBackgroundColor {
@@ -163,7 +132,9 @@ static char 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_badgeLabel.textColor = qmui_badgeTextColor;
+    if ([self.qmui_badgeView isKindOfClass:UILabel.class]) {
+        ((UILabel *)self.qmui_badgeView).textColor = qmui_badgeTextColor;
+    }
 }
 
 - (UIColor *)qmui_badgeTextColor {
@@ -173,8 +144,8 @@ static char 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_badgeLabel) {
-        self.qmui_badgeLabel.font = qmui_badgeFont;
+    if ([self.qmui_badgeView isKindOfClass:UILabel.class]) {
+        ((UILabel *)self.qmui_badgeView).font = qmui_badgeFont;
         [self setNeedsUpdateBadgeLabelLayout];
     }
 }
@@ -186,8 +157,8 @@ static char 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_badgeLabel) {
-        self.qmui_badgeLabel.contentEdgeInsets = qmui_badgeContentEdgeInsets;
+    if ([self.qmui_badgeView isKindOfClass:QMUILabel.class]) {
+        ((QMUILabel *)self.qmui_badgeView).contentEdgeInsets = qmui_badgeContentEdgeInsets;
         [self setNeedsUpdateBadgeLabelLayout];
     }
 }
@@ -199,7 +170,7 @@ static char kAssociatedObjectKey_badgeContentEdgeInsets;
 static char kAssociatedObjectKey_badgeOffset;
 - (void)setQmui_badgeOffset:(CGPoint)qmui_badgeOffset {
     objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffset, @(qmui_badgeOffset), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    self.qmui_badgeLabel.offset = qmui_badgeOffset;
+    [self setNeedsUpdateBadgeLabelLayout];
 }
 
 - (CGPoint)qmui_badgeOffset {
@@ -209,51 +180,30 @@ static char kAssociatedObjectKey_badgeOffset;
 static char kAssociatedObjectKey_badgeOffsetLandscape;
 - (void)setQmui_badgeOffsetLandscape:(CGPoint)qmui_badgeOffsetLandscape {
     objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape, @(qmui_badgeOffsetLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    self.qmui_badgeLabel.offsetLandscape = qmui_badgeOffsetLandscape;
+    [self setNeedsUpdateBadgeLabelLayout];
 }
 
 - (CGPoint)qmui_badgeOffsetLandscape {
     return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeOffsetLandscape)) CGPointValue];
 }
 
-BeginIgnoreDeprecatedWarning
-BeginIgnoreClangWarning(-Wdeprecated-implementations)
-
-static char kAssociatedObjectKey_badgeCenterOffset;
-- (void)setQmui_badgeCenterOffset:(CGPoint)qmui_badgeCenterOffset {
-    objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffset, [NSValue valueWithCGPoint:qmui_badgeCenterOffset], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    self.qmui_badgeLabel.centerOffset = qmui_badgeCenterOffset;
-}
-
-- (CGPoint)qmui_badgeCenterOffset {
-    return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffset)) CGPointValue];
-}
-
-static char kAssociatedObjectKey_badgeCenterOffsetLandscape;
-- (void)setQmui_badgeCenterOffsetLandscape:(CGPoint)qmui_badgeCenterOffsetLandscape {
-    objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffsetLandscape, [NSValue valueWithCGPoint:qmui_badgeCenterOffsetLandscape], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    self.qmui_badgeLabel.centerOffsetLandscape = qmui_badgeCenterOffsetLandscape;
-}
-
-- (CGPoint)qmui_badgeCenterOffsetLandscape {
-    return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeCenterOffsetLandscape)) CGPointValue];
-}
-
-EndIgnoreClangWarning
-EndIgnoreDeprecatedWarning
-
-static char kAssociatedObjectKey_badgeLabel;
-- (void)setQmui_badgeLabel:(UILabel *)qmui_badgeLabel {
-    objc_setAssociatedObject(self, &kAssociatedObjectKey_badgeLabel, qmui_badgeLabel, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+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];
+    }
 }
 
-- (_QMUIBadgeLabel *)qmui_badgeLabel {
-    return (_QMUIBadgeLabel *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeLabel);
+- (__kindof UIView *)qmui_badgeView {
+    return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_badgeView);
 }
 
 - (void)setNeedsUpdateBadgeLabelLayout {
-    if (self.qmui_badgeString.length) {
-        [self setNeedsLayout];
+    if (self.qmui_badgeView && !self.qmui_badgeView.hidden) {
+        [self qmuibdg_layoutSubviews];
     }
 }
 
@@ -264,19 +214,12 @@ static char kAssociatedObjectKey_shouldShowUpdatesIndicator;
     objc_setAssociatedObject(self, &kAssociatedObjectKey_shouldShowUpdatesIndicator, @(qmui_shouldShowUpdatesIndicator), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
     if (qmui_shouldShowUpdatesIndicator) {
         if (!self.qmui_updatesIndicatorView) {
-            self.qmui_updatesIndicatorView = [[_QMUIUpdatesIndicatorView alloc] qmui_initWithSize:self.qmui_updatesIndicatorSize];
+            self.qmui_updatesIndicatorView = [[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.qmui_updatesIndicatorView.offset = self.qmui_updatesIndicatorOffset;
-            self.qmui_updatesIndicatorView.offsetLandscape = self.qmui_updatesIndicatorOffsetLandscape;
-            BeginIgnoreDeprecatedWarning
-            self.qmui_updatesIndicatorView.centerOffset = self.qmui_updatesIndicatorCenterOffset;
-            self.qmui_updatesIndicatorView.centerOffsetLandscape = self.qmui_updatesIndicatorCenterOffsetLandscape;
-            EndIgnoreDeprecatedWarning
-            [self addSubview:self.qmui_updatesIndicatorView];
-            [self updateLayoutSubviewsBlockIfNeeded];
         }
         [self setNeedsUpdateIndicatorLayout];
+        QMUIAssert(!self.clipsToBounds, @"QMUIBadge", @"clipsToBounds should be NO when showing updatesIndicator");
         self.clipsToBounds = NO;
         self.qmui_updatesIndicatorView.hidden = NO;
     } else {
@@ -316,7 +259,6 @@ 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.qmui_updatesIndicatorView.offset = qmui_updatesIndicatorOffset;
         [self setNeedsUpdateIndicatorLayout];
     }
 }
@@ -329,7 +271,6 @@ 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.qmui_updatesIndicatorView.offsetLandscape = qmui_updatesIndicatorOffsetLandscape;
         [self setNeedsUpdateIndicatorLayout];
     }
 }
@@ -338,50 +279,23 @@ static char kAssociatedObjectKey_updatesIndicatorOffsetLandscape;
     return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorOffsetLandscape)) CGPointValue];
 }
 
-BeginIgnoreDeprecatedWarning
-BeginIgnoreClangWarning(-Wdeprecated-implementations)
-
-static char kAssociatedObjectKey_updatesIndicatorCenterOffset;
-- (void)setQmui_updatesIndicatorCenterOffset:(CGPoint)qmui_updatesIndicatorCenterOffset {
-    objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffset, [NSValue valueWithCGPoint:qmui_updatesIndicatorCenterOffset], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    if (self.qmui_updatesIndicatorView) {
-        self.qmui_updatesIndicatorView.centerOffset = qmui_updatesIndicatorCenterOffset;
-        [self setNeedsUpdateIndicatorLayout];
-    }
-}
-
-- (CGPoint)qmui_updatesIndicatorCenterOffset {
-    return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffset)) CGPointValue];
-}
-
-static char kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape;
-- (void)setQmui_updatesIndicatorCenterOffsetLandscape:(CGPoint)qmui_updatesIndicatorCenterOffsetLandscape {
-    objc_setAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape, [NSValue valueWithCGPoint:qmui_updatesIndicatorCenterOffsetLandscape], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    if (self.qmui_updatesIndicatorView) {
-        self.qmui_updatesIndicatorView.centerOffsetLandscape = qmui_updatesIndicatorCenterOffsetLandscape;
-        [self setNeedsUpdateIndicatorLayout];
-    }
-}
-
-- (CGPoint)qmui_updatesIndicatorCenterOffsetLandscape {
-    return [((NSValue *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorCenterOffsetLandscape)) CGPointValue];
-}
-
-EndIgnoreClangWarning
-EndIgnoreDeprecatedWarning
-
 static char kAssociatedObjectKey_updatesIndicatorView;
-- (void)setQmui_updatesIndicatorView:(UIView *)qmui_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];
+    }
 }
 
-- (_QMUIUpdatesIndicatorView *)qmui_updatesIndicatorView {
-    return (_QMUIUpdatesIndicatorView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorView);
+- (__kindof UIView *)qmui_updatesIndicatorView {
+    return (UIView *)objc_getAssociatedObject(self, &kAssociatedObjectKey_updatesIndicatorView);
 }
 
 - (void)setNeedsUpdateIndicatorLayout {
     if (self.qmui_shouldShowUpdatesIndicator) {
-        [self setNeedsLayout];
+        [self qmuibdg_layoutSubviews];
     }
 }
 
@@ -405,7 +319,8 @@ static char kAssociatedObjectKey_updatesIndicatorView;
     }
 }
 
-- (UIView *)findBarButtonImageViewIfOffsetByTopRight:(BOOL)offsetByTopRight {
+// 不管 image 还是 text 的 UIBarButtonItem 都获取内部的 _UIModernBarButton 即可
+- (UIView *)findBarButtonContentView {
     NSString *classString = NSStringFromClass(self.class);
     if ([classString isEqualToString:@"UITabBarButton"]) {
         // 特别的,对于 UITabBarItem,将 imageView 作为参考 view
@@ -413,17 +328,10 @@ static char kAssociatedObjectKey_updatesIndicatorView;
         return imageView;
     }
     
-    // 如果使用 centerOffset 则不特殊处理 UIBarButtonItem,以保持与旧版的逻辑一致
-    // TODO: molice 等废弃 qmui_badgeCenterOffset 系列接口后再删除
-    if (!offsetByTopRight) return nil;
-    
     if ([classString isEqualToString:@"_UIButtonBarButton"]) {
         for (UIView *subview in self.subviews) {
             if ([subview isKindOfClass:UIButton.class]) {
-                UIView *imageView = ((UIButton *)subview).imageView;
-                if (imageView && !imageView.hidden) {
-                    return imageView;
-                }
+                return subview;
             }
         }
     }
@@ -433,110 +341,36 @@ static char kAssociatedObjectKey_updatesIndicatorView;
 
 - (void)qmuibdg_layoutSubviews {
     
-    void (^layoutBlock)(UIView *view, UIView<_QMUIBadgeViewProtocol> *badgeView) = ^void(UIView *view, UIView<_QMUIBadgeViewProtocol> *badgeView) {
-        BOOL offsetByTopRight = !CGPointEqualToPoint(badgeView.offset, QMUIBadgeInvalidateOffset) || !CGPointEqualToPoint(badgeView.offsetLandscape, QMUIBadgeInvalidateOffset);
-        CGPoint offset = IS_LANDSCAPE ? (offsetByTopRight ? badgeView.offsetLandscape : badgeView.centerOffsetLandscape) : (offsetByTopRight ? badgeView.offset : badgeView.centerOffset);
+    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 *imageView = [view findBarButtonImageViewIfOffsetByTopRight:offsetByTopRight];
-        if (imageView) {
-            CGRect imageViewFrame = [view convertRect:imageView.frame fromView:imageView.superview];
-            if (offsetByTopRight) {
-                badgeView.frame = CGRectSetXY(badgeView.frame, CGRectGetMaxX(imageViewFrame) + offset.x, CGRectGetMinY(imageViewFrame) - CGRectGetHeight(badgeView.frame) + offset.y);
-            } else {
-                badgeView.center = CGPointMake(CGRectGetMidX(imageViewFrame) + offset.x, CGRectGetMidY(imageViewFrame) + offset.y);
-            }
+        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 {
-            if (offsetByTopRight) {
-                badgeView.frame = CGRectSetXY(badgeView.frame, CGRectGetWidth(view.bounds) + offset.x, - CGRectGetHeight(badgeView.frame) + offset.y);
-            } else {
-                badgeView.center = CGPointMake(CGRectGetMidX(view.bounds) + offset.x, CGRectGetMidY(view.bounds) + offset.y);
-            }
+            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_badgeLabel && !self.qmui_badgeLabel.hidden) {
-        [self.qmui_badgeLabel sizeToFit];
-        self.qmui_badgeLabel.layer.cornerRadius = MIN(self.qmui_badgeLabel.qmui_height / 2, self.qmui_badgeLabel.qmui_width / 2);
-        layoutBlock(self, self.qmui_badgeLabel);
-    }
-}
-
-@end
-
-@implementation _QMUIUpdatesIndicatorView
-
-@synthesize offset = _offset, offsetLandscape = _offsetLandscape, centerOffset = _centerOffset, centerOffsetLandscape = _centerOffsetLandscape;
-
-- (void)setOffset:(CGPoint)offset {
-    _offset = offset;
-    if (!IS_LANDSCAPE) {
-        [self.superview setNeedsLayout];
-    }
-}
-
-- (void)setOffsetLandscape:(CGPoint)offsetLandscape {
-    _offsetLandscape = offsetLandscape;
-    if (IS_LANDSCAPE) {
-        [self.superview setNeedsLayout];
-    }
-}
-
-- (void)setCenterOffset:(CGPoint)centerOffset {
-    _centerOffset = centerOffset;
-    if (!IS_LANDSCAPE) {
-        [self.superview setNeedsLayout];
-    }
-}
-
-- (void)setCenterOffsetLandscape:(CGPoint)centerOffsetLandscape {
-    _centerOffsetLandscape = centerOffsetLandscape;
-    if (IS_LANDSCAPE) {
-        [self.superview setNeedsLayout];
-    }
-}
-
-@end
-
-@implementation _QMUIBadgeLabel
-
-@synthesize offset = _offset, offsetLandscape = _offsetLandscape, centerOffset = _centerOffset, centerOffsetLandscape = _centerOffsetLandscape;
-
-- (void)setOffset:(CGPoint)offset {
-    _offset = offset;
-    if (!IS_LANDSCAPE) {
-        [self.superview setNeedsLayout];
-    }
-}
-
-- (void)setOffsetLandscape:(CGPoint)offsetLandscape {
-    _offsetLandscape = offsetLandscape;
-    if (IS_LANDSCAPE) {
-        [self.superview setNeedsLayout];
-    }
-}
-
-- (void)setCenterOffset:(CGPoint)centerOffset {
-    _centerOffset = centerOffset;
-    if (!IS_LANDSCAPE) {
-        [self.superview setNeedsLayout];
-    }
-}
-
-- (void)setCenterOffsetLandscape:(CGPoint)centerOffsetLandscape {
-    _centerOffsetLandscape = centerOffsetLandscape;
-    if (IS_LANDSCAPE) {
-        [self.superview setNeedsLayout];
+    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);
+        }
     }
 }
 
-- (CGSize)sizeThatFits:(CGSize)size {
-    CGSize result = [super sizeThatFits:size];
-    result = CGSizeMake(MAX(result.width, result.height), result.height);
-    return result;
-}
-
 @end

+ 5 - 0
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.h

@@ -48,6 +48,11 @@ extern const CGFloat QMUIButtonCornerRadiusAdjustsBounds;
  */
 - (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是不跟随的)<br/>
  * 默认为NO

+ 155 - 352
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m

@@ -17,6 +17,7 @@
 #import "QMUICore.h"
 #import "CALayer+QMUI.h"
 #import "UIButton+QMUI.h"
+#import "QMUILayouter.h"
 
 const CGFloat QMUIButtonCornerRadiusAdjustsBounds = -1;
 
@@ -28,10 +29,13 @@ const CGFloat QMUIButtonCornerRadiusAdjustsBounds = -1;
 
 @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);
@@ -58,6 +62,33 @@ const CGFloat QMUIButtonCornerRadiusAdjustsBounds = -1;
     
     // 图片默认在按钮左边,与系统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 里应该用这个方法
@@ -68,72 +99,116 @@ const CGFloat QMUIButtonCornerRadiusAdjustsBounds = -1;
     EndIgnorePerformSelectorLeaksWarning
 }
 
-- (CGSize)sizeThatFits:(CGSize)size {
-    // 如果调用 sizeToFit,那么传进来的 size 就是当前按钮的 size,此时的计算不要去限制宽高
-    // 系统 UIButton 不管任何时候,对 sizeThatFits:CGSizeZero 都会返回真实的内容大小,这里对齐
-    if (CGSizeEqualToSize(self.bounds.size, size) || CGSizeIsEmpty(size)) {
-        size = CGSizeMax;
-    }
+- (QMUILayouterItem *)generateLayouterForLayout:(BOOL)forLayout {
+    __weak __typeof(self)weakSelf = self;
+    
+    QMUILayouterAlignment horizontal = [@[
+        @(QMUILayouterAlignmentCenter),
+        @(QMUILayouterAlignmentLeading),
+        @(QMUILayouterAlignmentTrailing),
+        @(QMUILayouterAlignmentFill),
+        @(QMUILayouterAlignmentLeading),
+        @(QMUILayouterAlignmentTrailing),
+    ][self.contentHorizontalAlignment] integerValue];
+    QMUILayouterAlignment vertical = [@[
+        @(QMUILayouterAlignmentCenter),
+        @(QMUILayouterAlignmentLeading),
+        @(QMUILayouterAlignmentTrailing),
+        @(QMUILayouterAlignmentFill),
+    ][self.contentVerticalAlignment] integerValue];
     
     BOOL isImageViewShowing = !!self.currentImage;
-    BOOL isTitleLabelShowing = !!self.currentTitle || self.currentAttributedTitle;
-    CGSize imageTotalSize = CGSizeZero;// 包含 imageEdgeInsets 那些空间
-    CGSize titleTotalSize = CGSizeZero;// 包含 titleEdgeInsets 那些空间
-    CGFloat spacingBetweenImageAndTitle = flat(isImageViewShowing && isTitleLabelShowing ? self.spacingBetweenImageAndTitle : 0);// 如果图片或文字某一者没显示,则这个 spacing 不考虑进布局
-    UIEdgeInsets contentEdgeInsets = UIEdgeInsetsRemoveFloatMin(self.contentEdgeInsets);
-    CGSize resultSize = CGSizeZero;
-    CGSize contentLimitSize = CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(contentEdgeInsets), size.height - UIEdgeInsetsGetVerticalValue(contentEdgeInsets));
+    QMUILayouterItem *image = [QMUILayouterItem itemWithView:isImageViewShowing ? (forLayout ? self._qmui_imageView : self.imageView) : nil margin:self.imageEdgeInsets];
+    image.visibleBlock = ^BOOL(QMUILayouterItem * _Nonnull aItem) {
+        return !!weakSelf.currentImage;
+    };
+    image.sizeThatFitsBlock = ^CGSize(QMUILayouterItem * _Nonnull aItem, CGSize size, CGSize superResult) {
+        // 某些时机下存在 image 但 imageView.image 尚为 nil 导致计算出来的尺寸错误,所以这里做个保护(ed4d87e86af12110b2c14359ef287be959c70af0)
+        if (aItem.visible && CGSizeIsEmpty(superResult) && [aItem.view.superview isKindOfClass:QMUIButton.class]) {
+            QMUIButton *btn = (QMUIButton *)aItem.view.superview;
+            return btn.currentImage.size;
+        }
+        return superResult;
+    };
+    QMUILayouterItem *title = [QMUILayouterItem itemWithView:self.titleLabel margin:self.titleEdgeInsets grow:QMUILayouterGrowNever shrink:QMUILayouterShrinkDefault];
+    title.visibleBlock = ^BOOL(QMUILayouterItem * _Nonnull aItem) {
+        return !!weakSelf.currentTitle || !!weakSelf.currentAttributedTitle;
+    };
+    QMUILayouterItem *subtitle = [QMUILayouterItem itemWithView:self.subtitleLabel margin:self.subtitleEdgeInsets grow:QMUILayouterGrowNever shrink:QMUILayouterShrinkDefault];
+    QMUILayouterLinearVertical *titles = [QMUILayouterLinearVertical itemWithChildItems:@[
+        title,
+        subtitle,
+    ] spacingBetweenItems:0 horizontal:horizontal vertical:vertical];
+    titles.shrink = QMUILayouterShrinkDefault;
     
-    switch (self.imagePosition) {
-        case QMUIButtonImagePositionTop:
-        case QMUIButtonImagePositionBottom: {
-            // 图片和文字上下排版时,宽度以文字或图片的最大宽度为最终宽度
-            if (isImageViewShowing) {
-                CGFloat imageLimitWidth = contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets);
-                CGSize imageSize = self.imageView.image ? [self.imageView sizeThatFits:CGSizeMake(imageLimitWidth, CGFLOAT_MAX)] : self.currentImage.size;
-                imageSize.width = MIN(imageSize.width, imageLimitWidth);// QMUIButton sizeThatFits 时 self._imageView 为 nil 但 self.imageView 有值,而开启了 Bold Text 时,系统的 self.imageView sizeThatFits 返回值会比没开启 BoldText 时多 1pt(不知道为什么文字加粗与否会影响 imageView...),从而保证开启 Bold Text 后文字依然能完整展示出来,所以这里应该用 self.imageView 而不是 self._imageView
-                imageTotalSize = CGSizeMake(imageSize.width + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), imageSize.height + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
-            }
-            
-            if (isTitleLabelShowing) {
-                CGSize titleLimitSize = CGSizeMake(contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), contentLimitSize.height - imageTotalSize.height - spacingBetweenImageAndTitle - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
-                CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize];
-                titleSize.height = MIN(titleSize.height, titleLimitSize.height);
-                titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
+    if (self.imagePosition == QMUIButtonImagePositionTop || self.imagePosition == QMUIButtonImagePositionBottom) {
+        if (vertical == QMUILayouterAlignmentFill) {
+            if (image.visible && title.visible && !subtitle.visible) {
+                titles.grow = QMUILayouterGrowMost;
+                title.grow = QMUILayouterGrowMost;
+            } else if (image.visible && !title.visible && subtitle.visible) {
+                titles.grow = QMUILayouterGrowMost;
+                subtitle.grow = QMUILayouterGrowMost;
+            } else if (!image.visible && title.visible && subtitle.visible) {
+                titles.grow = QMUILayouterGrowMost;
+                title.grow = QMUILayouterGrowMost;
             }
-            
-            resultSize.width = UIEdgeInsetsGetHorizontalValue(contentEdgeInsets);
-            resultSize.width += MAX(imageTotalSize.width, titleTotalSize.width);
-            resultSize.height = UIEdgeInsetsGetVerticalValue(contentEdgeInsets) + imageTotalSize.height + spacingBetweenImageAndTitle + titleTotalSize.height;
         }
-            break;
-            
-        case QMUIButtonImagePositionLeft:
-        case QMUIButtonImagePositionRight: {
-            // 图片和文字水平排版时,高度以文字或图片的最大高度为最终高度
-            // 注意这里有一个和系统不一致的行为:当 titleLabel 为多行时,系统的 sizeThatFits: 计算结果固定是单行的,所以当 QMUIButtonImagePositionLeft 并且titleLabel 多行的情况下,QMUIButton 计算的结果与系统不一致
-            
-            if (isImageViewShowing) {
-                CGFloat imageLimitHeight = contentLimitSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets);
-                CGSize imageSize = self.imageView.image ? [self.imageView sizeThatFits:CGSizeMake(CGFLOAT_MAX, imageLimitHeight)] : self.currentImage.size;
-                imageSize.height = MIN(imageSize.height, imageLimitHeight);// QMUIButton sizeThatFits 时 self._imageView 为 nil 但 self.imageView 有值,而开启了 Bold Text 时,系统的 self.imageView sizeThatFits 返回值会比没开启 BoldText 时多 1pt(不知道为什么文字加粗与否会影响 imageView...),从而保证开启 Bold Text 后文字依然能完整展示出来,所以这里应该用 self.imageView 而不是 self._imageView
-                imageTotalSize = CGSizeMake(imageSize.width + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), imageSize.height + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
+    } else if (self.imagePosition == QMUIButtonImagePositionLeft || self.imagePosition == QMUIButtonImagePositionRight) {
+        if (horizontal == QMUILayouterAlignmentFill) {
+            if (image.visible && (title.visible || subtitle.visible)) {
+                titles.grow = QMUILayouterGrowMost;
             }
-            
-            if (isTitleLabelShowing) {
-                CGSize titleLimitSize = CGSizeMake(contentLimitSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) - imageTotalSize.width - spacingBetweenImageAndTitle, contentLimitSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
-                CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize];
-                titleSize.height = MIN(titleSize.height, titleLimitSize.height);
-                titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
+        }
+        if (vertical == QMUILayouterAlignmentFill) {
+            if (title.visible) {
+                title.grow = QMUILayouterGrowMost;
+            } else if (subtitle.visible) {
+                subtitle.grow = QMUILayouterGrowMost;
             }
-            
-            resultSize.width = UIEdgeInsetsGetHorizontalValue(contentEdgeInsets) + imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width;
-            resultSize.height = UIEdgeInsetsGetVerticalValue(contentEdgeInsets);
-            resultSize.height += MAX(imageTotalSize.height, titleTotalSize.height);
         }
-            break;
     }
-    return resultSize;
+    
+    switch (self.imagePosition) {
+        case QMUIButtonImagePositionTop: {
+            return [QMUILayouterLinearVertical itemWithChildItems:@[
+                image,
+                titles,
+            ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical];
+        }
+        case QMUIButtonImagePositionBottom: {
+            return [QMUILayouterLinearVertical itemWithChildItems:@[
+                titles,
+                image,
+            ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical];
+        }
+        case QMUIButtonImagePositionLeft: {
+            return [QMUILayouterLinearHorizontal itemWithChildItems:@[
+                image,
+                titles,
+            ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical];
+        }
+        case QMUIButtonImagePositionRight: {
+            return [QMUILayouterLinearHorizontal itemWithChildItems:@[
+                titles,
+                image,
+            ] spacingBetweenItems:self.spacingBetweenImageAndTitle horizontal:horizontal vertical:vertical];
+        }
+    }
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+    // 如果调用 sizeToFit,那么传进来的 size 就是当前按钮的 size,此时的计算不要去限制宽高
+    // 系统 UIButton 不管任何时候,对 sizeThatFits:CGSizeZero 都会返回真实的内容大小,这里对齐
+    if (CGSizeEqualToSize(self.bounds.size, size) || CGSizeIsEmpty(size)) {
+        size = CGSizeMax;
+    }
+    
+    QMUILayouterItem *layouter = [self generateLayouterForLayout:NO];
+    CGSize result = [layouter sizeThatFits:size];
+    result.width += UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets);
+    result.height += UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets);
+    return result;
 }
 
 - (CGSize)intrinsicContentSize {
@@ -151,297 +226,25 @@ const CGFloat QMUIButtonCornerRadiusAdjustsBounds = -1;
         self.layer.cornerRadius = CGRectGetHeight(self.bounds) / 2;
     }
     
-    BOOL isImageViewShowing = !!self.currentImage;
-    BOOL isTitleLabelShowing = !!self.currentTitle || !!self.currentAttributedTitle;
-    CGSize imageLimitSize = CGSizeZero;
-    CGSize titleLimitSize = CGSizeZero;
-    CGSize imageTotalSize = CGSizeZero;// 包含 imageEdgeInsets 那些空间
-    CGSize titleTotalSize = CGSizeZero;// 包含 titleEdgeInsets 那些空间
-    CGFloat spacingBetweenImageAndTitle = flat(isImageViewShowing && isTitleLabelShowing ? self.spacingBetweenImageAndTitle : 0);// 如果图片或文字某一者没显示,则这个 spacing 不考虑进布局
-    CGRect imageFrame = CGRectZero;
-    CGRect titleFrame = CGRectZero;
-    UIEdgeInsets contentEdgeInsets = UIEdgeInsetsRemoveFloatMin(self.contentEdgeInsets);
-    CGSize contentSize = CGSizeMake(CGRectGetWidth(self.bounds) - UIEdgeInsetsGetHorizontalValue(contentEdgeInsets), CGRectGetHeight(self.bounds) - UIEdgeInsetsGetVerticalValue(contentEdgeInsets));
-    
-    // 图片的布局原则都是尽量完整展示,所以不管 imagePosition 的值是什么,这个计算过程都是相同的
-    if (isImageViewShowing) {
-        imageLimitSize = CGSizeMake(contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
-        CGSize imageSize = self._qmui_imageView.image ? [self._qmui_imageView sizeThatFits:imageLimitSize] : self.currentImage.size;
-        imageSize.width = MIN(imageLimitSize.width, imageSize.width);
-        imageSize.height = MIN(imageLimitSize.height, imageSize.height);
-        imageFrame = CGRectMakeWithSize(imageSize);
-        imageTotalSize = CGSizeMake(imageSize.width + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets), imageSize.height + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
-    }
+    QMUILayouterItem *layouter = [self generateLayouterForLayout:YES];
+    layouter.frame = CGRectInsetEdges(self.bounds, self.contentEdgeInsets);
+    [layouter layoutIfNeeded];
     
-    // UIButton 如果本身大小为 (0,0),此时设置一个 imageEdgeInsets 会让 imageView 的 bounds 错误,导致后续 imageView 的 subviews 布局时会产生偏移,因此这里做一次保护
-    // https://github.com/Tencent/QMUI_iOS/issues/1012
-    void (^makesureBoundsPositive)(UIView *) = ^void(UIView *view) {
-        CGRect bounds = view.bounds;
-        if (CGRectGetMinX(bounds) < 0 || CGRectGetMinY(bounds) < 0) {
-            bounds = CGRectMakeWithSize(bounds.size);
-            view.bounds = bounds;
-        }
-    };
-    if (isImageViewShowing) {
-        makesureBoundsPositive(self._qmui_imageView);
-    }
-    if (isTitleLabelShowing) {
-        makesureBoundsPositive(self.titleLabel);
-    }
-    
-    if (self.imagePosition == QMUIButtonImagePositionTop || self.imagePosition == QMUIButtonImagePositionBottom) {
-        
-        if (isTitleLabelShowing) {
-            titleLimitSize = CGSizeMake(contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), contentSize.height - imageTotalSize.height - spacingBetweenImageAndTitle - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
-            CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize];
-            titleSize.width = MIN(titleLimitSize.width, titleSize.width);
-            titleSize.height = MIN(titleLimitSize.height, titleSize.height);
-            titleFrame = CGRectMakeWithSize(titleSize);
-            titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
-        }
-        
-        switch (self.contentHorizontalAlignment) {
-            case UIControlContentHorizontalAlignmentLeft:
-                imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left) : imageFrame;
-                titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left) : titleFrame;
-                break;
-            case UIControlContentHorizontalAlignmentCenter:
-                imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left + CGFloatGetCenter(imageLimitSize.width, CGRectGetWidth(imageFrame))) : imageFrame;
-                titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left + CGFloatGetCenter(titleLimitSize.width, CGRectGetWidth(titleFrame))) : titleFrame;
-                break;
-            case UIControlContentHorizontalAlignmentRight:
-                imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)) : imageFrame;
-                titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.titleEdgeInsets.right - CGRectGetWidth(titleFrame)) : titleFrame;
-                break;
-            case UIControlContentHorizontalAlignmentFill:
-                if (isImageViewShowing) {
-                    imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left);
-                    imageFrame = CGRectSetWidth(imageFrame, imageLimitSize.width);
-                }
-                if (isTitleLabelShowing) {
-                    titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left);
-                    titleFrame = CGRectSetWidth(titleFrame, titleLimitSize.width);
-                }
-                break;
-            default:
-                break;
-        }
-        
-        if (self.imagePosition == QMUIButtonImagePositionTop) {
-            switch (self.contentVerticalAlignment) {
-                case UIControlContentVerticalAlignmentTop:
-                    imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top) : imageFrame;
-                    titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + imageTotalSize.height + spacingBetweenImageAndTitle + self.titleEdgeInsets.top) : titleFrame;
-                    break;
-                case UIControlContentVerticalAlignmentCenter: {
-                    CGFloat contentHeight = imageTotalSize.height + spacingBetweenImageAndTitle + titleTotalSize.height;
-                    CGFloat minY = CGFloatGetCenter(contentSize.height, contentHeight) + contentEdgeInsets.top;
-                    imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, minY + self.imageEdgeInsets.top) : imageFrame;
-                    titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, minY + imageTotalSize.height + spacingBetweenImageAndTitle + self.titleEdgeInsets.top) : titleFrame;
-                }
-                    break;
-                case UIControlContentVerticalAlignmentBottom:
-                    titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)) : titleFrame;
-                    imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - titleTotalSize.height - spacingBetweenImageAndTitle - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)) : imageFrame;
-                    break;
-                case UIControlContentVerticalAlignmentFill: {
-                    if (isImageViewShowing && isTitleLabelShowing) {
-                        
-                        // 同时显示图片和 label 的情况下,图片高度按本身大小显示,剩余空间留给 label
-                        imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top) : imageFrame;
-                        titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + imageTotalSize.height + spacingBetweenImageAndTitle + self.titleEdgeInsets.top) : titleFrame;
-                        titleFrame = isTitleLabelShowing ? CGRectSetHeight(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetMinY(titleFrame)) : titleFrame;
-                        
-                    } else if (isImageViewShowing) {
-                        imageFrame = CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top);
-                        imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
-                    } else {
-                        titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top);
-                        titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
-                    }
-                }
-                    break;
-            }
-        } else {
-            switch (self.contentVerticalAlignment) {
-                case UIControlContentVerticalAlignmentTop:
-                    titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top) : titleFrame;
-                    imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + titleTotalSize.height + spacingBetweenImageAndTitle + self.imageEdgeInsets.top) : imageFrame;
-                    break;
-                case UIControlContentVerticalAlignmentCenter: {
-                    CGFloat contentHeight = imageTotalSize.height + titleTotalSize.height + spacingBetweenImageAndTitle;
-                    CGFloat minY = CGFloatGetCenter(contentSize.height, contentHeight) + contentEdgeInsets.top;
-                    titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, minY + self.titleEdgeInsets.top) : titleFrame;
-                    imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, minY + titleTotalSize.height + spacingBetweenImageAndTitle + self.imageEdgeInsets.top) : imageFrame;
-                }
-                    break;
-                case UIControlContentVerticalAlignmentBottom:
-                    imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)) : imageFrame;
-                    titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - imageTotalSize.height - spacingBetweenImageAndTitle - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)) : titleFrame;
-                    break;
-                case UIControlContentVerticalAlignmentFill: {
-                    if (isImageViewShowing && isTitleLabelShowing) {
-                        
-                        // 同时显示图片和 label 的情况下,图片高度按本身大小显示,剩余空间留给 label
-                        imageFrame = CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame));
-                        titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top);
-                        titleFrame = CGRectSetHeight(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - imageTotalSize.height - spacingBetweenImageAndTitle - self.titleEdgeInsets.bottom - CGRectGetMinY(titleFrame));
-                        
-                    } else if (isImageViewShowing) {
-                        imageFrame = CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top);
-                        imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
-                    } else {
-                        titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top);
-                        titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
-                    }
-                }
-                    break;
-            }
-        }
-        
-        if (isImageViewShowing) {
-            imageFrame = CGRectFlatted(imageFrame);
-            self._qmui_imageView.frame = imageFrame;
-        }
-        if (isTitleLabelShowing) {
-            titleFrame = CGRectFlatted(titleFrame);
-            self.titleLabel.frame = titleFrame;
-        }
-        
-    } else if (self.imagePosition == QMUIButtonImagePositionLeft || self.imagePosition == QMUIButtonImagePositionRight) {
-        
-        if (isTitleLabelShowing) {
-            titleLimitSize = CGSizeMake(contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets) - imageTotalSize.width - spacingBetweenImageAndTitle, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
-            CGSize titleSize = [self.titleLabel sizeThatFits:titleLimitSize];
-            titleSize.width = MIN(titleLimitSize.width, titleSize.width);
-            titleSize.height = MIN(titleLimitSize.height, titleSize.height);
-            titleFrame = CGRectMakeWithSize(titleSize);
-            titleTotalSize = CGSizeMake(titleSize.width + UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets), titleSize.height + UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
-        }
-        
-        switch (self.contentVerticalAlignment) {
-            case UIControlContentVerticalAlignmentTop:
-                imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top) : imageFrame;
-                titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top) : titleFrame;
-                
-                break;
-            case UIControlContentVerticalAlignmentCenter:
-                imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, contentEdgeInsets.top + CGFloatGetCenter(contentSize.height, CGRectGetHeight(imageFrame)) + self.imageEdgeInsets.top) : imageFrame;
-                titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, contentEdgeInsets.top + CGFloatGetCenter(contentSize.height, CGRectGetHeight(titleFrame)) + self.titleEdgeInsets.top) : titleFrame;
-                break;
-            case UIControlContentVerticalAlignmentBottom:
-                imageFrame = isImageViewShowing ? CGRectSetY(imageFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.imageEdgeInsets.bottom - CGRectGetHeight(imageFrame)) : imageFrame;
-                titleFrame = isTitleLabelShowing ? CGRectSetY(titleFrame, CGRectGetHeight(self.bounds) - contentEdgeInsets.bottom - self.titleEdgeInsets.bottom - CGRectGetHeight(titleFrame)) : titleFrame;
-                break;
-            case UIControlContentVerticalAlignmentFill:
-                if (isImageViewShowing) {
-                    imageFrame = CGRectSetY(imageFrame, contentEdgeInsets.top + self.imageEdgeInsets.top);
-                    imageFrame = CGRectSetHeight(imageFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets));
-                }
-                if (isTitleLabelShowing) {
-                    titleFrame = CGRectSetY(titleFrame, contentEdgeInsets.top + self.titleEdgeInsets.top);
-                    titleFrame = CGRectSetHeight(titleFrame, contentSize.height - UIEdgeInsetsGetVerticalValue(self.titleEdgeInsets));
-                }
-                break;
-        }
-        
-        if (self.imagePosition == QMUIButtonImagePositionLeft) {
-            switch (self.contentHorizontalAlignment) {
-                case UIControlContentHorizontalAlignmentLeft:
-                    imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left) : imageFrame;
-                    titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left) : titleFrame;
-                    break;
-                case UIControlContentHorizontalAlignmentCenter: {
-                    CGFloat contentWidth = imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width;
-                    CGFloat minX = contentEdgeInsets.left + CGFloatGetCenter(contentSize.width, contentWidth);
-                    imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, minX + self.imageEdgeInsets.left) : imageFrame;
-                    titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, minX + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left) : titleFrame;
-                }
-                    break;
-                case UIControlContentHorizontalAlignmentRight: {
-                    if (imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width > contentSize.width) {
-                        // 图片和文字总宽超过按钮宽度,则优先完整显示图片
-                        imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left) : imageFrame;
-                        titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left) : titleFrame;
-                    } else {
-                        // 内容不超过按钮宽度,则靠右布局即可
-                        titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.titleEdgeInsets.right - CGRectGetWidth(titleFrame)) : titleFrame;
-                        imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - titleTotalSize.width - spacingBetweenImageAndTitle - imageTotalSize.width + self.imageEdgeInsets.left) : imageFrame;
-                    }
-                }
-                    break;
-                case UIControlContentHorizontalAlignmentFill: {
-                    if (isImageViewShowing && isTitleLabelShowing) {
-                        // 同时显示图片和 label 的情况下,图片按本身宽度显示,剩余空间留给 label
-                        imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left);
-                        titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + imageTotalSize.width + spacingBetweenImageAndTitle + self.titleEdgeInsets.left);
-                        titleFrame = CGRectSetWidth(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.titleEdgeInsets.right - CGRectGetMinX(titleFrame));
-                    } else if (isImageViewShowing) {
-                        imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left);
-                        imageFrame = CGRectSetWidth(imageFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets));
-                    } else {
-                        titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left);
-                        titleFrame = CGRectSetWidth(titleFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets));
-                    }
-                }
-                    break;
-                default:
-                    break;
-            }
-        } else {
-            switch (self.contentHorizontalAlignment) {
-                case UIControlContentHorizontalAlignmentLeft: {
-                    if (imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width > contentSize.width) {
-                        // 图片和文字总宽超过按钮宽度,则优先完整显示图片
-                        imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)) : imageFrame;
-                        titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - imageTotalSize.width - spacingBetweenImageAndTitle - titleTotalSize.width + self.titleEdgeInsets.left) : titleFrame;
-                    } else {
-                        // 内容不超过按钮宽度,则靠左布局即可
-                        titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left) : titleFrame;
-                        imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, contentEdgeInsets.left + titleTotalSize.width + spacingBetweenImageAndTitle + self.imageEdgeInsets.left) : imageFrame;
-                    }
-                }
-                    break;
-                case UIControlContentHorizontalAlignmentCenter: {
-                    CGFloat contentWidth = imageTotalSize.width + spacingBetweenImageAndTitle + titleTotalSize.width;
-                    CGFloat minX = contentEdgeInsets.left + CGFloatGetCenter(contentSize.width, contentWidth);
-                    titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, minX + self.titleEdgeInsets.left) : titleFrame;
-                    imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, minX + titleTotalSize.width + spacingBetweenImageAndTitle + self.imageEdgeInsets.left) : imageFrame;
-                }
-                    break;
-                case UIControlContentHorizontalAlignmentRight:
-                    imageFrame = isImageViewShowing ? CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame)) : imageFrame;
-                    titleFrame = isTitleLabelShowing ? CGRectSetX(titleFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - imageTotalSize.width - spacingBetweenImageAndTitle - self.titleEdgeInsets.right - CGRectGetWidth(titleFrame)) : titleFrame;
-                    break;
-                case UIControlContentHorizontalAlignmentFill: {
-                    if (isImageViewShowing && isTitleLabelShowing) {
-                        // 图片按自身大小显示,剩余空间由标题占满
-                        imageFrame = CGRectSetX(imageFrame, CGRectGetWidth(self.bounds) - contentEdgeInsets.right - self.imageEdgeInsets.right - CGRectGetWidth(imageFrame));
-                        titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left);
-                        titleFrame = CGRectSetWidth(titleFrame, CGRectGetMinX(imageFrame) - self.imageEdgeInsets.left - spacingBetweenImageAndTitle - self.titleEdgeInsets.right - CGRectGetMinX(titleFrame));
-                        
-                    } else if (isImageViewShowing) {
-                        imageFrame = CGRectSetX(imageFrame, contentEdgeInsets.left + self.imageEdgeInsets.left);
-                        imageFrame = CGRectSetWidth(imageFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets));
-                    } else {
-                        titleFrame = CGRectSetX(titleFrame, contentEdgeInsets.left + self.titleEdgeInsets.left);
-                        titleFrame = CGRectSetWidth(titleFrame, contentSize.width - UIEdgeInsetsGetHorizontalValue(self.titleEdgeInsets));
-                    }
-                }
-                    break;
-                default:
-                    break;
-            }
-        }
-        
-        if (isImageViewShowing) {
-            imageFrame = CGRectFlatted(imageFrame);
-            self._qmui_imageView.frame = imageFrame;
+    // UIButton 有一个特性是不管哪种 alignment,imageView 的宽高必定不超过 button 的宽高(也不管 imageView 的宽高比例是否产生变化),从而保证就算设置了超过 button 大小的 image,也会在 button 容器内部显示。这里对齐系统的特性
+    BOOL isImageViewShowing = !!self.currentImage;
+    if (isImageViewShowing && !CGRectIsEmpty(self.bounds)) {
+        UIImageView *imageView = self._qmui_imageView;
+        CGRect rect = imageView.frame;
+        CGRect limitRect = CGRectInsetEdges(CGRectInsetEdges(self.bounds, self.contentEdgeInsets), self.imageEdgeInsets);
+        if (CGRectGetWidth(rect) > CGRectGetWidth(limitRect)) {
+            rect = CGRectSetWidth(rect, CGRectGetWidth(limitRect));
+            rect = CGRectSetX(rect, self.contentEdgeInsets.left + self.imageEdgeInsets.left);
         }
-        if (isTitleLabelShowing) {
-            titleFrame = CGRectFlatted(titleFrame);
-            self.titleLabel.frame = titleFrame;
+        if (CGRectGetHeight(rect) > CGRectGetHeight(limitRect)) {
+            rect = CGRectSetHeight(rect, CGRectGetHeight(limitRect));
+            rect = CGRectSetY(rect, self.contentEdgeInsets.top + self.imageEdgeInsets.top);
         }
+        imageView.frame = rect;
     }
 }
 
@@ -501,10 +304,8 @@ const CGFloat QMUIButtonCornerRadiusAdjustsBounds = -1;
 
 - (void)setEnabled:(BOOL)enabled {
     [super setEnabled:enabled];
-    if (!enabled && self.adjustsButtonWhenDisabled) {
-        self.alpha = ButtonDisabledAlpha;
-    } else {
-        self.alpha = 1;
+    if (self.adjustsButtonWhenDisabled) {
+        self.alpha = enabled ? 1 : ButtonDisabledAlpha;
     }
 }
 
@@ -532,14 +333,16 @@ const CGFloat QMUIButtonCornerRadiusAdjustsBounds = -1;
 }
 
 - (void)updateTitleColorIfNeeded {
-    if (self.adjustsTitleTintColorAutomatically && self.currentTitleColor) {
+    if (!self.adjustsTitleTintColorAutomatically) return;
+    if (self.currentTitleColor) {
         [self setTitleColor:self.tintColor forState:UIControlStateNormal];
     }
-    if (self.adjustsTitleTintColorAutomatically && self.currentAttributedTitle) {
+    if (self.currentAttributedTitle) {
         NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:self.currentAttributedTitle];
         [attributedString addAttribute:NSForegroundColorAttributeName value:self.tintColor range:NSMakeRange(0, attributedString.length)];
         [self setAttributedTitle:attributedString forState:UIControlStateNormal];
     }
+    self.subtitleColor = self.tintColor;
 }
 
 - (void)setAdjustsImageTintColorAutomatically:(BOOL)adjustsImageTintColorAutomatically {
@@ -576,7 +379,7 @@ const CGFloat QMUIButtonCornerRadiusAdjustsBounds = -1;
 }
 
 - (void)setImage:(UIImage *)image forState:(UIControlState)state {
-    if (self.adjustsImageTintColorAutomatically) {
+    if (self.adjustsImageTintColorAutomatically && image.renderingMode != UIImageRenderingModeAlwaysOriginal) {
         image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
     }
     

+ 48 - 1
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m

@@ -24,6 +24,8 @@
 #import "NSString+QMUI.h"
 #import "UINavigationController+QMUI.h"
 #import "UINavigationItem+QMUI.h"
+#import "UINavigationBar+QMUI.h"
+#import "NSArray+QMUI.h"
 
 typedef NS_ENUM(NSInteger, QMUINavigationButtonPosition) {
     QMUINavigationButtonPositionNone = -1,  // 不处于navigationBar最左(右)边的按钮,则使用None。用None则不会在alignmentRectInsets里调整位置
@@ -107,6 +109,7 @@ typedef NS_ENUM(NSInteger, QMUINavigationButtonPosition) {
         }
             break;
         case QMUINavigationButtonTypeBack: {
+            self.qmui_outsideEdge = UIEdgeInsetsMake(-12, -12, -24, -24);
             UIImage *backIndicatorImage = UINavigationBar.qmui_appearanceConfigured.backIndicatorImage;
             if (!backIndicatorImage) {
                 // 配置表没有自定义的图片,则按照系统的返回按钮图片样式创建一张,颜色按照 tintColor 来
@@ -362,7 +365,7 @@ typedef NS_ENUM(NSInteger, QMUINavigationButtonPosition) {
         for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); index++) {
             SEL originalSelector = selectors[index];
             SEL swizzledSelector = NSSelectorFromString([@"qmui_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
-            ExchangeImplementations([self class], originalSelector, swizzledSelector);
+            ExchangeImplementations([UINavigationItem class], originalSelector, swizzledSelector);
         }
     });
 }
@@ -477,6 +480,50 @@ typedef NS_ENUM(NSInteger, QMUINavigationButtonPosition) {
             };
         });
         
+        // 系统的 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<UIView *> *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;
+            };
+        });
     });
 }
 

+ 3 - 3
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUICellHeightCache.m

@@ -222,7 +222,7 @@ QMUISynthesizeBOOLProperty(qmui_invalidateIndexPathHeightCachedAutomatically, se
         for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
             SEL originalSelector = selectors[index];
             SEL swizzledSelector = NSSelectorFromString([@"qmuiTableCache_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
-            ExchangeImplementations([self class], originalSelector, swizzledSelector);
+            ExchangeImplementations([UITableView class], originalSelector, swizzledSelector);
         }
     });
 }
@@ -383,8 +383,8 @@ QMUISynthesizeBOOLProperty(qmui_invalidateIndexPathHeightCachedAutomatically, se
         // 没有的话,则需要通过register来注册一个cell,否则会crash
         if (!templateCell) {
             templateCell = [self dequeueReusableCellWithIdentifier:identifier];
-            QMUIAssert(templateCell != nil, @"QMUICellHeightCache", @"通过 %s %@ 无法得到一个 cell 对象", __func__, identifier);
         }
+        QMUIAssert(templateCell != nil, @"QMUICellHeightCache", @"通过 %s %@ 无法得到一个 cell 对象", __func__, identifier);
         templateCell.contentView.translatesAutoresizingMaskIntoConstraints = NO;
         templateCellsByIdentifiers[identifier] = templateCell;
     }
@@ -526,7 +526,7 @@ QMUISynthesizeIdStrongProperty(qmuiCollectionCache_allIndexPathHeightCaches, set
         for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); index++) {
             SEL originalSelector = selectors[index];
             SEL swizzledSelector = NSSelectorFromString([@"qmuiCollectionCache_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
-            ExchangeImplementations([self class], originalSelector, swizzledSelector);
+            ExchangeImplementations([UICollectionView class], originalSelector, swizzledSelector);
         }
     });
 }

+ 17 - 17
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIConsole/QMUIConsoleViewController.m

@@ -234,13 +234,13 @@
     [self.containerView addSubview:self.toolbar];
     
     __weak __typeof(self)weakSelf = self;
-    self.levelMenu = [self generatePopupMenuView];
+    self.levelMenu = [self generatePopupMenuViewWithSelectedArray:self.selectedLevels];
     self.levelMenu.willHideBlock = ^(BOOL hidesByUserTap, BOOL animated) {
         weakSelf.toolbar.levelButton.selected = weakSelf.selectedLevels.count > 0;
     };
     self.levelMenu.sourceView = self.toolbar.levelButton;
     
-    self.nameMenu = [self generatePopupMenuView];
+    self.nameMenu = [self generatePopupMenuViewWithSelectedArray:self.selectedNames];
     self.nameMenu.willHideBlock = ^(BOOL hidesByUserTap, BOOL animated) {
         weakSelf.toolbar.nameButton.selected = weakSelf.selectedNames.count > 0;
     };
@@ -365,7 +365,7 @@
             NSArray<QMUIConsoleLogItem *> *matchedItems = [self.showingLogItems qmui_filterWithBlock:^BOOL(QMUIConsoleLogItem * _Nonnull item) {
                 return item.searchResults.count > 0;
             }];
-            NSArray<NSArray<NSTextCheckingResult *> *> *matchedResults = [matchedItems qmui_mapWithBlock:^id _Nonnull(QMUIConsoleLogItem * _Nonnull item) {
+            NSArray<NSArray<NSTextCheckingResult *> *> *matchedResults = [matchedItems qmui_mapWithBlock:^id _Nonnull(QMUIConsoleLogItem * _Nonnull item, NSInteger index) {
                 return item.searchResults;
             }];
             self.searchResultsTotalCount = 0;
@@ -541,52 +541,52 @@
     self.toolbar.nameButton.enabled = self.logItems.count > 0;
 }
 
-- (QMUIPopupMenuView *)generatePopupMenuView {
+- (QMUIPopupMenuView *)generatePopupMenuViewWithSelectedArray:(NSArray<NSString *> *)selectedArray {
     QMUIPopupMenuView *menuView = [[QMUIPopupMenuView alloc] init];
     menuView.padding = UIEdgeInsetsMake(3, 6, 3, 6);
     menuView.cornerRadius = 3;
     menuView.arrowSize = CGSizeMake(8, 4);
     menuView.borderWidth = 0;
     menuView.itemHeight = 28;
-    menuView.itemTitleFont = UIFontMake(12);
-    menuView.itemTitleColor = UIColorMake(53, 60, 70);
     menuView.maskViewBackgroundColor = nil;
     menuView.backgroundColor = UIColorWhite;
-    menuView.itemConfigurationHandler = ^(QMUIPopupMenuView *aMenuView, QMUIPopupMenuButtonItem *aItem, NSInteger section, NSInteger index) {
-        aItem.highlightedBackgroundColor = [[UIColor blackColor] colorWithAlphaComponent:.15];
-        QMUIButton *button = aItem.button;
+    menuView.itemViewConfigurationHandler = ^(QMUIPopupMenuView * _Nonnull aMenuView, __kindof QMUIPopupMenuItem * _Nonnull aItem, QMUIPopupMenuItemView * _Nonnull aItemView, NSInteger section, NSInteger index) {
+        aItemView.button.highlightedBackgroundColor = [[UIColor blackColor] colorWithAlphaComponent:.15];
+        QMUIButton *button = aItemView.button;
+        button.titleLabel.font = UIFontMake(12);
+        button.tintColorAdjustsTitleAndImage = UIColorMake(53, 60, 70);
         button.imagePosition = QMUIButtonImagePositionRight;
         button.spacingBetweenImageAndTitle = 10;
-        UIImage *selectedImage = [UIImage qmui_imageWithShape:QMUIImageShapeCheckmark size:CGSizeMake(12, 9) lineWidth:1 tintColor:aMenuView.itemTitleColor];
+        UIImage *selectedImage = [[UIImage qmui_imageWithShape:QMUIImageShapeCheckmark size:CGSizeMake(12, 9) lineWidth:1 tintColor:UIColor.blackColor] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
         UIImage *normalImage = [UIImage qmui_imageWithColor:UIColorClear size:selectedImage.size cornerRadius:0];
         [button setImage:normalImage forState:UIControlStateNormal];// 无图像也弄一张空白图,以保证 state 变化时布局不跳动
         [button setImage:selectedImage forState:UIControlStateSelected];
         [button setImage:selectedImage forState:UIControlStateSelected|UIControlStateHighlighted];
+        button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill;
+        button.selected = [selectedArray containsObject:aItem.title];
     };
     menuView.hidden = YES;
     [self.view addSubview:menuView];
     return menuView;
 }
 
-- (NSArray<QMUIPopupMenuButtonItem *> *)popupMenuItemsByTitleBlock:(nullable NSString * (^)(QMUIConsoleLogItem *logItem))titleBlock selectedArray:(NSMutableArray<NSString *> *)selectedArray {
+- (NSArray<QMUIPopupMenuItem *> *)popupMenuItemsByTitleBlock:(nullable NSString * (^)(QMUIConsoleLogItem *logItem))titleBlock selectedArray:(NSMutableArray<NSString *> *)selectedArray {
     __weak __typeof(self)weakSelf = self;
-    NSMutableArray<QMUIPopupMenuButtonItem *> *items = [[NSMutableArray alloc] init];
+    NSMutableArray<QMUIPopupMenuItem *> *items = [[NSMutableArray alloc] init];
     NSMutableSet<NSString *> *itemTitles = [[NSMutableSet alloc] init];
     [self.logItems enumerateObjectsUsingBlock:^(QMUIConsoleLogItem * _Nonnull logItem, NSUInteger idx, BOOL * _Nonnull stop) {
         [itemTitles addObject:titleBlock(logItem)];
     }];
     [[itemTitles sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:NSStringFromSelector(@selector(description)) ascending:YES]]] enumerateObjectsUsingBlock:^(NSString * _Nonnull title, NSUInteger idx, BOOL * _Nonnull stop) {
-        QMUIPopupMenuButtonItem *item = [QMUIPopupMenuButtonItem itemWithImage:nil title:title handler:^(QMUIPopupMenuButtonItem *aItem) {
-            aItem.button.selected = !aItem.button.selected;
-            if (aItem.button.selected) {
+        QMUIPopupMenuItem *item = [QMUIPopupMenuItem itemWithTitle:title handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, QMUIPopupMenuItemView * _Nonnull aItemView, NSInteger section, NSInteger index) {
+            aItemView.button.selected = !aItemView.button.selected;
+            if (aItemView.button.selected) {
                 [selectedArray addObject:title];
             } else {
                 [selectedArray removeObject:title];
             }
             [weakSelf printLog];
         }];
-        item.button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentFill;
-        item.button.selected = [selectedArray containsObject:title];
         [items addObject:item];
     }];
     return items.copy;

+ 1 - 6
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIImagePreviewView/QMUIImagePreviewViewController.m

@@ -147,12 +147,7 @@ const CGFloat QMUIImagePreviewViewControllerCornerRadiusAutomaticDimension = -1;
     if (self.qmui_visibleState < QMUIViewControllerDidAppear || self.qmui_visibleState >= QMUIViewControllerDidDisappear) {
         // 在 present/dismiss 动画过程中,都使用原界面的状态栏显隐状态
         if (self.presentingViewController) {
-            BOOL statusBarHidden = NO;
-            if (@available(iOS 13.0, *)) {
-                statusBarHidden = self.presentingViewController.view.window.windowScene.statusBarManager.statusBarHidden;
-            } else {
-                statusBarHidden = UIApplication.sharedApplication.statusBarHidden;
-            }
+            BOOL statusBarHidden = self.presentingViewController.view.window.windowScene.statusBarManager.statusBarHidden;
             self.originalStatusBarHidden = statusBarHidden;
             return self.originalStatusBarHidden;
         }

+ 53 - 60
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIKeyboardManager.m

@@ -19,6 +19,7 @@
 #import "QMUIAppearance.h"
 #import "QMUIMultipleDelegates.h"
 #import "NSArray+QMUI.h"
+#import "UIView+QMUI.h"
 
 @class QMUIKeyboardViewFrameObserver;
 @protocol QMUIKeyboardViewFrameObserverDelegate <NSObject>
@@ -230,14 +231,12 @@ static char kAssociatedObjectKey_KeyboardViewFrameObserver;
         CGRect beginFrame = [[self.originUserInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
         CGRect endFrame = [[self.originUserInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
         
-        if (@available(iOS 13.0, *)) {
-            // 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;
-            }
+        // 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;
@@ -832,38 +831,8 @@ static char kAssociatedObjectKey_KeyboardViewFrameObserver;
     return distance;
 }
 
-+ (UIWindow *)keyboardWindow {
-    for (UIWindow *window in UIApplication.sharedApplication.windows) {
-        if ([self positionedKeyboardViewInWindow:window]) {
-            return 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;
-}
-
-+ (UIView *)keyboardView {
-    for (UIWindow *window in UIApplication.sharedApplication.windows) {
-        UIView *view = [self positionedKeyboardViewInWindow:window];
-        if (view) {
-            return view;
-        }
-    }
-    return nil;
-}
-
 /**
- 从给定的 window 里寻找代表键盘当前布局位置的 view。
+ 从所有 window 里寻找代表键盘当前布局位置的 view。
  iOS 15 及以前(包括用 Xcode 13 编译的 App 运行在 iOS 16 上的场景),键盘的 UI 层级是:
  |- UIApplication.windows
     |- UIRemoteKeyboardWindow
@@ -885,28 +854,48 @@ static char kAssociatedObjectKey_KeyboardViewFrameObserver;
  
  所以只要找到 UIInputSetHostView 即可,优先从 UIRemoteKeyboardWindow 找,不存在的话则从 UITextEffectsWindow 找。
  */
-+ (UIView *)positionedKeyboardViewInWindow:(UIWindow *)window {
++ (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 (!window) return nil;
+    if (inputSetHostView) return inputSetHostView;
     
-    NSString *windowName = NSStringFromClass(window.class);
-    if ([windowName isEqualToString:@"UIRemoteKeyboardWindow"]) {
-        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"];
-        }];
-        return result;
-    }
-    if ([windowName isEqualToString:@"UITextEffectsWindow"]) {
-        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;
+    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;
     }
-    return nil;
+    
+    window = [UIApplication.sharedApplication.windows qmui_firstMatchWithBlock:^BOOL(__kindof UIWindow * _Nonnull item) {
+        return [NSStringFromClass(item.class) isEqualToString:@"UITextEffectsWindow"];
+    }];
+    return window;
 }
 
 + (BOOL)isKeyboardVisible {
@@ -937,7 +926,10 @@ static char kAssociatedObjectKey_KeyboardViewFrameObserver;
 
 + (CGFloat)visibleKeyboardHeight {
     UIView *keyboardView = [self keyboardView];
-    UIWindow *keyboardWindow = keyboardView.window;
+    // iPad“侧拉”模式打开的 App,App Window 和键盘 Window 尺寸不同,如果以键盘 Window 为准则会认为键盘一直在屏幕上,从而出现误判,所以这里改为用 App Window。
+    // iPhone、iPad 全屏/分屏/台前调度,都没这个问题
+//    UIWindow *keyboardWindow = keyboardView.window;
+    UIWindow *keyboardWindow = UIApplication.sharedApplication.delegate.window;
     if (!keyboardView || !keyboardWindow) {
         return 0;
     } else {
@@ -947,7 +939,8 @@ static char kAssociatedObjectKey_KeyboardViewFrameObserver;
             return 0;
         }
         
-        CGRect visibleRect = CGRectIntersection(CGRectFlatted(keyboardWindow.bounds), CGRectFlatted(keyboardView.frame));
+        CGRect keyboardFrame = [keyboardWindow qmui_convertRect:keyboardView.bounds fromView:keyboardView];
+        CGRect visibleRect = CGRectIntersection(keyboardWindow.bounds, keyboardFrame);
         if (CGRectIsValidated(visibleRect)) {
             return CGRectGetHeight(visibleRect);
         }

+ 1 - 2
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILabel.m

@@ -177,8 +177,7 @@
         UIMenuController *menuController = [UIMenuController sharedMenuController];
         UIMenuItem *copyMenuItem = [[UIMenuItem alloc] initWithTitle:self.menuItemTitleForCopyAction ?: @"复制" action:@selector(copyString:)];
         [[UIMenuController sharedMenuController] setMenuItems:@[copyMenuItem]];
-        [menuController setTargetRect:self.frame inView:self.superview];
-        [menuController setMenuVisible:YES animated:YES];
+        [menuController showMenuFromView:self.superview rect:self.frame];
         
         self.highlighted = YES;
     } else if (gestureRecognizer.state == UIGestureRecognizerStatePossible) {

+ 20 - 20
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUILogManagerViewController.m

@@ -49,7 +49,7 @@
 - (void)initSearchController {
     [super initSearchController];
     self.searchController.qmui_preferredStatusBarStyleBlock = ^UIStatusBarStyle{
-        return QMUIStatusBarStyleDarkContent;
+        return UIStatusBarStyleDarkContent;
     };
 }
 
@@ -168,25 +168,25 @@
     menuView.maximumWidth = 124;
     menuView.safetyMarginsOfSuperview = UIEdgeInsetsSetRight(menuView.safetyMarginsOfSuperview, 6);
     menuView.items = @[
-                       [QMUIPopupMenuButtonItem itemWithImage:nil title:@"开启全部" handler:^(QMUIPopupMenuButtonItem *aItem) {
-                           for (NSString *logName in self.allNames) {
-                               [[QMUILogger sharedInstance].logNameManager setEnabled:YES forLogName:logName];
-                           }
-                           [self reloadData];
-                           [aItem.menuView hideWithAnimated:YES];
-                       }],
-                       [QMUIPopupMenuButtonItem itemWithImage:nil title:@"禁用全部" handler:^(QMUIPopupMenuButtonItem *aItem) {
-                           for (NSString *logName in self.allNames) {
-                               [[QMUILogger sharedInstance].logNameManager setEnabled:NO forLogName:logName];
-                           }
-                           [self reloadData];
-                           [aItem.menuView hideWithAnimated:YES];
-                       }],
-                       [QMUIPopupMenuButtonItem itemWithImage:nil title:@"清空全部" handler:^(QMUIPopupMenuButtonItem *aItem) {
-                           [[QMUILogger sharedInstance].logNameManager removeAllNames];
-                           [self reloadData];
-                           [aItem.menuView hideWithAnimated:YES];
-                       }]];
+        [QMUIPopupMenuItem itemWithTitle:@"开启全部" handler:^(__kindof QMUIPopupMenuItem * _Nonnull aItem, __kindof UIControl<QMUIPopupMenuItemViewProtocol> * _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<QMUIPopupMenuItemViewProtocol> * _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<QMUIPopupMenuItemViewProtocol> * _Nonnull aItemView, NSInteger section, NSInteger index) {
+            [[QMUILogger sharedInstance].logNameManager removeAllNames];
+            [self reloadData];
+            [aItem.menuView hideWithAnimated:YES];
+        }]];
     menuView.sourceBarItem = self.navigationItem.rightBarButtonItem;
     [menuView showWithAnimated:YES];
 }

+ 6 - 0
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.h

@@ -142,6 +142,12 @@ typedef NS_ENUM(NSUInteger, QMUIModalPresentationAnimationStyle) {
  */
 @property(nullable, nonatomic, strong, readonly) UIWindow *window;
 
+/**
+ 如果 modal 是以 window 形式显示的话,通过这个属性来决定 window 是否需要以 keyWindow 形式存在(keyWindow 一般用于与键盘交互的场景,没输入框可以不用开启它)
+ 默认为 YES。
+ */
+@property(nonatomic, assign) BOOL shouldBecomeKeyWindow;
+
 /**
  如果 modal 是以 window 形式显示的话,控制在 modal 显示时是否要自动把 App 主界面置灰。
  默认为 YES。

+ 6 - 18
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIModalPresentationViewController.m

@@ -98,6 +98,7 @@
     
     self.shouldDimmedAppAutomatically = YES;
     self.onlyRespondsToKeyboardEventFromDescendantViews = YES;
+    self.shouldBecomeKeyWindow = YES;
     self.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
     self.modalPresentationStyle = UIModalPresentationCustom;
     
@@ -512,7 +513,11 @@
         [self updateWindowStatusBarCapture];
     }
     self.window.rootViewController = self;
-    [self.window makeKeyAndVisible];
+    if (self.shouldBecomeKeyWindow) {
+        [self.window makeKeyAndVisible];
+    } else {
+        self.window.hidden = NO;
+    }
 }
 
 - (void)hidingAnimationWithCompletion:(void (^)(BOOL))completion {
@@ -805,23 +810,6 @@
 
 @implementation QMUIModalPresentationWindow
 
-- (void)layoutSubviews {
-    [super layoutSubviews];
-    // 避免来电状态时只 modal 的遮罩只盖住一部分的状态栏
-    // 但在 iOS 13 及以后,来电状态下状态栏的高度不会再变化了
-    // https://github.com/Tencent/QMUI_iOS/issues/375
-    if (@available(iOS 13.0, *)) {
-    } else {
-        if (self.rootViewController) {
-            UIView *rootView = self.rootViewController.view;
-            if (CGRectGetMinY(rootView.frame) > 0 && !UIApplication.sharedApplication.statusBarHidden && StatusBarHeight > CGRectGetMinY(rootView.frame)) {
-                rootView.frame = self.bounds;
-            }
-        }
-    }
-
-}
-
 @end
 
 @implementation UIViewController (QMUIModalPresentationViewController)

+ 10 - 1
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUINavigationTitleView.h

@@ -72,6 +72,7 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) {
 @property(nonatomic, weak) id<QMUINavigationTitleViewDelegate> 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
@@ -85,6 +86,13 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) {
 /// 当 tintColor 发生变化时是否要自动把 titleLabel、subtitleLabel、loadingView 的颜色也更新为 tintColor 的色值,默认为 YES,如果你自己修改了 titleLabel、subtitleLabel、loadingView 的颜色,需要把这个值置为 NO
 @property(nonatomic, assign) BOOL adjustsSubviewsTintColorAutomatically UI_APPEARANCE_SELECTOR;
 
+/**
+ * 是否自动调整 highlighted 时的样式,默认为YES。<br/>
+ * 当值为 YES 时,标题 highlighted 时会改变自身的 alpha 属性为 <b>UIControlHighlightedAlpha</b>
+ * 适用于比如说整个 titleView 不需要接受点击,但 accessoryView 需要接受点击,此时就应该 titleView.userInteractionEnabled = YES、titleView.adjustsSubviewsWhenHighlighted = NO
+ */
+@property(nonatomic, assign) BOOL adjustsSubviewsWhenHighlighted;
+
 /// 水平布局下的标题字体,默认为 NavBarTitleFont
 @property(nonatomic, strong) UIFont *horizontalTitleFont UI_APPEARANCE_SELECTOR;
 
@@ -178,6 +186,7 @@ typedef NS_ENUM(NSInteger, QMUINavigationTitleViewAccessoryType) {
 
 @interface UIView (QMUINavigationTitleView)
 
-/// 标记当前 view 是用于自定义的导航栏标题,QMUI 可以帮你自动处理系统的一些布局 bug。对于 QMUINavigationTitleView 而言默认值为 YES,其他 UIView 默认值为 NO
+/// 标记当前 view 是用于自定义的导航栏标题,QMUI 可以帮你自动处理系统的一些布局 bug,并且保证 pop 时导航栏标题颜色不会被前一个界面影响。
+/// 对于 QMUINavigationTitleView 而言默认值为 YES,其他 UIView 默认值为 NO。
 @property(nonatomic, assign) BOOL qmui_useAsNavigationTitleView;
 @end

+ 27 - 7
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUINavigationTitleView.m

@@ -49,10 +49,10 @@
     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];
-        _contentView.userInteractionEnabled = NO;
         [self addSubview:self.contentView];
         
         _titleLabel = [[UILabel alloc] init];
@@ -81,7 +81,8 @@
         self.horizontalTitleFont = QMUINavigationTitleView.appearance.horizontalTitleFont ?: UINavigationBar.qmui_appearanceConfigured.titleTextAttributes[NSFontAttributeName];
         self.horizontalSubtitleFont = QMUINavigationTitleView.appearance.horizontalSubtitleFont ?: self.horizontalTitleFont;
         
-        self.adjustsSubviewsTintColorAutomatically = YES;
+        self.adjustsSubviewsTintColorAutomatically = QMUINavigationTitleView.appearance.adjustsSubviewsTintColorAutomatically;
+        self.adjustsSubviewsWhenHighlighted = QMUINavigationTitleView.appearance.adjustsSubviewsWhenHighlighted;
         self.tintColor = QMUICMIActivated ? NavBarTitleColor : UINavigationBar.qmui_appearanceConfigured.titleTextAttributes[NSForegroundColorAttributeName];
     }
     return self;
@@ -223,7 +224,9 @@
 
 - (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;
 }
 
@@ -234,13 +237,13 @@
     
     [super layoutSubviews];
     
-    self.contentView.frame = self.bounds;
+    self.contentView.frame = CGRectInsetEdges(self.bounds, self.padding);
     
     BOOL alignLeft = self.contentHorizontalAlignment == UIControlContentHorizontalAlignmentLeft;
     BOOL alignRight = self.contentHorizontalAlignment == UIControlContentHorizontalAlignmentRight;
     
     // 通过sizeThatFit计算出来的size,如果大于可使用的最大宽度,则会被系统改为最大限制的最大宽度
-    CGSize maxSize = self.bounds.size;
+    CGSize maxSize = self.contentView.bounds.size;
     
     // 实际内容的size,小于等于maxSize
     CGSize contentSize = [self contentSize];
@@ -379,6 +382,11 @@
     [self refreshLayout];
 }
 
+- (void)setPadding:(UIEdgeInsets)padding {
+    _padding = padding;
+    [self refreshLayout];
+}
+
 - (void)setContentHorizontalAlignment:(UIControlContentHorizontalAlignment)contentHorizontalAlignment {
     [super setContentHorizontalAlignment:contentHorizontalAlignment];
     [self refreshLayout];
@@ -545,7 +553,8 @@
     _needsLoadingView = needsLoadingView;
     if (needsLoadingView) {
         if (!self.loadingView) {
-            _loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:NavBarActivityIndicatorViewStyle size:self.loadingViewSize];
+            _loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:NavBarActivityIndicatorViewStyle];
+            self.loadingView.qmui_size = self.loadingViewSize;
             self.loadingView.color = self.tintColor;
             [self.loadingView stopAnimating];
             [self.contentView addSubview:self.loadingView];
@@ -626,7 +635,17 @@
 
 - (void)setHighlighted:(BOOL)highlighted {
     [super setHighlighted:highlighted];
-    self.alpha = highlighted ? UIControlHighlightedAlpha : 1;
+    if (self.adjustsSubviewsWhenHighlighted) {
+        self.alpha = highlighted ? UIControlHighlightedAlpha : 1;
+    }
+}
+
+- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
+    UIView *result = [super hitTest:point withEvent:event];
+    if (result == self.contentView) {
+        return self;
+    }
+    return result;
 }
 
 - (void)handleTouchTitleViewEvent {
@@ -710,6 +729,7 @@
 + (void)setDefaultAppearance {
     QMUINavigationTitleView *appearance = [QMUINavigationTitleView appearance];
     appearance.adjustsSubviewsTintColorAutomatically = YES;
+    appearance.adjustsSubviewsWhenHighlighted = YES;
     appearance.maximumWidth = CGFLOAT_MAX;
     appearance.loadingViewSize = CGSizeMake(18, 18);
     appearance.loadingViewMarginRight = 3;
@@ -778,7 +798,7 @@
         return;
     }
     
-    QMUIAssert(viewController.navigationController == self, @"QMUINavigationTitleView", @"不存在 UINavigationController");
+    if (viewController.navigationController != self) return;
     
     QMUINavigationTitleView *navigationTitleView = (QMUINavigationTitleView *)titleView;
     UIView *largeTitleView = self.navigationBar.qmui_largeTitleView;

+ 43 - 3
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupContainerView.h

@@ -25,6 +25,12 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
     QMUIPopupContainerViewLayoutDirectionRight
 };
 
+typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutAlignment) {
+    QMUIPopupContainerViewLayoutAlignmentCenter,
+    QMUIPopupContainerViewLayoutAlignmentLeading,
+    QMUIPopupContainerViewLayoutAlignmentTrailing,
+};
+
 /**
  * 带箭头的小tips浮层,自带 imageView 和 textLabel,可展示简单的图文信息,支持 UIViewContentModeTop/UIViewContentModeBottom/UIViewContentModeCenter 三种布局方式。
  * QMUIPopupContainerView 支持以两种方式显示在界面上:
@@ -50,9 +56,11 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
  */
 @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;
@@ -92,6 +100,7 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
 @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;
 
@@ -101,7 +110,7 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
 /// 最小宽度(指整个控件的宽度,而不是contentView部分),默认为0
 @property(nonatomic, assign) CGFloat minimumWidth UI_APPEARANCE_SELECTOR;
 
-/// 最大高度(指整个控件的高度,而不是contentView部分),默认为CGFLOAT_MAX
+/// 最大高度(指整个控件的高度,而不是contentView部分),默认为CGFLOAT_MAX,会在布局时被动态修改。
 @property(nonatomic, assign) CGFloat maximumHeight UI_APPEARANCE_SELECTOR;
 
 /// 最小高度(指整个控件的高度,而不是contentView部分),默认为0
@@ -113,10 +122,22 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
 /// 最终的布局方向(preferLayoutDirection只是期望的方向,但有可能那个方向已经没有剩余空间可摆放控件了,所以会自动变换)
 @property(nonatomic, assign, readonly) QMUIPopupContainerViewLayoutDirection currentLayoutDirection;
 
+/// 计算布局时期望浮层与目标位置的对齐方式,默认为 QMUIPopupContainerViewLayoutAlignmentCenter,也即浮层和目标位置相对居中。
+/// 对 preferLayoutDirection 为 Above/Below 而言,Leading 表示浮层的左侧与目标位置左边缘对齐,Trailing 表示浮层的右侧与目标位置右边缘对齐。
+/// 对 preferLayoutDirection 为 Left/Right 而言,Leading 表示浮层的顶端与目标位置顶边缘对齐,Trailing 表示浮层的底端与目标位置底边缘对齐。
+/// 如果预期的对齐方式无法被满足时,会根据 usesOppositeLayoutAlignmentIfNeeded 的值来决定备选方案。
+@property(nonatomic, assign) QMUIPopupContainerViewLayoutAlignment preferLayoutAlignment UI_APPEARANCE_SELECTOR;
+
+/// 表示 preferLayoutAlignment 在极端情况下无法满足调用方设置的值时,应该以什么方式作为备选。
+/// 若当前属性值为 YES,则表示用相反的对齐方式去尝试(例如 preferLayoutAlignment = QMUIPopupContainerViewLayoutAlignmentLeading 则在极端情况下会用 QMUIPopupContainerViewLayoutAlignmentTrailing 作为备选),若当前属性值为 NO 则表示保持对齐方向不变,让浮层的边缘紧贴着 safetyMarginsOfSuperview 即可。
+/// 默认为 YES。
+/// @warning 对 QMUIPopupContainerViewLayoutAlignmentCenter 无意义,因为 QMUIPopupContainerViewLayoutAlignmentCenter 没有所谓的相反概念。
+@property(nonatomic, assign) BOOL usesOppositeLayoutAlignmentIfNeeded UI_APPEARANCE_SELECTOR;
+
 /// 最终布局时箭头距离目标边缘的距离,默认为5
 @property(nonatomic, assign) CGFloat distanceBetweenSource UI_APPEARANCE_SELECTOR;
 
-/// 最终布局时与父节点的边缘的临界点,默认为(10, 10, 10, 10)
+/// 最终布局时与父节点的边缘的临界点,默认为(10, 10, 10, 10),注意这里的值不需要由业务考虑 safeAreaInsets,内部会自己叠加。
 @property(nonatomic, assign) UIEdgeInsets safetyMarginsOfSuperview UI_APPEARANCE_SELECTOR;
 
 /// 允许用一个自定的 view 作为背景,会自动将其 mask 为圆角带箭头的造型,当同时使用 backgroundView 和 arrowImage 时,arrowImage 只作为遮罩使用(也即使用它的造型,不显示它的图片内容)。
@@ -147,7 +168,10 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
 /// rect 需要处于 QMUIPopupContainerView 所在的坐标系内,例如如果 popup 使用 addSubview: 的方式添加到界面,则 sourceRect 应该是 superview 坐标系内的;如果 popup 使用 window 的方式展示,则 sourceRect 需要转换为 window 坐标系内。
 @property(nonatomic, assign) CGRect sourceRect;
 
-/// 立即刷新当前 popup 的布局,前提是 popup 已经被 show 过。
+/// 标记为需要更新布局,会在下一次 runloop 里统一调用 updateLayout。一般情况请用这个方法,避免直接用 updateLayout,从而获取更佳的性能。
+- (void)setNeedsUpdateLayout;
+
+/// 立即刷新当前 popup 的布局,前提是 popup.isShowing 为 YES。
 - (void)updateLayout;
 
 - (void)showWithAnimated:(BOOL)animated;
@@ -156,6 +180,22 @@ typedef NS_ENUM(NSUInteger, QMUIPopupContainerViewLayoutDirection) {
 - (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 参数来实现。

+ 255 - 119
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupContainerView.m

@@ -101,7 +101,7 @@
 }
 
 - (void)setBackgroundView:(UIView *)backgroundView {
-    if (_backgroundView && !backgroundView) {
+    if (_backgroundView && _backgroundView != backgroundView) {
         [_backgroundView removeFromSuperview];
     }
     _backgroundView = backgroundView;
@@ -110,6 +110,7 @@
         // 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];
@@ -144,17 +145,17 @@
 
 - (void)setShadow:(NSShadow *)shadow {
     _shadow = shadow;
-    _backgroundLayer.qmui_shadow = shadow;
+    _borderLayer.qmui_shadow = shadow;
 }
 
 - (void)setBorderColor:(UIColor *)borderColor {
     _borderColor = borderColor;
-    _backgroundLayer.strokeColor = borderColor.CGColor;
+    _borderLayer.strokeColor = borderColor.CGColor;
 }
 
 - (void)setBorderWidth:(CGFloat)borderWidth {
     _borderWidth = borderWidth;
-    _backgroundLayer.lineWidth = _borderWidth;
+    _borderLayer.lineWidth = _borderWidth;
 }
 
 - (void)setCornerRadius:(CGFloat)cornerRadius {
@@ -171,6 +172,20 @@
     }
 }
 
+- (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;
@@ -188,90 +203,44 @@
     BOOL isUsingArrowImage = !!self.arrowImage;
     CGAffineTransform arrowImageTransform = CGAffineTransformIdentity;
     CGPoint arrowImagePosition = CGPointZero;
-    
     CGSize arrowSize = self.arrowSizeAuto;
-    CGRect roundedRect = CGRectMake(self.borderWidth / 2.0 + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight ? arrowSize.width : 0),
-                                    self.borderWidth / 2.0 + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow ? arrowSize.height : 0),
-                                    CGRectGetWidth(self.bounds) - self.borderWidth - self.arrowSpacingInHorizontal,
-                                    CGRectGetHeight(self.bounds) - self.borderWidth - self.arrowSpacingInVertical);
-    CGFloat cornerRadius = self.cornerRadius;
-    
-    CGPoint leftTopArcCenter = CGPointMake(CGRectGetMinX(roundedRect) + cornerRadius, CGRectGetMinY(roundedRect) + cornerRadius);
-    CGPoint leftBottomArcCenter = CGPointMake(leftTopArcCenter.x, CGRectGetMaxY(roundedRect) - cornerRadius);
-    CGPoint rightTopArcCenter = CGPointMake(CGRectGetMaxX(roundedRect) - cornerRadius, leftTopArcCenter.y);
-    CGPoint rightBottomArcCenter = CGPointMake(rightTopArcCenter.x, leftBottomArcCenter.y);
-    
-    // 从左上角逆时针绘制
-    UIBezierPath *path = [UIBezierPath bezierPath];
-    [path moveToPoint:CGPointMake(leftTopArcCenter.x, CGRectGetMinY(roundedRect))];
-    [path addArcWithCenter:leftTopArcCenter radius:cornerRadius startAngle:M_PI * 1.5 endAngle:M_PI clockwise:NO];
-    
-    if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight) {
-        // 箭头向左
-        if (isUsingArrowImage) {
-            arrowImageTransform = CGAffineTransformMakeRotation(AngleWithDegrees(90));
-            arrowImagePosition = CGPointMake(arrowSize.width / 2, _arrowMinY + arrowSize.height / 2);
-        } else {
-            [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) {
-            arrowImagePosition = CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetHeight(self.bounds) - arrowSize.height / 2);
-        } else {
-            [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) {
-            arrowImageTransform = CGAffineTransformMakeRotation(AngleWithDegrees(-90));
-            arrowImagePosition = CGPointMake(CGRectGetWidth(self.bounds) - arrowSize.width / 2, _arrowMinY + arrowSize.height / 2);
-        } else {
-            [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)];
+    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;
     }
     
-    [path addLineToPoint:CGPointMake(CGRectGetMaxX(roundedRect), rightTopArcCenter.y)];
-    [path addArcWithCenter:rightTopArcCenter radius:cornerRadius startAngle:0.0 endAngle:M_PI * 1.5 clockwise:NO];
+    UIBezierPath *borderPath = [self generatePathForBorder:YES];
+    _borderLayer.path = borderPath.CGPath;
+    _borderLayer.shadowPath = borderPath.CGPath;
+    _borderLayer.frame = self.bounds;
     
-    if (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow) {
-        // 箭头向上
-        if (isUsingArrowImage) {
-            arrowImageTransform = CGAffineTransformMakeRotation(AngleWithDegrees(-180));
-            arrowImagePosition = CGPointMake(_arrowMinX + arrowSize.width / 2, arrowSize.height / 2);
-        } else {
-            [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width, CGRectGetMinY(roundedRect))];
-            [path addLineToPoint:CGPointMake(_arrowMinX + arrowSize.width / 2, CGRectGetMinY(roundedRect) - arrowSize.height)];
-            [path addLineToPoint:CGPointMake(_arrowMinX, CGRectGetMinY(roundedRect))];
-        }
-    }
-    [path closePath];
-    
-    _backgroundLayer.path = path.CGPath;
-    _backgroundLayer.shadowPath = path.CGPath;
+    UIBezierPath *backgroundPath = [self generatePathForBorder:NO];
+    _backgroundLayer.path = backgroundPath.CGPath;
     _backgroundLayer.frame = self.bounds;
     
-    if (isUsingArrowImage) {
-        _arrowImageView.transform = arrowImageTransform;
-        _arrowImageView.center = arrowImagePosition;
-    }
-    
     if (self.backgroundView) {
         self.backgroundView.frame = self.bounds;
         _backgroundViewMaskLayer.frame = self.bounds;
@@ -291,10 +260,12 @@
 
 - (void)layoutDefaultSubviews {
     self.contentView.frame = CGRectMake(
-                                        self.borderWidth + self.contentEdgeInsets.left + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionRight ? self.arrowSizeAuto.width : 0),
-                                        self.borderWidth + self.contentEdgeInsets.top + (self.currentLayoutDirection == QMUIPopupContainerViewLayoutDirectionBelow ? self.arrowSizeAuto.height : 0),
-                                        CGRectGetWidth(self.bounds) - self.borderWidth * 2 - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) - self.arrowSpacingInHorizontal,
-                                        CGRectGetHeight(self.bounds) - self.borderWidth * 2 - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) - self.arrowSpacingInVertical);
+                                        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));
@@ -310,12 +281,12 @@
         } 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), CGRectGetHeight(_imageView.frame)));
+            _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), ceil(CGRectGetHeight(self.contentView.bounds) - self.textEdgeInsets.top - self.textEdgeInsets.bottom));
+        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) {
@@ -323,11 +294,79 @@
         } 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), CGRectGetHeight(_textLabel.frame)));
+            _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;
@@ -374,6 +413,16 @@
     }
 }
 
+- (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) {
@@ -383,6 +432,7 @@
     } else {
         self.sourceRect = self.sourceRect;
     }
+    _shouldInvalidateLayout = NO;
 }
 
 // 参数 targetRect 在 window 模式下是 window 的坐标系内的,如果是 subview 模式下则是 superview 坐标系内的
@@ -425,14 +475,36 @@
     
     if (self.isVerticalLayoutDirection) {
         // 保护tips最往左只能到达self.safetyMarginsAvoidSafeAreaInsets.left
-        CGFloat a = CGRectGetMidX(targetRect) - tipSize.width / 2;
+        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 = tipMaxX - (CGRectGetMaxX(containerRect) - self.safetyMarginsAvoidSafeAreaInsets.right);
+            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;
@@ -488,7 +560,9 @@
         // 调整浮层里的箭头的位置
         CGPoint targetRectCenter = CGPointGetCenterWithRect(targetRect);
         CGFloat selfMidX = targetRectCenter.x - CGRectGetMinX(self.frame);
-        _arrowMinX = selfMidX - self.arrowSizeAuto.width / 2;
+        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;
@@ -639,28 +713,43 @@
     
     if (animated) {
         if (isShowingByWindowMode) {
-            self.popupWindow.alpha = 0;
+            self.popupWindow.rootViewController.view.alpha = 0;// 请操作 vc.view.alpha 而不是 window.alpha,如果是后者,会导致 popup 显示出来前有一小段时间无法屏蔽界面的触摸事件,从而引发一些状态混乱问题
         } else {
             self.alpha = 0;
         }
         self.layer.transform = CATransform3DMakeScale(0.98, 0.98, 1);
-        [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.3 initialSpringVelocity:12 options:UIViewAnimationOptionCurveLinear animations:^{
-            self.layer.transform = CATransform3DMakeScale(1, 1, 1);
-        } completion:^(BOOL finished) {
-            if (completion) {
-                completion(finished);
-            }
-        }];
-        [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
-            if (isShowingByWindowMode) {
-                self.popupWindow.alpha = 1;
-            } else {
-                self.alpha = 1;
-            }
-        } completion:nil];
+        if (self.showingAnimationBlock) {
+            self.showingAnimationBlock(^{
+                self.layer.transform = CATransform3DMakeScale(1, 1, 1);
+                if (isShowingByWindowMode) {
+                    self.popupWindow.rootViewController.view.alpha = 1;
+                } else {
+                    self.alpha = 1;
+                }
+            }, ^(BOOL finished) {
+                if (completion) {
+                    completion(finished);
+                }
+            }, isShowingByWindowMode, self.popupWindow.rootViewController.view, self);
+        } else {
+            [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.3 initialSpringVelocity:12 options:UIViewAnimationOptionCurveLinear animations:^{
+                self.layer.transform = CATransform3DMakeScale(1, 1, 1);
+            } completion:^(BOOL finished) {
+                if (completion) {
+                    completion(finished);
+                }
+            }];
+            [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
+                if (isShowingByWindowMode) {
+                    self.popupWindow.rootViewController.view.alpha = 1;
+                } else {
+                    self.alpha = 1;
+                }
+            } completion:nil];
+        }
     } else {
         if (isShowingByWindowMode) {
-            self.popupWindow.alpha = 1;
+            self.popupWindow.rootViewController.view.alpha = 1;
         } else {
             self.alpha = 1;
         }
@@ -682,15 +771,25 @@
     BOOL isShowingByWindowMode = !!self.popupWindow;
     
     if (animated) {
-        [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
+        void (^a)(void) = ^void(void) {
             if (isShowingByWindowMode) {
-                self.popupWindow.alpha = 0;
+                self.popupWindow.rootViewController.view.alpha = 0;
             } else {
                 self.alpha = 0;
             }
-        } completion:^(BOOL finished) {
+        };
+        void (^c)(BOOL finished) = ^void(BOOL finished) {
             [self hideCompletionWithWindowMode:isShowingByWindowMode completion:completion];
-        }];
+        };
+        if (self.hidingAnimationBlock) {
+            self.hidingAnimationBlock(a, c, isShowingByWindowMode, self.popupWindow.rootViewController.view, self);
+        } else {
+            [UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
+                a();
+            } completion:^(BOOL finished) {
+                c(finished);
+            }];
+        }
     } else {
         [self hideCompletionWithWindowMode:isShowingByWindowMode completion:completion];
     }
@@ -755,17 +854,17 @@
 
 /// 根据一个给定的大小(包含箭头,不含 distanceBetweenSource ),计算出符合这个大小的内容大小(去掉箭头和白色内部的 contentEdgeInsets 后)
 - (CGSize)contentSizeInSize:(CGSize)size {
-    CGSize contentSize = CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) - self.borderWidth * 2 - self.arrowSpacingInHorizontal, size.height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) - self.borderWidth * 2 - self.arrowSpacingInVertical);
+    CGSize contentSize = CGSizeMake(size.width - UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) - self.borderWidth - self.arrowSpacingInHorizontal - (self.arrowSpacingInHorizontal > 0 ? 0 : self.borderWidth), size.height - UIEdgeInsetsGetVerticalValue(self.contentEdgeInsets) - self.borderWidth - self.arrowSpacingInVertical - (self.arrowSpacingInVertical > 0 ? 0 : self.borderWidth));
     return contentSize;
 }
 
 /// 根据内容大小和外部限制的大小,计算出合适的self size(包含箭头)
 - (CGSize)sizeWithContentSize:(CGSize)contentSize sizeThatFits:(CGSize)sizeThatFits {
-    CGFloat resultWidth = contentSize.width + UIEdgeInsetsGetHorizontalValue(self.contentEdgeInsets) + self.borderWidth * 2 + self.arrowSpacingInHorizontal;
+    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 * 2 + self.arrowSpacingInVertical;
+    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);
     
@@ -802,6 +901,9 @@
 - (void)setArrowSize:(CGSize)arrowSize {
     if (!self.arrowImage) {
         _arrowSize = arrowSize;
+        if (self.isShowing) {
+            [self setNeedsUpdateLayout];
+        }
     }
 }
 
@@ -818,6 +920,34 @@
     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) {
@@ -835,6 +965,11 @@
 @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];
@@ -859,16 +994,16 @@
     BOOL isImageViewShowing = [self isSubviewShowing:_imageView];
     if (isImageViewShowing) {
         CGSize imageViewSize = [_imageView sizeThatFits:size];
-        resultSize.width += ceil(imageViewSize.width) + self.imageEdgeInsets.left;
-        resultSize.height += ceil(imageViewSize.height) + self.imageEdgeInsets.top;
+        resultSize.width += ceil(imageViewSize.width) + UIEdgeInsetsGetHorizontalValue(self.imageEdgeInsets);
+        resultSize.height += ceil(imageViewSize.height) + UIEdgeInsetsGetVerticalValue(self.imageEdgeInsets);
     }
     
     BOOL isTextLabelShowing = [self isSubviewShowing:_textLabel];
     if (isTextLabelShowing) {
-        CGSize textLabelLimitSize = CGSizeMake(size.width - resultSize.width - self.imageEdgeInsets.right, size.height);
+        CGSize textLabelLimitSize = CGSizeMake(size.width - resultSize.width - UIEdgeInsetsGetHorizontalValue(self.textEdgeInsets), size.height);
         CGSize textLabelSize = [_textLabel sizeThatFits:textLabelLimitSize];
-        resultSize.width += (isImageViewShowing ? self.imageEdgeInsets.right : 0) + ceil(textLabelSize.width) + self.textEdgeInsets.left;
-        resultSize.height = MAX(resultSize.height, ceil(textLabelSize.height) + self.textEdgeInsets.top);
+        resultSize.width += ceil(textLabelSize.width) + UIEdgeInsetsGetHorizontalValue(self.textEdgeInsets);
+        resultSize.height = MAX(resultSize.height, ceil(textLabelSize.height) + UIEdgeInsetsGetVerticalValue(self.textEdgeInsets));
     }
     return resultSize;
 }
@@ -893,6 +1028,7 @@
     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 更早才行

+ 94 - 17
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.h

@@ -15,12 +15,25 @@
 
 #import <UIKit/UIKit.h>
 #import "QMUIPopupContainerView.h"
-#import "QMUIPopupMenuItemProtocol.h"
-#import "QMUIPopupMenuBaseItem.h"
-#import "QMUIPopupMenuButtonItem.h"
+#import "QMUIPopupMenuItemViewProtocol.h"
+#import "QMUIPopupMenuItem.h"
+#import "QMUITableView.h"
+#import "QMUILabel.h"
+#import "QMUIPopupMenuItemView.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
+typedef NS_ENUM(NSInteger, QMUIPopupMenuSelectedStyle) {
+    QMUIPopupMenuSelectedStyleCheckmark,    // 小勾
+    QMUIPopupMenuSelectedStyleCheckbox,     // 圆形勾
+    QMUIPopupMenuSelectedStyleCustom,       // 自定义,默认不做任何表现,交给业务自行处理
+};
+
+typedef NS_ENUM(NSInteger, QMUIPopupMenuSelectedLayout) {
+    QMUIPopupMenuSelectedLayoutAtEnd,
+    QMUIPopupMenuSelectedLayoutAtStart,
+};
+
 /**
  *  用于弹出浮层里显示一行一行的菜单的控件。
  *  使用方式:
@@ -30,9 +43,17 @@ NS_ASSUME_NONNULL_BEGIN
  *  4. 通过为 sourceBarItem/sourceView/sourceRect 三者中的一个赋值,来决定浮层布局的位置(参考父类)。
  *  5. 调用 showWithAnimated: 即可显示(参考父类)。
  *
- *  注意,QMUIPopupMenuView 的大小默认是按内容自适应的(item 的 sizeThatFits),但同时又受 maximumWidth/minimumWidth 的限制。
+ *  注意,QMUIPopupMenuView 的大小默认是按内容自适应的(item 的 sizeThatFits),但同时又受 adjustsWidthAutomatically/maximumWidth/minimumWidth 的控制。
+ *
+ *  关于颜色的设置:
+ *  1. 如果整个菜单的颜色(包括图片、title、subtitle、checkmark、checkbox)均一致,则直接通过 menu.tintColor 设置即可,默认情况下这些元素的 tintColor 都是 nil,也即跟随 superview 的 tintColor 走。
+ *  2. 如果 item 里某个元素的颜色与整体相比有差异化的诉求,则需要继承 QMUIPopupMenuItemView 实现一个子类,在子类的 setHighlighted:、setSelected:、tintColorDidChange 里处理,然后通过 menu.itemViewGenerator 返回这个子类。
+ *  3. 特别的,QMUIPopupMenuItem.image 默认会以 AlwaysTemplate 方式渲染,也即由 tintColor 决定图片颜色,可显式声明为 AlwaysOriginal 来保持图片原始的颜色。
  */
-@interface QMUIPopupMenuView : QMUIPopupContainerView
+@interface QMUIPopupMenuView : QMUIPopupContainerView<QMUITableViewDataSource, QMUITableViewDelegate>
+
+/// contentView 里的 scrollView,所有 itemButton 都是放在这里面的。
+@property(nonatomic, strong, readonly) QMUITableView *tableView;
 
 /// 是否需要显示每个 item 之间的分隔线,默认为 NO,当为 YES 时,每个 section 除了最后一个 item 外其他 item 底部都会显示分隔线。分隔线显示在当前 item 上方,不占位。
 @property(nonatomic, assign) BOOL shouldShowItemSeparator UI_APPEARANCE_SELECTOR;
@@ -43,23 +64,29 @@ NS_ASSUME_NONNULL_BEGIN
 /// item 分隔线的位置偏移,默认为 UIEdgeInsetsZero。item 分隔线的默认布局是 menuView 宽度减去左右 padding,如果你希望分隔线左右贴边则可为这个属性设置一个负值的 left/right。
 @property(nonatomic, assign) UIEdgeInsets itemSeparatorInset UI_APPEARANCE_SELECTOR;
 
-/// 是否显示 section 和 section 之间的分隔线,默认为 NO,当为 YES 时,除了最后一个 section,其他 section 底部都会显示一条分隔线。分隔线会显示在所在的 item 之上,不占位。
+/// item 分隔线的高度,默认为 PixelOne。分隔线拥有自己的占位,不与 item 重叠。
+@property(nonatomic, assign) CGFloat itemSeparatorHeight UI_APPEARANCE_SELECTOR;
+
+/// 是否显示 section 和 section 之间的分隔线,默认为 NO,当为 YES 时,除了最后一个 section,其他 section 底部都会显示一条分隔线。分隔线拥有自己的占位,不与 item、sectionSpacing 重叠。
 @property(nonatomic, assign) BOOL shouldShowSectionSeparator UI_APPEARANCE_SELECTOR;
 
-/// section 分隔线的颜色,默认为 UIColorSeparator。
+/// section 分隔线的颜色,默认为 UIColorSeparator。分隔线拥有自己的占位,不与 sectionSpacing 重叠。
 @property(nonatomic, strong, nullable) UIColor *sectionSeparatorColor UI_APPEARANCE_SELECTOR;
 
 /// section 分隔线的位置偏移,默认为 UIEdgeInsetsZero。section 分隔线的默认布局是撑满整个 menuView,如果你不希望分隔线左右贴边则可为这个属性设置一个 left/right 不为 0 的值即可。
 @property(nonatomic, assign) UIEdgeInsets sectionSeparatorInset UI_APPEARANCE_SELECTOR;
 
+/// section 分隔线的高度,默认为 PixelOne。
+@property(nonatomic, assign) CGFloat sectionSeparatorHeight UI_APPEARANCE_SELECTOR;
+
 /// section 之间的间隔,默认为0,也即贴合到一起。
 @property(nonatomic, assign) CGFloat sectionSpacing UI_APPEARANCE_SELECTOR;
 
-/// item 里文字的字体,默认为 UIFontMake(16)
-@property(nonatomic, strong, nullable) UIFont *itemTitleFont UI_APPEARANCE_SELECTOR;
+/// section 之间的间隔颜色,当 sectionSpacing > 0 时才有意义,默认为 UIColorSeparator
+@property(nonatomic, strong, nullable) UIColor *sectionSpacingColor UI_APPEARANCE_SELECTOR;
 
-/// item 里文字的颜色,默认为 UIColorBlue
-@property(nonatomic, strong, nullable) UIColor *itemTitleColor UI_APPEARANCE_SELECTOR;
+/// 批量设置 sectionTitleLabel 的样式
+@property(nonatomic, copy, nullable) void (^sectionTitleConfigurationHandler)(__kindof QMUIPopupMenuView *aMenuView, QMUILabel *sectionTitleLabel, NSInteger section);
 
 /// 整个 menuView 内部上下左右的 padding,其中 padding.left/right 会被作为 item.button.contentEdgeInsets.left/right,也即每个 item 的宽度一定是撑满整个 menuView 的。
 @property(nonatomic, assign) UIEdgeInsets padding UI_APPEARANCE_SELECTOR;
@@ -68,17 +95,67 @@ NS_ASSUME_NONNULL_BEGIN
 /// 如果将 itemHeight 设置为 QMUIViewSelfSizingHeight 则会以 item sizeThatFits: 返回的结果作为最终的 item 高度。
 @property(nonatomic, assign) CGFloat itemHeight UI_APPEARANCE_SELECTOR;
 
-/// 批量设置 item 的样式
-@property(nonatomic, copy, nullable) void (^itemConfigurationHandler)(QMUIPopupMenuView *aMenuView, __kindof QMUIPopupMenuBaseItem *aItem, NSInteger section, NSInteger index);
+/// 默认 YES,也即会自动计算每个 item 的宽度,取其中最宽的值作为整个 menu 的宽度。
+/// 当数据量大的情况下请手动置为 NO 并改为用 maximumWidth、minimumWidth 控制 menu 宽度,从而获取更优的性能。
+@property(nonatomic, assign) BOOL adjustsWidthAutomatically;
+
+/// item、sectionTitle 之间是否复用以提升性能,默认为 NO。
+/// 当数据量大或有复杂异步场景的情况下可改为 YES。
+/// 若需要修改值,建议在设置 items/sectionItems 之前就先设置好。
+@property(nonatomic, assign) BOOL shouldReuseItems;
+
+/// 当需要创建一个 itemView 时会试图从这个 block 获取,若业务没实现这个 block,则默认返回一个 @c QMUIPopupMenuItemView 实例。
+@property(nonatomic, copy, nullable) __kindof UIControl<QMUIPopupMenuItemViewProtocol> * (^itemViewGenerator)(__kindof QMUIPopupMenuView *aMenuView);
 
-/// 如果 items 是 QMUIPopupMenuButtonItem 或其子类,则当任一  item 被点击前,都会先调用这个 block。
-@property(nonatomic, copy, nullable) void (^willHandleButtonItemEventBlock)(QMUIPopupMenuView *aMenuView, __kindof QMUIPopupMenuButtonItem *aItem, NSInteger section, NSInteger index);
+/// 批量设置 itemView 的样式
+@property(nonatomic, copy, nullable) void (^itemViewConfigurationHandler)(__kindof QMUIPopupMenuView *aMenuView, __kindof QMUIPopupMenuItem *aItem, __kindof UIControl<QMUIPopupMenuItemViewProtocol> *aItemView, NSInteger section, NSInteger index);
 
 /// 设置 item,均处于同一个 section 内
-@property(nonatomic, copy, nullable) NSArray<__kindof QMUIPopupMenuBaseItem *> *items;
+@property(nonatomic, copy, nullable) NSArray<__kindof QMUIPopupMenuItem *> *items;
 
 /// 设置多个 section 的多个 item
-@property(nonatomic, copy, nullable) NSArray<NSArray<__kindof QMUIPopupMenuBaseItem *> *> *itemSections;
+@property(nonatomic, copy, nullable) NSArray<NSArray<__kindof QMUIPopupMenuItem *> *> *itemSections;
+
+/// 为每个 section 设置标题,不需要显示标题的 section 请使用空字符串占位。必须保证 @c sectionTitles 和 @c itemSections 长度相等。
+/// @note 请在设置 item、itemSections 之前先设置本属性。
+@property(nonatomic, copy, nullable) NSArray<NSString *> *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<NSIndexPath *> *selectedItemIndexPaths;
+
+/// 当处于 @c allowsSelection 模式时,默认每个 item 都可被选中。如果希望某个 item 不参与 selected 操作,可通过该 block 返回 NO 来实现。
+/// 如果想实现“最少选择n个”或“选择任意一个后无法再清空选择”的交互,也可通过这个 block 实现。
+@property(nonatomic, copy, nullable) BOOL (^shouldSelectItemBlock)(__kindof QMUIPopupMenuItem *aItem, __kindof UIControl<QMUIPopupMenuItemViewProtocol> *aItemView, NSInteger section, NSInteger index);
+
+/// 固定显示在菜单底部的 view,不跟随滚动,大小通过调用自身的 sizeThatFits: 获取。
+/// @note 菜单的 padding 会作用在 item 上(也即列表),不会作用在 bottomAccessoryView 上,bottomAccessoryView 始终都是宽度撑满菜单,底部紧贴菜单。
+@property(nonatomic, strong, nullable) __kindof UIView *bottomAccessoryView;
+
+/// 刷新当前菜单的内容及布局
+- (void)reload;
 
 @end
 

+ 481 - 182
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIPopupMenuView/QMUIPopupMenuView.m

@@ -18,12 +18,92 @@
 #import "UIView+QMUI.h"
 #import "CALayer+QMUI.h"
 #import "NSArray+QMUI.h"
+#import "UIFont+QMUI.h"
+#import "UITableViewCell+QMUI.h"
 
-@interface QMUIPopupMenuView ()
+@interface QMUIPopupMenuCell : UITableViewCell
+@property(nonatomic, strong) __kindof UIControl<QMUIPopupMenuItemViewProtocol> *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<QMUIPopupMenuItemViewProtocol> *)itemView {
+    if (_itemView) return;
+    _itemView = itemView;
+    [self.contentView addSubview:itemView];
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+    CGSize result = [self.itemView sizeThatFits:size];
+    result.height += self.qmui_borderWidth;
+    return result;
+}
+
+- (void)layoutSubviews {
+    [super layoutSubviews];
+    self.itemView.frame = CGRectInsetEdges(self.contentView.bounds, UIEdgeInsetsMake(0, 0, self.qmui_borderWidth, 0));
+}
+
+@end
+
+@interface QMUIPopupMenuSectionHeaderView : UITableViewHeaderFooterView
+@property(nonatomic, strong) QMUILabel *label;
+@end
+
+@implementation QMUIPopupMenuSectionHeaderView
+
+- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier {
+    if (self = [super initWithReuseIdentifier:reuseIdentifier]) {
+        _label = QMUILabel.new;
+        _label.numberOfLines = 0;
+        _label.font = UIFontMediumMake(13);
+        _label.textColor = UIColorGray;
+        _label.contentEdgeInsets = UIEdgeInsetsMake(12, 16, 2, 16);
+        [self.contentView addSubview:self.label];
+    }
+    return self;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+    return [self.label sizeThatFits:size];
+}
 
-@property(nonatomic, strong) UIScrollView *scrollView;
-@property(nonatomic, strong) NSMutableArray<CALayer *> *itemSeparatorLayers;
-@property(nonatomic, strong) NSMutableArray<CALayer *> *sectionSeparatorLayers;
+- (void)layoutSubviews {
+    [super layoutSubviews];
+    self.label.frame = self.contentView.bounds;
+}
+
+@end
+
+@interface QMUIPopupMenuSectionFooterView : UITableViewHeaderFooterView
+@end
+
+@implementation QMUIPopupMenuSectionFooterView
+
+- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier {
+    if (self = [super initWithReuseIdentifier:reuseIdentifier]) {
+        self.backgroundView = [[UIView alloc] init];// 去掉默认的背景,以便屏蔽系统对背景色的控制
+    }
+    return self;
+}
+
+// 系统的 UITableViewHeaderFooterView 不允许修改 backgroundColor,都应该放到 backgroundView 里,但却没有在文档中写明,只有不小心误用时才会在 Xcode 控制台里提示,所以这里做个转换,保护误用的情况。
+- (void)setBackgroundColor:(UIColor *)backgroundColor {
+//    [super setBackgroundColor:backgroundColor];
+    self.backgroundView.backgroundColor = backgroundColor;
+}
+
+@end
+
+@interface QMUIPopupMenuView ()<QMUITableViewDataSource, QMUITableViewDelegate>
 @end
 
 @interface QMUIPopupMenuView (UIAppearance)
@@ -33,238 +113,453 @@
 
 @implementation QMUIPopupMenuView
 
-- (void)setItems:(NSArray<__kindof QMUIPopupMenuBaseItem *> *)items {
-    [_items enumerateObjectsUsingBlock:^(__kindof QMUIPopupMenuBaseItem * _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) {
-        item.menuView = nil;
-    }];
+- (void)setItems:(NSArray<__kindof QMUIPopupMenuItem *> *)items {
     _items = items;
-    if (!items) {
-        self.itemSections = nil;
-    } else {
-        self.itemSections = @[_items];
-    }
+    self.itemSections = items ? @[_items] : nil;
 }
 
-- (void)setItemSections:(NSArray<NSArray<__kindof QMUIPopupMenuBaseItem *> *> *)itemSections {
-    [_itemSections qmui_enumerateNestedArrayWithBlock:^(__kindof QMUIPopupMenuBaseItem * item, BOOL *stop) {
+- (void)setItemSections:(NSArray<NSArray<__kindof QMUIPopupMenuItem *> *> *)itemSections {
+    [_itemSections qmui_enumerateNestedArrayWithBlock:^(__kindof QMUIPopupMenuItem *item, BOOL *stop) {
         item.menuView = nil;
     }];
     _itemSections = itemSections;
-    [self configureItems];
+    [_itemSections qmui_enumerateNestedArrayWithBlock:^(__kindof QMUIPopupMenuItem *item, BOOL * _Nonnull stop) {
+        item.menuView = self;
+    }];
+    [self reload];// 涉及到数据的必须立即刷新,否则容易因为异步导致 cell 里的 view 和当前的 item 不匹配的 bug
 }
 
-- (void)setItemConfigurationHandler:(void (^)(QMUIPopupMenuView *, __kindof QMUIPopupMenuBaseItem *, NSInteger, NSInteger))itemConfigurationHandler {
-    _itemConfigurationHandler = [itemConfigurationHandler copy];
-    if (_itemConfigurationHandler && self.itemSections.count) {
-        for (NSInteger section = 0, sectionCount = self.itemSections.count; section < sectionCount; section ++) {
-            NSArray<QMUIPopupMenuBaseItem *> *items = self.itemSections[section];
-            for (NSInteger row = 0, rowCount = items.count; row < rowCount; row ++) {
-                QMUIPopupMenuBaseItem *item = items[row];
-                _itemConfigurationHandler(self, item, section, row);
-            }
+- (void)setSectionTitles:(NSArray<NSString *> *)sectionTitles {
+    _sectionTitles = sectionTitles;
+    [self reload];
+}
+
+- (void)setItemViewConfigurationHandler:(void (^)(__kindof QMUIPopupMenuView * _Nonnull, __kindof QMUIPopupMenuItem * _Nonnull, __kindof UIControl<QMUIPopupMenuItemViewProtocol> * _Nonnull, NSInteger, NSInteger))itemViewConfigurationHandler {
+    _itemViewConfigurationHandler = [itemViewConfigurationHandler copy];
+    [self setNeedsReload];
+}
+
+- (void)setSectionTitleConfigurationHandler:(void (^)(__kindof QMUIPopupMenuView * _Nonnull, QMUILabel * _Nonnull, NSInteger))sectionTitleConfigurationHandler {
+    _sectionTitleConfigurationHandler = [sectionTitleConfigurationHandler copy];
+    [self setNeedsReload];
+}
+
+- (void)setPadding:(UIEdgeInsets)padding {
+    _padding = padding;
+    self.tableView.contentInset = UIEdgeInsetsMake(padding.top, self.tableView.contentInset.left, padding.bottom, self.tableView.contentInset.right);
+    [self setNeedsReload];
+}
+
+- (void)setShouldShowItemSeparator:(BOOL)shouldShowItemSeparator {
+    _shouldShowItemSeparator = shouldShowItemSeparator;
+    [self setNeedsReload];
+}
+
+- (void)setItemSeparatorInset:(UIEdgeInsets)itemSeparatorInset {
+    _itemSeparatorInset = itemSeparatorInset;
+    [self setNeedsReload];
+}
+
+- (void)setShouldShowSectionSeparator:(BOOL)shouldShowSectionSeparator {
+    _shouldShowSectionSeparator = shouldShowSectionSeparator;
+    [self setNeedsReload];
+}
+
+- (void)setSectionSeparatorHeight:(CGFloat)sectionSeparatorHeight {
+    _sectionSeparatorHeight = sectionSeparatorHeight;
+    [self setNeedsReload];
+}
+
+- (void)setItemHeight:(CGFloat)itemHeight {
+    _itemHeight = itemHeight;
+    [self setNeedsReload];
+}
+
+- (void)setSelectedStyle:(QMUIPopupMenuSelectedStyle)selectedStyle {
+    _selectedStyle = selectedStyle;
+    [self setNeedsReload];
+}
+
+- (void)setSelectedLayout:(QMUIPopupMenuSelectedLayout)selectedLayout {
+    _selectedLayout = selectedLayout;
+    [self setNeedsReload];
+}
+
+- (void)setAllowsSelection:(BOOL)allowsSelection {
+    _allowsSelection = allowsSelection;
+    if (!allowsSelection) {
+        self.selectedItemIndexPaths = nil;
+    }
+    [self setNeedsReload];
+}
+
+- (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection {
+    _allowsMultipleSelection = allowsMultipleSelection;
+    if (allowsMultipleSelection) {
+        _allowsSelection = YES;
+    } else {
+        if (self.selectedItemIndexPaths.count > 1) {
+            self.selectedItemIndexPaths = [self.selectedItemIndexPaths subarrayWithRange:NSMakeRange(0, 1)];
         }
     }
+    [self setNeedsReload];
 }
 
-- (void)configureItems {
-    __block NSInteger globalItemIndex = 0;
-    __block NSInteger separatorIndex = 0;
-    
-    // 移除所有 item
-    [self.scrollView qmui_removeAllSubviews];
-    [self.itemSeparatorLayers enumerateObjectsUsingBlock:^(CALayer * _Nonnull layer, NSUInteger idx, BOOL * _Nonnull stop) {
-        layer.hidden = YES;
-    }];
-    [self.sectionSeparatorLayers enumerateObjectsUsingBlock:^(CALayer * _Nonnull layer, NSUInteger idx, BOOL * _Nonnull stop) {
-        layer.hidden = YES;
-    }];
-    
-    [self enumerateItemsWithBlock:^(QMUIPopupMenuBaseItem *item, NSInteger section, NSInteger sectionCount, NSInteger row, NSInteger rowCount) {
-        item.menuView = self;
-        [item updateAppearance];
-        if (self.itemConfigurationHandler) {
-            self.itemConfigurationHandler(self, item, section, row);
+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<NSIndexPath *> *)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];
         }
-        [self.scrollView addSubview:item];
-        
-        // 配置分隔线,注意每一个 section 里的最后一行是不显示分隔线的
-        BOOL shouldShowItemSeparator = self.shouldShowItemSeparator && row < rowCount - 1;
-        if (shouldShowItemSeparator) {
-            CALayer *separatorLayer = nil;
-            if (separatorIndex < self.itemSeparatorLayers.count) {
-                separatorLayer = self.itemSeparatorLayers[separatorIndex];
-            } else {
-                separatorLayer = [CALayer qmui_separatorLayer];
-                [self.scrollView.layer addSublayer:separatorLayer];
-                [self.itemSeparatorLayers addObject:separatorLayer];
+    });
+}
+
+- (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];
             }
-            separatorLayer.hidden = NO;
-            separatorLayer.backgroundColor = self.itemSeparatorColor.CGColor;
-            separatorIndex++;
         }
-        
-        globalItemIndex++;
-    }];
+    }
+    return nil;
+}
+
+- (void)handleItemViewEvent:(UIControl<QMUIPopupMenuItemViewProtocol> *)itemView {
+    NSIndexPath *indexPath = [self indexPathForItem:itemView.item];
+    if (!indexPath) {
+        NSAssert(NO, @"the indexPath for the item could not be found");
+        return;
+    }
     
-    for (NSInteger section = 0, sectionCount = self.itemSections.count; section < sectionCount; section ++) {
-        BOOL shouldShowSectionSeparator = self.shouldShowSectionSeparator && section < sectionCount - 1;
-        if (shouldShowSectionSeparator) {
-            CALayer *separatorLayer = nil;
-            if (section < self.sectionSeparatorLayers.count) {
-                separatorLayer = self.sectionSeparatorLayers[section];
+    if (self.allowsSelection) {
+        BOOL shouldSelectItem = YES;
+        if (self.shouldSelectItemBlock) {
+            shouldSelectItem = self.shouldSelectItemBlock(itemView.item, itemView, indexPath.section, indexPath.row);
+        }
+        if (shouldSelectItem) {
+            NSMutableArray<NSIndexPath *> *selectedIndexPaths = self.selectedItemIndexPaths ? self.selectedItemIndexPaths.mutableCopy : [[NSMutableArray alloc] init];
+            if (self.allowsMultipleSelection) {
+                if (itemView.selected) {
+                    [selectedIndexPaths removeObject:indexPath];
+                } else {
+                    [selectedIndexPaths addObject:indexPath];
+                }
             } else {
-                separatorLayer = [CALayer qmui_separatorLayer];
-                [self.scrollView.layer addSublayer:separatorLayer];
-                [self.sectionSeparatorLayers addObject:separatorLayer];
+                // 单选,得把其他选中都清除
+                [selectedIndexPaths removeAllObjects];
+                if (!itemView.selected) {
+                    [selectedIndexPaths addObject:indexPath];
+                }
             }
-            separatorLayer.hidden = NO;
-            separatorLayer.backgroundColor = self.sectionSeparatorColor.CGColor;
+            self.selectedItemIndexPaths = selectedIndexPaths.copy;
         }
     }
+    
+    if (itemView.item.handler) {
+        itemView.item.handler(itemView.item, itemView, indexPath.section, indexPath.row);
+    }
 }
 
-- (void)setItemSeparatorInset:(UIEdgeInsets)itemSeparatorInset {
-    _itemSeparatorInset = itemSeparatorInset;
-    [self setNeedsLayout];
+- (void)setBottomAccessoryView:(__kindof UIView *)bottomAccessoryView {
+    if (bottomAccessoryView != _bottomAccessoryView) {
+        [_bottomAccessoryView removeFromSuperview];
+    }
+    _bottomAccessoryView = bottomAccessoryView;
+    [self.contentView addSubview:_bottomAccessoryView];
+    [self setNeedsUpdateLayout];
 }
 
-- (void)setItemSeparatorColor:(UIColor *)itemSeparatorColor {
-    _itemSeparatorColor = itemSeparatorColor;
-    [self.itemSeparatorLayers enumerateObjectsUsingBlock:^(CALayer * _Nonnull layer, NSUInteger idx, BOOL * _Nonnull stop) {
-        layer.backgroundColor = itemSeparatorColor.CGColor;
-    }];
+- (void)tintColorDidChange {
+    [super tintColorDidChange];
+    [self setNeedsReload];
 }
 
-- (void)setSectionSeparatorInset:(UIEdgeInsets)sectionSeparatorInset {
-    _sectionSeparatorInset = sectionSeparatorInset;
-    [self setNeedsLayout];
+- (NSString *)reuseIdentifierAtIndexPath:(NSIndexPath *)indexPath forType:(NSInteger)type {
+    if (self.shouldReuseItems) {
+        return @[@"cell", @"header", @"footer"][type];
+    }
+    if (type == 0) {
+        QMUIPopupMenuItem *item = self.itemSections[indexPath.section][indexPath.row];
+        return [NSString stringWithFormat:@"cell_%p", item];
+    }
+    if (type == 1) {
+        return [NSString stringWithFormat:@"header_%p", self.itemSections[indexPath.section]];
+    }
+    if (type == 2) {
+        return [NSString stringWithFormat:@"footer_%p", self.itemSections[indexPath.section]];
+    }
+    return nil;
 }
 
-- (void)setSectionSpacing:(CGFloat)sectionSpacing {
-    _sectionSpacing = sectionSpacing;
-    [self setNeedsLayout];
+#pragma mark - <QMUITableViewDataSource, QMUITableViewDelegate>
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
+    return self.itemSections.count;
 }
 
-- (void)setSectionSeparatorColor:(UIColor *)sectionSeparatorColor {
-    _sectionSeparatorColor = sectionSeparatorColor;
-    [self.sectionSeparatorLayers enumerateObjectsUsingBlock:^(CALayer * _Nonnull layer, NSUInteger idx, BOOL * _Nonnull stop) {
-        layer.backgroundColor = sectionSeparatorColor.CGColor;
-    }];
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+    return self.itemSections[section].count;
 }
 
-- (void)setItemTitleFont:(UIFont *)itemTitleFont {
-    _itemTitleFont = itemTitleFont;
-    [self enumerateItemsWithBlock:^(QMUIPopupMenuBaseItem *item, NSInteger section, NSInteger sectionCount, NSInteger row, NSInteger rowCount) {
-        [item updateAppearance];
-    }];
+- (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<QMUIPopupMenuItemViewProtocol> *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;
 }
 
-- (void)setItemTitleColor:(UIColor *)itemTitleColor {
-    _itemTitleColor = itemTitleColor;
-    [self enumerateItemsWithBlock:^(QMUIPopupMenuBaseItem *item, NSInteger section, NSInteger sectionCount, NSInteger row, NSInteger rowCount) {
-        [item updateAppearance];
-    }];
+- (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
 }
 
-- (void)setPadding:(UIEdgeInsets)padding {
-    _padding = padding;
-    [self enumerateItemsWithBlock:^(QMUIPopupMenuBaseItem *item, NSInteger section, NSInteger sectionCount, NSInteger row, NSInteger rowCount) {
-        [item updateAppearance];
-    }];
+- (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;
 }
 
-- (void)enumerateItemsWithBlock:(void (^)(QMUIPopupMenuBaseItem *item, NSInteger section, NSInteger sectionCount, NSInteger row, NSInteger rowCount))block {
-    if (!block) return;
-    for (NSInteger section = 0, sectionCount = self.itemSections.count; section < sectionCount; section ++) {
-        NSArray<QMUIPopupMenuBaseItem *> *items = self.itemSections[section];
-        for (NSInteger row = 0, rowCount = items.count; row < rowCount; row ++) {
-            QMUIPopupMenuBaseItem *item = items[row];
-            block(item, section, sectionCount, row, rowCount);
+- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
+    if (section >= self.sectionTitles.count) return CGFLOAT_MIN;
+    NSString *string = self.sectionTitles[section];
+    if (!string.length) return CGFLOAT_MIN;
+    return UITableViewAutomaticDimension;
+}
+
+- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
+    BOOL shouldShowSectionSeparator = self.shouldShowSectionSeparator && self.sectionSeparatorHeight;
+    BOOL shouldShowSectionFooter = shouldShowSectionSeparator || self.sectionSpacing > 0;
+    if (shouldShowSectionFooter && section != tableView.numberOfSections - 1) {
+        NSString *identifier = [self reuseIdentifierAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section] forType:2];
+        QMUIPopupMenuSectionFooterView *footer = [tableView dequeueReusableHeaderFooterViewWithIdentifier:identifier];
+        if (!footer) {
+            footer = [[QMUIPopupMenuSectionFooterView alloc] initWithReuseIdentifier:identifier];
+        }
+        if (shouldShowSectionSeparator) {
+            footer.qmui_borderPosition = QMUIViewBorderPositionTop;
+            footer.qmui_borderWidth = self.sectionSeparatorHeight;
+            footer.qmui_borderColor = self.sectionSeparatorColor;
+            footer.qmui_borderInsets = self.sectionSeparatorInset;
+        } else {
+            footer.qmui_borderPosition = QMUIViewBorderPositionNone;
+        }
+        if (self.sectionSpacing > 0) {
+            footer.backgroundColor = self.sectionSpacingColor;
+        } else {
+            footer.backgroundColor = nil;
         }
+        return footer;
     }
+    return nil;
+}
+
+- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
+    if (section == tableView.numberOfSections - 1) {
+        return CGFLOAT_MIN;
+    }
+    CGFloat height = 0;
+    if (self.shouldShowSectionSeparator && self.sectionSeparatorHeight) {
+        height += self.sectionSeparatorHeight;
+    }
+    if (self.sectionSpacing > 0) {
+        height += self.sectionSpacing;
+    }
+    return height > 0 ? height : CGFLOAT_MIN;
 }
 
 #pragma mark - (UISubclassingHooks)
 
 - (void)didInitialize {
     [super didInitialize];
+    _adjustsWidthAutomatically = YES;
+    _selectedItemIndex = NSNotFound;
     self.contentEdgeInsets = UIEdgeInsetsZero;
     
-    self.scrollView = [[UIScrollView alloc] init];
-    self.scrollView.scrollsToTop = NO;
-    self.scrollView.showsHorizontalScrollIndicator = NO;
-    self.scrollView.showsVerticalScrollIndicator = NO;
-    self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
-    [self.contentView addSubview:self.scrollView];
-    
-    self.itemSeparatorLayers = [[NSMutableArray alloc] init];
-    self.sectionSeparatorLayers = [[NSMutableArray alloc] init];
+    _tableView = [[QMUITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
+    self.tableView.scrollsToTop = NO;
+    self.tableView.alwaysBounceHorizontal = NO;
+    self.tableView.alwaysBounceVertical = NO;
+    self.tableView.showsHorizontalScrollIndicator = NO;
+    self.tableView.showsVerticalScrollIndicator = NO;
+    self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
+    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
+    self.tableView.backgroundColor = nil;
+    self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, CGFLOAT_MIN)];
+    self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, CGFLOAT_MIN)];// 避免尾部出现20pt空白
+    self.tableView.backgroundView = UIView.new;
+    self.tableView.estimatedRowHeight = self.itemHeight;
+    self.tableView.estimatedSectionHeaderHeight = 20;
+    self.tableView.dataSource = self;
+    self.tableView.delegate = self;
+    [self.contentView addSubview:self.tableView];
     
     [self updateAppearanceForPopupMenuView];
 }
 
 - (CGSize)sizeThatFitsInContentView:(CGSize)size {
-    __block CGFloat width = 0;
-    __block CGFloat height = UIEdgeInsetsGetVerticalValue(self.padding);
-    [self.itemSections qmui_enumerateNestedArrayWithBlock:^(__kindof QMUIPopupMenuBaseItem *item, BOOL *stop) {
-        CGSize itemSize = [item sizeThatFits:CGSizeMake(size.width, CGFLOAT_MAX)];
-        CGFloat itemHeight = item.height;
-        if (itemHeight < 0) {
-            itemHeight = self.itemHeight;
+    __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;
         }
-        // QMUIViewSelfSizingHeight
-        if (isinf(itemHeight)) {
-            itemHeight = itemSize.height;
+        if (self.shouldShowItemSeparator) {
+            result.height -= self.itemSections.count * self.itemSeparatorHeight;// 减去每个 section 结尾的那个 item 的分隔线
         }
-        height += itemHeight;
-        width = MAX(width, MIN(itemSize.width, size.width));
-    }];
-    height += (self.itemSections.count - 1) * self.sectionSpacing;
-    size.width = width;
-    size.height = height;
-    return size;
+    }
+    if (self.bottomAccessoryView) {
+        CGSize accessoryViewSize = [self.bottomAccessoryView sizeThatFits:CGSizeMake(size.width, CGFLOAT_MAX)];
+        result.height += accessoryViewSize.height;
+    }
+    result.height += UIEdgeInsetsGetVerticalValue(self.padding);// contentInset 不在系统 sizeThatFits: 返回结果内,要自己加
+    return result;
 }
 
 - (void)layoutSubviews {
     [super layoutSubviews];
-    self.scrollView.frame = self.contentView.bounds;
-    
-    CGFloat minY = self.padding.top;
-    CGFloat contentWidth = CGRectGetWidth(self.scrollView.bounds);
-    NSInteger separatorIndex = 0;
-    for (NSInteger section = 0, sectionCount = self.itemSections.count; section < sectionCount; section ++) {
-        NSArray<QMUIPopupMenuBaseItem *> *items = self.itemSections[section];
-        for (NSInteger row = 0, rowCount = items.count; row < rowCount; row ++) {
-            QMUIPopupMenuBaseItem *item = items[row];
-            CGFloat itemHeight = item.height;
-            if (itemHeight < 0) {
-                itemHeight = self.itemHeight;
-            }
-            if (isinf(itemHeight)) {
-                itemHeight = [item sizeThatFits:CGSizeMake(contentWidth, CGFLOAT_MAX)].height;
-            }
-            item.frame = CGRectMake(0, minY, contentWidth, itemHeight);
-            minY = CGRectGetMaxY(item.frame);
-            
-            if (self.shouldShowItemSeparator && row < rowCount - 1) {
-                CALayer *layer = self.itemSeparatorLayers[separatorIndex];
-                if (!layer.hidden) {
-                    layer.frame = CGRectMake(self.padding.left + self.itemSeparatorInset.left, minY - PixelOne + self.itemSeparatorInset.top - self.itemSeparatorInset.bottom, contentWidth - UIEdgeInsetsGetHorizontalValue(self.padding) - UIEdgeInsetsGetHorizontalValue(self.itemSeparatorInset), PixelOne);
-                    separatorIndex++;
-                }
-            }
-        }
-        
-        if (section < sectionCount - 1) {
-            if (self.shouldShowSectionSeparator) {
-                self.sectionSeparatorLayers[section].frame = CGRectMake(0, minY - PixelOne + self.sectionSeparatorInset.top - self.sectionSeparatorInset.bottom, contentWidth - UIEdgeInsetsGetHorizontalValue(self.sectionSeparatorInset), PixelOne);
-            }
-            
-            minY += self.sectionSpacing;
-        }
+    CGRect contentRect = self.contentView.bounds;
+    if (self.bottomAccessoryView) {
+        CGSize accessoryViewSize = [self.bottomAccessoryView sizeThatFits:CGSizeMake(CGRectGetWidth(contentRect), CGFLOAT_MAX)];
+        self.bottomAccessoryView.frame = CGRectMake(0, CGRectGetHeight(contentRect) - accessoryViewSize.height, CGRectGetWidth(contentRect), accessoryViewSize.height);
+        contentRect = CGRectSetHeight(contentRect, CGRectGetMinY(self.bottomAccessoryView.frame));
     }
-    minY += self.padding.bottom;
-    self.scrollView.contentSize = CGSizeMake(contentWidth, minY);
+    self.tableView.frame = contentRect;
 }
 
 @end
@@ -280,14 +575,16 @@
 
 + (void)setDefaultAppearanceForPopupMenuView {
     QMUIPopupMenuView *appearance = [QMUIPopupMenuView appearance];
-    appearance.shouldShowItemSeparator = NO;
+    appearance.shouldShowItemSeparator = YES;
     appearance.itemSeparatorColor = UIColorSeparator;
     appearance.itemSeparatorInset = UIEdgeInsetsZero;
-    appearance.shouldShowSectionSeparator = NO;
+    appearance.itemSeparatorHeight = PixelOne;
+    appearance.shouldShowSectionSeparator = YES;
     appearance.sectionSeparatorColor = UIColorSeparator;
     appearance.sectionSeparatorInset = UIEdgeInsetsZero;
-    appearance.itemTitleFont = UIFontMake(16);
-    appearance.itemTitleColor = UIColorBlue;
+    appearance.sectionSeparatorHeight = PixelOne;
+    appearance.sectionSpacing = 8;
+    appearance.sectionSpacingColor = UIColorSeparator;
     appearance.padding = UIEdgeInsetsMake([QMUIPopupContainerView appearance].cornerRadius / 2.0, 16, [QMUIPopupContainerView appearance].cornerRadius / 2.0, 16);
     appearance.itemHeight = 44;
 }
@@ -297,12 +594,14 @@
     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.itemTitleFont = appearance.itemTitleFont;
-    self.itemTitleColor = appearance.itemTitleColor;
+    self.sectionSpacingColor = appearance.sectionSpacingColor;
     self.padding = appearance.padding;
     self.itemHeight = appearance.itemHeight;
 }

+ 30 - 7
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUISearchController.h

@@ -16,6 +16,8 @@
 #import "QMUICommonViewController.h"
 #import "QMUICommonTableViewController.h"
 
+NS_ASSUME_NONNULL_BEGIN
+
 @class QMUIEmptyView;
 @class QMUISearchController;
 
@@ -32,7 +34,7 @@
  *  搜索框文字发生变化时的回调,请自行调用 `[tableView reloadData]` 来更新界面。
  *  @warning 搜索框文字为空(例如第一次点击搜索框进入搜索状态时,或者文字全被删掉了,或者点击搜索框的×)也会走进来,此时参数searchString为@"",这是为了和系统的UISearchController保持一致
  */
-- (void)searchController:(QMUISearchController *)searchController updateResultsForSearchString:(NSString *)searchString;
+- (void)searchController:(QMUISearchController *)searchController updateResultsForSearchString:(nullable NSString *)searchString;
 
 @optional
 - (void)willPresentSearchController:(QMUISearchController *)searchController;
@@ -56,6 +58,11 @@
  */
 @interface QMUISearchController : QMUICommonViewController<UISearchResultsUpdating, UISearchControllerDelegate>
 
+- (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上添加搜索功能
@@ -70,18 +77,24 @@
 
 @property(nonatomic, weak) id<QMUISearchControllerDelegate> searchResultsDelegate;
 
+/// 内部使用的系统的 UISearchController 的引用
+@property(nonatomic, strong, readonly) UISearchController *searchController;
+
+/// 等价于 self.searchController.searchResultsController,展示搜索结果的 viewController。若通过 initWithContentsViewController:resultsTableViewStyle: 初始化,则默认的 searchResultsController 为 QMUICommonTableViewController 的子类。
+@property(nonatomic, strong, readonly, nullable) __kindof UIViewController *searchResultsController;
+
 /// 搜索框
 @property(nonatomic, strong, readonly) UISearchBar *searchBar;
 
-/// 搜索结果列表
-@property(nonatomic, strong, readonly) QMUITableView *tableView;
+/// 搜索结果列表,仅当通过 initWithContentsViewController: 或 initWithContentsViewController:resultsTableViewStyle: 初始化时才有效。
+@property(nonatomic, strong, readonly, nullable) QMUITableView *tableView;
 
 /// 在搜索文字为空时会展示的一个 view,通常用于实现“最近搜索”之类的功能。launchView 最终会被布局为撑满搜索框以下的所有空间。
-@property(nonatomic, strong) UIView *launchView;
+@property(nonatomic, strong, nullable) UIView *launchView;
 
 /// 升起键盘时的半透明遮罩,nil 表示用系统的,非 nil 则用自己的。默认为 nil。
 /// @note 如果使用了 launchView 则该属性无效。
-@property(nonatomic, strong) UIColor *dimmingColor;
+@property(nonatomic, strong, nullable) UIColor *dimmingColor;
 
 /// 控制以无动画的形式进入/退出搜索状态
 @property(nonatomic, assign, getter=isActive) BOOL active;
@@ -95,6 +108,14 @@
 
 /// 进入搜索状态时是否要把原界面的 navigationBar 推走,默认为 YES
 @property(nonatomic, assign) BOOL hidesNavigationBarDuringPresentation;
+
+/// 在展示搜索结果或者 launchView 时是否支持左侧屏幕边缘向右滑退出搜索,默认为 NO
+/// @warning 使用截图的方式实现,所以暂不支持横竖屏切换,请自行屏蔽横竖屏场景
+@property(nonatomic, assign) BOOL supportsSwipeToDismissSearch;
+
+/// 当开启了 supportsSwipeToDismissSearch 则在 willPresentSearchController: 里会创建这个手势对象
+@property(nonatomic, strong, readonly, nullable) UIScreenEdgePanGestureRecognizer *swipeGestureRecognizer;
+
 @end
 
 
@@ -115,7 +136,7 @@
  *
  *  @see QMUITableViewDelegate
  */
-@property(nonatomic, strong, readonly) QMUISearchController *searchController;
+@property(nonatomic, strong, readonly, nullable) QMUISearchController *searchController;
 
 /**
  *  获取当前的 searchBar,注意只有当 `shouldShowSearchBar` 为 `YES` 时才有用
@@ -124,7 +145,7 @@
  *
  *  @see QMUITableViewDelegate
  */
-@property(nonatomic, strong, readonly) UISearchBar *searchBar;
+@property(nonatomic, strong, readonly, nullable) UISearchBar *searchBar;
 
 /**
  *  是否应该在显示空界面时自动隐藏搜索框
@@ -143,3 +164,5 @@
 - (void)initSearchController;
 
 @end
+
+NS_ASSUME_NONNULL_END

+ 225 - 111
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUISearchController.m

@@ -24,6 +24,8 @@
 #import "NSObject+QMUI.h"
 #import "UIView+QMUI.h"
 #import "UIViewController+QMUI.h"
+#import "UISearchController+QMUI.h"
+#import "UIGestureRecognizer+QMUI.h"
 
 BeginIgnoreDeprecatedWarning
 
@@ -43,7 +45,12 @@ BeginIgnoreDeprecatedWarning
 
 - (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];
@@ -62,89 +69,25 @@ BeginIgnoreDeprecatedWarning
 
 @end
 
-@interface QMUICustomSearchController : UISearchController
-
-@property(nonatomic, strong) UIView *customDimmingView;
-@property(nonatomic, strong) UIColor *dimmingColor;
-@end
-
-@implementation QMUICustomSearchController
-
-- (instancetype)initWithSearchResultsController:(UIViewController *)searchResultsController {
-    if (self = [super initWithSearchResultsController:searchResultsController]) {
-        if (@available(iOS 15.0, *)) {
-            self.dimsBackgroundDuringPresentation = YES;// iOS 15 开始该默认值为 NO 了,为了保持与旧版本一致的表现,这里改默认值
-        }
-    }
-    return self;
-}
-
-- (void)setCustomDimmingView:(UIView *)customDimmingView {
-    if (_customDimmingView != customDimmingView) {
-        [_customDimmingView removeFromSuperview];
-    }
-    _customDimmingView = customDimmingView;
-    
-    self.dimsBackgroundDuringPresentation = !_customDimmingView;
-    if ([self isViewLoaded]) {
-        [self addCustomDimmingView];
-    }
-}
-
-- (void)viewWillAppear:(BOOL)animated {
-    [super viewWillAppear:animated];
-    [self addCustomDimmingView];
-}
-
-- (void)addCustomDimmingView {
-    UIView *superviewOfDimmingView = self.searchResultsController.view.superview;
-    if (self.customDimmingView && self.customDimmingView.superview != superviewOfDimmingView) {
-        [superviewOfDimmingView insertSubview:self.customDimmingView atIndex:0];
-        [self layoutCustomDimmingView];
-    }
-}
-
-- (void)layoutCustomDimmingView {
-    UIView *searchBarContainerView = nil;
-    for (UIView *subview in self.view.subviews) {
-        if ([NSStringFromClass(subview.class) isEqualToString:@"_UISearchBarContainerView"]) {
-            searchBarContainerView = subview;
-            break;
-        }
-    }
-    
-    self.customDimmingView.frame = CGRectInsetEdges(self.customDimmingView.superview.bounds, UIEdgeInsetsMake(searchBarContainerView ? CGRectGetMaxY(searchBarContainerView.frame) : 0, 0, 0, 0));
-}
-
-- (void)viewDidLayoutSubviews {
-    [super viewDidLayoutSubviews];
-    
-    if (self.customDimmingView) {
-        [UIView animateWithDuration:[CATransaction animationDuration] animations:^{
-            [self layoutCustomDimmingView];
-        }];
-    }
-}
-
-@end
-
-@interface QMUISearchController () <QMUISearchResultsTableViewControllerDelegate>
-
-@property(nonatomic,strong) QMUICustomSearchController *searchController;
+@interface QMUISearchController () <QMUISearchResultsTableViewControllerDelegate, UIGestureRecognizerDelegate>
+@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 resultsTableViewStyle:(UITableViewStyle)resultsTableViewStyle {
-    if (self = [self initWithNibName:nil bundle:nil]) {
+- (instancetype)initWithContentsViewController:(UIViewController *)viewController resultsViewController:(__kindof UIViewController *)resultsViewController {
+    if (self = [super initWithNibName:nil bundle:nil]) {
         // 将 definesPresentationContext 置为 YES 有两个作用:
         // 1、保证从搜索结果界面进入子界面后,顶部的searchBar不会依然停留在navigationBar上
         // 2、使搜索结果界面的tableView的contentInset.top正确适配searchBar
         viewController.definesPresentationContext = YES;
+        [QMUISearchController fixDefinesPresentationContextBug];
         
-        QMUISearchResultsTableViewController *searchResultsViewController = [[QMUISearchResultsTableViewController alloc] initWithStyle:resultsTableViewStyle];
-        searchResultsViewController.delegate = self;
-        self.searchController = [[QMUICustomSearchController alloc] initWithSearchResultsController:searchResultsViewController];
+        _searchController = [[UISearchController alloc] initWithSearchResultsController:resultsViewController];
+        self.searchController.obscuresBackgroundDuringPresentation = YES;// iOS 15 开始该默认值为 NO 了,为了保持与旧版本一致的表现,这里改默认值
         self.searchController.searchResultsUpdater = self;
         self.searchController.delegate = self;
         _searchBar = self.searchController.searchBar;
@@ -159,10 +102,41 @@ BeginIgnoreDeprecatedWarning
     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
@@ -177,37 +151,7 @@ BeginIgnoreDeprecatedWarning
 
 - (void)setDimmingColor:(UIColor *)dimmingColor {
     _dimmingColor = dimmingColor;
-    self.searchController.dimmingColor = dimmingColor;
-    [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) {
-                    if ([NSStringFromClass(subview.class) isEqualToString:[NSString qmui_stringByConcat:@"_", @"UISearchController", @"View", nil]]) {
-                        UISearchController *searchController = subview.qmui_viewController;
-                        if ([searchController isKindOfClass:UISearchController.class]) {
-                            if ([searchController respondsToSelector:@selector(dimmingColor)]) {
-                                BeginIgnorePerformSelectorLeaksWarning
-                                UIColor *color = [searchController performSelector:@selector(dimmingColor)];
-                                EndIgnorePerformSelectorLeaksWarning
-                                if (color) {
-                                    [selfObject qmui_performSelector:@selector(setDimmingColor:) withArguments:&color, nil];
-                                }
-                            }
-                        }
-                        
-                        break;
-                    }
-                }
-                
-                // call super
-                void (*originSelectorIMP)(id, SEL);
-                originSelectorIMP = (void (*)(id, SEL))originalIMPProvider();
-                originSelectorIMP(selfObject, originCMD);
-            };
-        });
-    } oncePerIdentifier:@"QMUISearchController dimmingColor"];
+    self.searchController.qmui_dimmingColor = dimmingColor;
 }
 
 - (BOOL)isActive {
@@ -219,16 +163,38 @@ BeginIgnoreDeprecatedWarning
 }
 
 - (void)setActive:(BOOL)active animated:(BOOL)animated {
-    self.searchController.active = active;
+    if (!animated) {
+        [UIView performWithoutAnimation:^{
+            self.searchController.active = active;
+            // animated:NO 的情况下设置 active:NO,取消按钮无法自动消失(系统 bug),所以这里手动管理
+            // 如果是 animated:YES 或者 active:YES 则没这个问题
+            // 这里修改了 searchBar.showsCancelButton 属性会让 automaticallyShowsCancelButton 变为 NO,且不能在这时候立马把它改为 YES,否则会立马出现取消按钮,所以改为在下一次 willPresentSearchController: 里重置为系统自动管理。
+            if (!active && self.searchController.automaticallyShowsCancelButton) {
+                self.searchController.searchBar.showsCancelButton = NO;
+                self.hasSetShowsCancelButton = YES;
+            }
+        }];
+    } else {
+        self.searchController.active = active;
+    }
 }
 
 - (UITableView *)tableView {
-    return ((QMUICommonTableViewController *)self.searchController.searchResultsController).tableView;
+    if ([self.searchResultsController respondsToSelector:@selector(tableView)]) {
+        BeginIgnorePerformSelectorLeaksWarning
+        return [self.searchResultsController performSelector:@selector(tableView)];
+        EndIgnorePerformSelectorLeaksWarning
+    }
+    return nil;
+}
+
+- (__kindof UIViewController *)searchResultsController {
+    return self.searchController.searchResultsController;
 }
 
-- (void)setLaunchView:(UIView *)dimmingView {
-    _launchView = dimmingView;
-    self.searchController.customDimmingView = _launchView;
+- (void)setLaunchView:(UIView *)launchView {
+    _launchView = launchView;
+    self.searchController.qmui_launchView = launchView;
 }
 
 - (BOOL)hidesNavigationBarDuringPresentation {
@@ -249,6 +215,113 @@ BeginIgnoreDeprecatedWarning
     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 - <UIGestureRecognizerDelegate>
+
+// 由于手势是加在 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 {
@@ -287,6 +360,11 @@ BeginIgnoreDeprecatedWarning
 #pragma mark - <UISearchResultsUpdating>
 
 - (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];
     }
@@ -295,6 +373,17 @@ BeginIgnoreDeprecatedWarning
 #pragma mark - <UISearchControllerDelegate>
 
 - (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];
     }
@@ -316,6 +405,11 @@ BeginIgnoreDeprecatedWarning
     if ([self.searchResultsDelegate respondsToSelector:@selector(willDismissSearchController:)]) {
         [self.searchResultsDelegate willDismissSearchController:self];
     }
+    
+    // 先手势返回触发各种对象的初始化,然后又取消手势,正常点取消按钮退出搜索,此时就不应该看到背后有截图存在了
+    if (!self.dismissBySwipe) {
+        [self cleanSnapshotObjects];
+    }
 }
 
 - (void)didDismissSearchController:(UISearchController *)searchController {
@@ -325,6 +419,10 @@ BeginIgnoreDeprecatedWarning
     if ([self.searchResultsDelegate respondsToSelector:@selector(didDismissSearchController:)]) {
         [self.searchResultsDelegate didDismissSearchController:self];
     }
+    
+    if (self.supportsSwipeToDismissSearch && !self.dismissBySwipe) {
+        [self cleanSnapshotObjects];
+    }
 }
 
 @end
@@ -400,7 +498,7 @@ static char kAssociatedObjectKey_shouldShowSearchBar;
 
 - (void)initSearchController {
     if ([self isViewLoaded] && self.shouldShowSearchBar && !self.searchController) {
-        self.searchController = [[QMUISearchController alloc] initWithContentsViewController:self resultsTableViewStyle:self.tableView.qmui_style];
+        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
@@ -420,3 +518,19 @@ static char kAssociatedObjectKey_shouldShowSearchBar;
 }
 
 @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

+ 1 - 1
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITableViewHeaderFooterView.m

@@ -60,7 +60,7 @@
 - (void)updateAppearance {
     if (!QMUICMIActivated || (!self.parentTableView && !self.qmui_tableView) || self.type == QMUITableViewHeaderFooterViewTypeUnknow) return;
     
-    UITableViewStyle style = (self.parentTableView ?: self.qmui_tableView).qmui_style;
+    UITableViewStyle style = (self.parentTableView ?: self.qmui_tableView).style;
     
     if (self.type == QMUITableViewHeaderFooterViewTypeHeader) {
         self.titleLabel.font = PreferredValueForTableViewStyle(style, TableViewSectionHeaderFont, TableViewGroupedSectionHeaderFont, TableViewInsetGroupedSectionHeaderFont);

+ 17 - 5
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITextField.m

@@ -143,6 +143,7 @@
 
 #pragma mark - Positioning Overrides
 
+// 这样写已经可以让 sizeThatFits 时高度加上 textInsets 的值了
 - (CGRect)textRectForBounds:(CGRect)bounds {
     bounds = CGRectInsetEdges(bounds, self.textInsets);
     CGRect resultRect = [super textRectForBounds:bounds];
@@ -222,7 +223,7 @@
                 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:shouldChange];
+                        shouldChange = [textField.delegate textField:textField shouldChangeCharactersInRange:range replacementString:allowedText originalValue:YES];
                     }
                     if (!shouldChange) {
                         return NO;
@@ -249,8 +250,7 @@
     }
     
     if ([textField.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:originalValue:)]) {
-        BOOL delegateValue = [textField.delegate textField:textField shouldChangeCharactersInRange:range replacementString:string originalValue:YES];
-        return delegateValue;
+        return [textField.delegate textField:textField shouldChangeCharactersInRange:range replacementString:string originalValue:YES];
     }
     
     return YES;
@@ -268,8 +268,20 @@
     
     if (!textField.markedTextRange) {
         if ([textField lengthWithString:textField.text] > textField.maximumTextLength) {
-            textField.text = [textField.text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, textField.maximumTextLength) lessValue:YES countingNonASCIICharacterAsTwo:textField.shouldCountingNonASCIICharacterAsTwo];
-            
+            NSString *text = nil;
+            NSInteger lastLength = textField.text.length - NSMaxRange(textField.qmui_selectedRange);// selectedRange 是系统的,所以这里按 shouldCountingNonASCIICharacterAsTwo = NO 来计算
+            if (lastLength > 0) {
+                // 光标在中间就触发了最长文本限制,要从前面截断,不要影响光标后面的原始文本
+                NSString *lastText = [textField.text substringFromIndex:NSMaxRange(textField.qmui_selectedRange)];
+                NSInteger lastLengthInQMUI = [textField lengthWithString:lastText];
+                NSInteger preLengthInQMUI = textField.maximumTextLength - lastLengthInQMUI;
+                NSString *preText = [textField.text qmui_substringAvoidBreakingUpCharacterSequencesToIndex:preLengthInQMUI lessValue:YES countingNonASCIICharacterAsTwo:textField.shouldCountingNonASCIICharacterAsTwo];
+                text = [preText stringByAppendingString:lastText];
+            } else {
+                text = [textField.text qmui_substringAvoidBreakingUpCharacterSequencesWithRange:NSMakeRange(0, textField.maximumTextLength) lessValue:YES countingNonASCIICharacterAsTwo:textField.shouldCountingNonASCIICharacterAsTwo];
+            }
+            textField.text = text;
+            textField.qmui_selectedRange = NSMakeRange(textField.text.length - lastLength, 0);
             if ([textField.delegate respondsToSelector:@selector(textField:didPreventTextChangeInRange:replacementString:)]) {
                 [textField.delegate textField:textField didPreventTextChangeInRange:textField.qmui_selectedRange replacementString:nil];
             }

+ 5 - 0
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITextView.h

@@ -101,6 +101,11 @@
  */
 @property(nonatomic, assign) CGFloat maximumHeight;
 
+/**
+ 在 textView:shouldChangeTextInRange:replacementText: 里可用这个属性判断当前是否点击了删除。特别注意,当输入框为空时继续点删除也会触发,且这种情况只能通过这个属性区分,无法用别的判断方式。
+ */
+@property(nonatomic, assign) BOOL isDeletingDuringTextChange;
+
 /**
  *  控制输入框是否要出现“粘贴”menu
  *  @param sender 触发这次询问事件的来源

+ 29 - 0
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITextView.m

@@ -98,6 +98,8 @@ const UIEdgeInsets kSystemTextViewFixTextInsets = {0, 5, 0, 5};
     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTextChanged:) name:UITextViewTextDidChangeNotification object:nil];
     
     self.postInitializationMethodCalled = YES;
+    
+    [self hookKeyboardDeleteEventIfNeeded];
 }
 
 - (void)dealloc {
@@ -368,6 +370,33 @@ const UIEdgeInsets kSystemTextViewFixTextInsets = {0, 5, 0, 5};
     }
 }
 
+- (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 - <UIResponderStandardEditActions>
 
 - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {

+ 5 - 11
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/QMUIThemeManager.m

@@ -43,28 +43,22 @@ NSString *const QMUIThemeDidChangeNotification = @"QMUIThemeDidChangeNotificatio
         _name = name;
         self._themeIdentifiers = NSMutableArray.new;
         self._themes = NSMutableArray.new;
-        if (@available(iOS 13.0, *)) {
-            [UITraitCollection qmui_addUserInterfaceStyleWillChangeObserver:self selector:@selector(handleUserInterfaceStyleWillChangeEvent:)];
-        }
+        [UITraitCollection qmui_addUserInterfaceStyleWillChangeObserver:self selector:@selector(handleUserInterfaceStyleWillChangeEvent:)];
     }
     return self;
 }
 
 - (void)handleUserInterfaceStyleWillChangeEvent:(UITraitCollection *)traitCollection {
     if (!_respondsSystemStyleAutomatically) return;
-    if (@available(iOS 13.0, *)) {
-        if (traitCollection && self.identifierForTrait) {
-            self.currentThemeIdentifier = self.identifierForTrait(traitCollection);
-        }
+    if (traitCollection && self.identifierForTrait) {
+        self.currentThemeIdentifier = self.identifierForTrait(traitCollection);
     }
 }
 
 - (void)setRespondsSystemStyleAutomatically:(BOOL)respondsSystemStyleAutomatically {
     _respondsSystemStyleAutomatically = respondsSystemStyleAutomatically;
-    if (@available(iOS 13.0, *)) {
-        if (_respondsSystemStyleAutomatically && self.identifierForTrait) {
-             self.currentThemeIdentifier = self.identifierForTrait([UITraitCollection currentTraitCollection]);
-        }
+    if (_respondsSystemStyleAutomatically && self.identifierForTrait) {
+         self.currentThemeIdentifier = self.identifierForTrait([UITraitCollection currentTraitCollection]);
     }
 }
 

+ 35 - 97
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/QMUIThemePrivate.m

@@ -38,7 +38,6 @@
 #import "QMUIImagePreviewView.h"
 #import "QMUILabel.h"
 #import "QMUIPopupContainerView.h"
-#import "QMUIPopupMenuButtonItem.h"
 #import "QMUIPopupMenuView.h"
 #import "QMUITextField.h"
 #import "QMUITextView.h"
@@ -91,21 +90,11 @@
                                            result.copy;
                                        }),
                                        NSStringFromClass(UIToolbar.class):                  @[NSStringFromSelector(@selector(barTintColor)),],
-                                       NSStringFromClass(UITabBar.class):                   ({
-                                           NSMutableArray<NSString *> *result = @[
-                                               NSStringFromSelector(@selector(qmui_effect)),
-                                               NSStringFromSelector(@selector(qmui_effectForegroundColor)),
-                                           ].mutableCopy;
-                                           if (@available(iOS 13.0, *)) {
-                                               // iOS 13 在 UITabBar (QMUI) 里对所有旧版接口都映射到 standardAppearance,所以重新设置一次 standardAppearance 就可以更新所有样式
-                                               [result addObject:NSStringFromSelector(@selector(standardAppearance))];
-                                           } else {
-                                               [result addObjectsFromArray:@[NSStringFromSelector(@selector(barTintColor)),
-                                                                             NSStringFromSelector(@selector(unselectedItemTintColor)),
-                                                                             NSStringFromSelector(@selector(selectedImageTintColor)),]];
-                                           }
-                                           result.copy;
-                                       }),
+                                       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)),],
@@ -140,10 +129,10 @@
                                                                                                       NSStringFromSelector(@selector(maskViewBackgroundColor)),
                                                                                                       NSStringFromSelector(@selector(borderColor)),
                                                                                                       NSStringFromSelector(@selector(arrowImage)),],
-                                       NSStringFromClass(QMUIPopupMenuButtonItem.class):            @[NSStringFromSelector(@selector(highlightedBackgroundColor)),],
                                        NSStringFromClass(QMUIPopupMenuView.class):                  @[NSStringFromSelector(@selector(itemSeparatorColor)),
                                                                                                       NSStringFromSelector(@selector(sectionSeparatorColor)),
-                                                                                                      NSStringFromSelector(@selector(itemTitleColor))],
+                                                                                                      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)),],
@@ -194,18 +183,6 @@
                 originSelectorIMP(selfObject, originCMD, tintColor);
             };
         });
-        
-        // iOS 12 及以下的版本,[UIView setBackgroundColor:] 并不会保存传进来的 color,所以要自己用个变量保存起来,不然 QMUIThemeColor 对象就会被丢弃
-        if (@available(iOS 13.0, *)) {
-        } else {
-            ExtendImplementationOfVoidMethodWithSingleArgument([UIView class], @selector(setBackgroundColor:), UIColor *, ^(UIView *selfObject, UIColor *color) {
-                [selfObject qmui_bindObject:color forKey:@"UIView(QMUIThemeCompatibility).backgroundColor"];
-            });
-            ExtendImplementationOfNonVoidMethodWithoutArguments([UIView class], @selector(backgroundColor), UIColor *, ^UIColor *(UIView *selfObject, UIColor *originReturnValue) {
-                UIColor *color = [selfObject qmui_getBoundObjectForKey:@"UIView(QMUIThemeCompatibility).backgroundColor"];
-                return color ?: originReturnValue;
-            });
-        }
     });
 }
 
@@ -217,32 +194,29 @@
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
         // 这里反而是 iOS 13 才需要用 copy 的方式强制触发更新,否则如果某个 UISwitch 处于 off 的状态,此时去更新它的 onTintColor 不会立即生效,而是要等切换到 on 时,才会看到旧的 onTintColor 一闪而过变成新的 onTintColor,所以这里加个强制刷新
-        if (@available(iOS 13.0, *)) {
-            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);
-                };
-            });
-        }
-
+        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);
+            };
+        });
     });
 }
 
@@ -390,40 +364,6 @@
 
 @end
 
-@implementation UILabel (QMUIThemeCompatibility)
-
-+ (void)load {
-    static dispatch_once_t onceToken;
-    dispatch_once(&onceToken, ^{
-        // iOS 10-11 里,UILabel.attributedText 如果整个字符串都是同个颜色,则调用 -[UILabel setNeedsDisplay] 无法刷新文字样式,但如果字符串中存在不同 range 有不同颜色,就可以刷新。iOS 9、12-13 都没这个问题,所以这里做了兼容,给 UIView (QMUITheme) 那边刷新 UILabel 用。
-        if (@available(iOS 12.0, *)) {
-        } else {
-            OverrideImplementation([UILabel class], NSSelectorFromString(@"_needsContentsFormatUpdate"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
-                return ^BOOL(UILabel *selfObject) {
-                    
-                    __block BOOL attributedTextContainsDynamicColor = NO;
-                    if (selfObject.attributedText) {
-                        [selfObject.attributedText enumerateAttribute:NSForegroundColorAttributeName inRange:NSMakeRange(0, selfObject.attributedText.length) options:0 usingBlock:^(UIColor *color, NSRange range, BOOL * _Nonnull stop) {
-                            if (color.qmui_isQMUIDynamicColor) {
-                                attributedTextContainsDynamicColor = YES;
-                                *stop = YES;
-                            }
-                        }];
-                    }
-                    BOOL textColorIsDynamicColor = selfObject.textColor.qmui_isQMUIDynamicColor;
-                    if (attributedTextContainsDynamicColor || textColorIsDynamicColor) return YES;
-                    
-                    BOOL (*originSelectorIMP)(id, SEL);
-                    originSelectorIMP = (BOOL (*)(id, SEL))originalIMPProvider();
-                    return originSelectorIMP(selfObject, originCMD);
-                };
-            });
-        }
-    });
-}
-
-@end
-
 @interface CALayer ()
 
 @property(nonatomic, strong) UIColor *qcl_originalBackgroundColor;
@@ -485,11 +425,9 @@ QMUISynthesizeIdStrongProperty(qcl_originalShadowColor, setQcl_originalShadowCol
         
         // iOS 13 下,如果系统的主题发生变化,会自动调用每个 view 的 layoutSubviews,所以我们在这里面自动更新样式
         // 如果是 QMUIThemeManager 引发的主题变化,会在 theme 那边主动调用 qmui_setNeedsUpdateDynamicStyle,就不依赖这里
-        if (@available(iOS 13.0, *)) {
-            ExtendImplementationOfVoidMethodWithoutArguments([UIView class], @selector(layoutSubviews), ^(UIView *selfObject) {
-                [selfObject.layer qmui_setNeedsUpdateDynamicStyle];
-            });
-        }
+        ExtendImplementationOfVoidMethodWithoutArguments([UIView class], @selector(layoutSubviews), ^(UIView *selfObject) {
+            [selfObject.layer qmui_setNeedsUpdateDynamicStyle];
+        });
     });
 }
 
@@ -543,8 +481,8 @@ QMUISynthesizeIdStrongProperty(qcl_originalShadowColor, setQcl_originalShadowCol
                     // if (UITextFieldBorderView._image == image) return
                     // 由于 QMUIDynamicImage 随时可能发生图片的改变,这里要绕过这个判断:必须先清空一下 image,并马上调用 layoutIfNeeded 触发 -[UITextFieldBorderView setImage:] 使得 UITextFieldBorderView 内部的 image 清空,这样再设置新的才会生效。
                     originSelectorIMP(selfObject, originCMD, UIImage.new, state);
-                    [selfObject.qmui_textField setNeedsLayout];
-                    [selfObject.qmui_textField layoutIfNeeded];
+                    [selfObject.searchTextField setNeedsLayout];
+                    [selfObject.searchTextField layoutIfNeeded];
                 }
                 originSelectorIMP(selfObject, originCMD, image, state);
                 

+ 26 - 26
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIColor+QMUITheme.m

@@ -28,32 +28,30 @@
         // 随着 iOS 版本的迭代,需要不断检查 UIDynamicColor 对比 UIColor 多出来的方法是哪些,然后在 QMUIThemeColor 里补齐,否则可能出现”unrecognized selector sent to instance“的 crash
         // https://github.com/Tencent/QMUI_iOS/issues/791
 #ifdef DEBUG
-        if (@available(iOS 13.0, *)) {
-            Class dynamicColorClass = NSClassFromString(@"UIDynamicColor");
-            NSMutableSet<NSString *> *unrecognizedSelectors = NSMutableSet.new;
-            NSDictionary<NSString *, NSMutableSet<NSString *> *> *methods = @{
-                NSStringFromClass(UIColor.class): NSMutableSet.new,
-                NSStringFromClass(dynamicColorClass): NSMutableSet.new,
-                NSStringFromClass(self): NSMutableSet.new
-            };
-            [methods enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull classString, NSMutableSet<NSString *> * _Nonnull methods, BOOL * _Nonnull stop) {
-                [NSObject qmui_enumrateInstanceMethodsOfClass:NSClassFromString(classString) includingInherited:NO usingBlock:^(Method  _Nonnull method, SEL  _Nonnull selector) {
-                    [methods addObject:NSStringFromSelector(selector)];
-                }];
+        Class dynamicColorClass = NSClassFromString(@"UIDynamicColor");
+        NSMutableSet<NSString *> *unrecognizedSelectors = NSMutableSet.new;
+        NSDictionary<NSString *, NSMutableSet<NSString *> *> *methods = @{
+            NSStringFromClass(UIColor.class): NSMutableSet.new,
+            NSStringFromClass(dynamicColorClass): NSMutableSet.new,
+            NSStringFromClass(self): NSMutableSet.new
+        };
+        [methods enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull classString, NSMutableSet<NSString *> * _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);
+        }];
+        [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
     });
@@ -102,10 +100,12 @@
     // CGColor 必须通过 CGColorCreate 创建。UIColor.CGColor 返回的是一个多对象复用的 CGColor 值(例如,如果 QMUIThemeA.light 值和 UIColorB 的值刚好相同,那么他们的 CGColor 可能也是同一个对象,所以 UIColorB.CGColor 可能会错误地使用了原本仅属于 QMUIThemeColorA 的 bindObject)
     // 经测试,qmui_red 系列接口适用于不同的 ColorSpace,应该是能放心使用的😜
     // https://github.com/Tencent/QMUI_iOS/issues/1463
-    CGColorRef cgColor = CGColorCreate(CGColorSpaceCreateDeviceRGB(), (CGFloat[]){rawColor.qmui_red, rawColor.qmui_green, rawColor.qmui_blue, rawColor.qmui_alpha});
+    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 cgColor;
+    return (CGColorRef)CFAutorelease(cgColor);
 }
 
 - (NSString *)colorSpaceName {

+ 1 - 4
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.h

@@ -22,9 +22,6 @@ NS_ASSUME_NONNULL_BEGIN
 
 @required
 
-/// 获取当前 image 的标记名称,仅对 QMUIThemeImage 有效,其他 class 返回 nil。
-@property(nonatomic, copy, readonly) NSString *qmui_name;
-
 /// 获取当前 UIImage 的实际图片(返回的图片必定不是 dynamic image)
 @property(nonatomic, strong, readonly) UIImage *qmui_rawImage;
 
@@ -74,7 +71,7 @@ NS_ASSUME_NONNULL_BEGIN
 /**
  内部用,标志 QMUIThemeImage 对 UIImage (QMUI) 里使用动态颜色生成动态图片的适配 hook 是否已生效。例如在配置表这种“加载时机特别早”的场景,此时 UIImage (QMUITheme) +load 方法尚未被调用,这些 hook 还没生效,此时如果你使用 [UIImage qmui_imageWithTintColor:dynamicColor] 得到的 image 是无法自动响应 theme 切换的。
  */
-@property(class, nonatomic, assign) BOOL qmui_generatorSupportsDynamicColor;
+@property(class, nonatomic, assign, readonly) BOOL qmui_generatorSupportsDynamicColor;
 
 @end
 

+ 40 - 45
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIImage+QMUITheme.m

@@ -21,7 +21,7 @@
 #import "UIImage+QMUI.h"
 #import <objc/message.h>
 
-@interface UIImage (QMUITheme)
+@interface UIImage ()
 
 @property(nonatomic, assign) BOOL qmui_shouldUseSystemIMP;
 + (nullable UIImage *)qmui_dynamicImageWithOriginalImage:(UIImage *)image tintColor:(UIColor *)tintColor originalActionBlock:(UIImage * (^)(UIImage *aImage, UIColor *aTintColor))originalActionBlock;
@@ -163,6 +163,10 @@ static IMP qmui_getMsgForwardIMP(NSObject *self, SEL selector) {
     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;
@@ -339,10 +343,6 @@ static IMP qmui_getMsgForwardIMP(NSObject *self, SEL selector) {
 
 #pragma mark - <QMUIDynamicImageProtocol>
 
-- (NSString *)qmui_name {
-    return self.name;
-}
-
 - (UIImage *)qmui_rawImage {
     if (!_themeProvider) return nil;
     QMUIThemeManager *manager = [QMUIThemeManagerCenter themeManagerWithName:self.managerName];
@@ -462,42 +462,41 @@ static BOOL generatorSupportsDynamicColor = NO;
                 return result;
             };
         });
-        if (@available(iOS 13.0, *)) {
-            // 如果一个静态的 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;
-                };
-            });
-        }
+        
+        // 如果一个静态的 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;
     });
@@ -549,10 +548,6 @@ static BOOL generatorSupportsDynamicColor = NO;
 
 #pragma mark - <QMUIDynamicImageProtocol>
 
-- (NSString *)qmui_name {
-    return nil;
-}
-
 - (UIImage *)qmui_rawImage {
     return self;
 }

+ 12 - 25
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m

@@ -140,19 +140,6 @@ QMUISynthesizeIdCopyProperty(qmui_themeDidChangeBlock, setQmui_themeDidChangeBlo
         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) {
-            
-            // 修复 iOS 12 及以下版本,QMUIThemeImage 在搭配 resizable 使用的情况下可能无法跟随主题刷新的 bug
-            // https://github.com/Tencent/QMUI_iOS/issues/971
-            if (@available(iOS 13.0, *)) {
-            } else {
-                if (isValidatedImage) {
-                    QMUIThemeImage *image = (QMUIThemeImage *)value;
-                    if (image.qmui_resizable) {
-                        value = image.copy;
-                    }
-                }
-            }
-            
             [self performSelector:setter withObject:value];
         }
         EndIgnorePerformSelectorLeaksWarning
@@ -165,22 +152,22 @@ QMUISynthesizeIdCopyProperty(qmui_themeDidChangeBlock, setQmui_themeDidChangeBlo
     }
     
     if ([self isKindOfClass:UITextView.class]) {
-        UITextView *textView = (UITextView *)self;
+#ifdef IOS16_SDK_ALLOWED
         if (@available(iOS 16.0, *)) {
-            // iOS 16 里无法通过 setNeedsDisplay 去刷新文本颜色了,所以只能重新把 textColor 设置一遍
-            // 测过 textColor 和 typingAttributes[NSForegroundColorAttributeName] 是互通的,所以只操作任意一个即可
-            if (textView.textColor.qmui_isQMUIDynamicColor) {
-                textView.textColor = textView.textColor;
+            // iOS 16 里使用 TextKit 2 的输入框无法通过 setNeedsDisplay 去刷新文本颜色了,所以改为用这种方式去刷新
+            // 以下语句对 iOS 16 里因为访问 UITextView.layoutManager 而回退到 TextKit 1 的输入框无效,但由于 TextKit 1 本来就可以正常刷新,所以没问题。
+            // 注意要考虑输入框内可能存在多种颜色的富文本场景
+            UITextView *textView = (UITextView *)self;
+            NSTextRange *textRange = textView.textLayoutManager.textContentManager.documentRange;
+            if (textRange) {
+                [textView.textLayoutManager invalidateLayoutForRange:textRange];
             }
         } else {
-            if (@available(iOS 12.0, *)) {
-                [self setNeedsDisplay];
-            } else {
-                // 系统 UITextView 在 iOS 12 及以上重写了 -[UIView setNeedsDisplay],在里面会去刷新文字样式,但 iOS 11 及以下没有重写,所以这里对此作了兼容。实现思路是参考高版本系统的实现。
-                UIView *textContainerView = [self qmui_valueForKey:@"_containerView"];
-                if (textContainerView) [textContainerView setNeedsDisplay];
-            }
+#endif
+            [self setNeedsDisplay];
+#ifdef IOS16_SDK_ALLOWED
         }
+#endif
     }
     
     // 输入框、搜索框的键盘跟随主题变化

+ 11 - 10
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m

@@ -18,7 +18,7 @@
 
 @interface NSObject (QMUIWindowSizeMonitor_Private)
 
-@property(nonatomic, readonly) NSMutableArray <QMUIWindowSizeObserverHandler> *qwsm_windowSizeChangeHandlers;
+@property(nonatomic, readonly) NSMutableDictionary<QMUIWindowSizeObserverHandler, QMUIWeakObjectContainer *> *qwsm_windowSizeChangeHandlers;
 
 @end
 
@@ -58,21 +58,21 @@
     };
     
     void * blockFuncPtr = ((__bridge struct Block_literal *)handler)->__FuncPtr;
-    for (QMUIWindowSizeObserverHandler handler in self.qwsm_windowSizeChangeHandlers) {
+    for (QMUIWindowSizeObserverHandler handler in self.qwsm_windowSizeChangeHandlers.allKeys) {
         // 由于利用 block 的 __FuncPtr 指针来判断同一个实现的 block 过滤掉,防止重复添加监听
         if (((__bridge struct Block_literal *)handler)->__FuncPtr == blockFuncPtr) {
             return;
         }
     }
     
-    [self.qwsm_windowSizeChangeHandlers addObject:handler];
+    self.qwsm_windowSizeChangeHandlers[(id)handler] = [[QMUIWeakObjectContainer alloc] initWithObject:window];
     [window qwsm_addSizeObserver:self];
 }
 
-- (NSMutableArray<QMUIWindowSizeObserverHandler> *)qwsm_windowSizeChangeHandlers {
-    NSMutableArray *_handlers = objc_getAssociatedObject(self, _cmd);
+- (NSMutableDictionary<QMUIWindowSizeObserverHandler, QMUIWeakObjectContainer *> *)qwsm_windowSizeChangeHandlers {
+    NSMutableDictionary *_handlers = objc_getAssociatedObject(self, _cmd);
     if (!_handlers) {
-        _handlers = [NSMutableArray array];
+        _handlers = [[NSMutableDictionary alloc] init];
         objc_setAssociatedObject(self, _cmd, _handlers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
     }
     return _handlers;
@@ -158,10 +158,11 @@ QMUISynthesizeCGSizeProperty(qwsm_previousSize, setQwsm_previousSize)
     // notify sizeObservers
     for (NSUInteger i = 0, count = self.qwsm_sizeObservers.count; i < count; i++) {
         NSObject *object = [self.qwsm_sizeObservers pointerAtIndex:i];
-        for (NSUInteger i = 0, count = object.qwsm_windowSizeChangeHandlers.count; i < count; i++) {
-            QMUIWindowSizeObserverHandler handler = object.qwsm_windowSizeChangeHandlers[i];
-            handler(newSize);
-        }
+        [object.qwsm_windowSizeChangeHandlers enumerateKeysAndObjectsUsingBlock:^(QMUIWindowSizeObserverHandler  _Nonnull key, QMUIWeakObjectContainer * _Nonnull obj, BOOL * _Nonnull stop) {
+            if (obj.object == self) {
+                key(newSize);
+            }
+        }];
     }
     // send ‘windowDidTransitionToSize:’ to responders
     for (NSUInteger i = 0, count = self.qwsm_canReceiveWindowDidTransitionToSizeResponders.count; i < count; i++) {

+ 1 - 1
Pods/QMUIKit/QMUIKit/QMUIComponents/QMUIZoomImageView.m

@@ -396,7 +396,7 @@ static NSUInteger const kTagForCenteredPlayButton = 1;
 
 - (BOOL)enabledZoomImageView {
     BOOL enabledZoom = YES;
-    BOOL isLivePhoto = isLivePhoto = !!self.livePhoto;
+    BOOL isLivePhoto = !!self.livePhoto;
     if ([self.delegate respondsToSelector:@selector(enabledZoomViewInZoomImageView:)]) {
         enabledZoom = [self.delegate enabledZoomViewInZoomImageView:self];
     } else if (!self.image && !isLivePhoto && !self.videoPlayerItem) {

+ 1 - 1
Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastView.h

@@ -111,7 +111,7 @@ typedef NS_ENUM(NSInteger, QMUIToastViewPosition) {
 /**
  * 会盖住整个superView,防止手指可以点击到ToastView下面的内容,默认透明。
  */
-@property(nonatomic, strong, readonly) UIView *maskView;
+@property(nonatomic, strong, readonly) UIView *dimmingView;
 
 /**s
  * 承载Toast内容的UIView,可以自定义并赋值给contentView。如果contentView需要跟随ToastView的tintColor变化而变化,可以重写自定义view的`tintColorDidChange`来实现。默认使用`QMUIToastContentView`实现。

+ 4 - 4
Pods/QMUIKit/QMUIKit/QMUIComponents/ToastView/QMUIToastView.m

@@ -74,9 +74,9 @@ static NSMutableArray <QMUIToastView *> *kToastViews = nil;
     self.backgroundColor = UIColorClear;
     self.layer.allowsGroupOpacity = NO;
     
-    _maskView = [[UIView alloc] init];
-    self.maskView.backgroundColor = UIColorClear;
-    [self addSubview:self.maskView];
+    _dimmingView = [[UIView alloc] init];
+    self.dimmingView.backgroundColor = UIColorClear;
+    [self addSubview:self.dimmingView];
     
     [self registerNotifications];
 }
@@ -144,7 +144,7 @@ static NSMutableArray <QMUIToastView *> *kToastViews = nil;
     [super layoutSubviews];
     
     self.frame = self.parentView.bounds;
-    self.maskView.frame = self.bounds;
+    self.dimmingView.frame = self.bounds;
     
     CGFloat contentWidth = CGRectGetWidth(self.parentView.bounds);
     CGFloat contentHeight = CGRectGetHeight(self.parentView.bounds);

+ 35 - 12
Pods/QMUIKit/QMUIKit/QMUICore/QMUICommonDefines.h

@@ -71,6 +71,16 @@
 #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
@@ -92,8 +102,8 @@
 #pragma mark - 忽略 iOS 13 KVC 访问私有属性限制
 
 /// 将 KVC 代码包裹在这个宏中,可忽略系统的  KVC 访问限制
-#define BeginIgnoreUIKVCAccessProhibited if (@available(iOS 13.0, *)) NSThread.currentThread.qmui_shouldIgnoreUIKVCAccessProhibited = YES;
-#define EndIgnoreUIKVCAccessProhibited if (@available(iOS 13.0, *)) NSThread.currentThread.qmui_shouldIgnoreUIKVCAccessProhibited = NO;
+#define BeginIgnoreUIKVCAccessProhibited NSThread.currentThread.qmui_shouldIgnoreUIKVCAccessProhibited = YES;
+#define EndIgnoreUIKVCAccessProhibited NSThread.currentThread.qmui_shouldIgnoreUIKVCAccessProhibited = NO;
 
 #pragma mark - 变量-设备相关
 
@@ -142,6 +152,8 @@
 #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
@@ -167,6 +179,9 @@
 /// 是否放大模式(iPhone 6及以上的设备支持放大模式,iPhone X 除外)
 #define IS_ZOOMEDMODE [QMUIHelper isZoomedMode]
 
+/// 当前设备是否拥有灵动岛
+#define IS_DYNAMICISLAND_DEVICE [QMUIHelper isDynamicIslandDevice]
+
 #pragma mark - 变量-布局相关
 
 /// 获取一个像素
@@ -179,10 +194,10 @@
 #define ScreenNativeScale ([[UIScreen mainScreen] nativeScale])
 
 /// toolBar相关frame
-#define ToolBarHeight (IS_IPAD ? (IS_NOTCHED_SCREEN ? 70 : (IOS_VERSION >= 12.0 ? 50 : 44)) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44) + SafeAreaInsetsConstantForDeviceWithNotch.bottom)
+#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 : (IOS_VERSION >= 12.0 ? 50 : 49)) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(49, 32) : 49) + SafeAreaInsetsConstantForDeviceWithNotch.bottom)
+#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)
@@ -191,14 +206,14 @@
 #define StatusBarHeightConstant [QMUIHelper statusBarHeightConstant]
 
 /// navigationBar 的静态高度
-#define NavigationBarHeight (IS_IPAD ? (IOS_VERSION >= 12.0 ? 50 : 44) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44))
+#define NavigationBarHeight (IS_IPAD ? 50 : (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44))
 
 /// 代表(导航栏+状态栏),这里用于获取其高度
 /// @warn 如果是用于 viewController,请使用 UIViewController(QMUI) qmui_navigationBarMaxYInViewCoordinator 代替
 #define NavigationContentTop (StatusBarHeight + NavigationBarHeight)
 
 /// 同上,这里用于获取它的静态常量值
-#define NavigationContentTopConstant (StatusBarHeightConstant + NavigationBarHeight)
+#define NavigationContentTopConstant (QMUIHelper.navigationBarMaxYConstant)
 
 /// 判断当前是否是处于分屏模式的 iPad 或 iOS 16.1 的台前调度模式
 #define IS_SPLIT_SCREEN_IPAD (IS_IPAD && APPLICATION_WIDTH != SCREEN_WIDTH)
@@ -297,16 +312,13 @@ AddAccessibilityHint(NSObject *obj, NSString *hint) {
 
 #pragma mark - 其他
 
-// 固定黑色的 StatusBarStyle,用于亮色背景,作为 -preferredStatusBarStyle 方法的 return 值使用。
-#define QMUIStatusBarStyleDarkContent [QMUIHelper statusBarStyleDarkContent]
-
 #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__);}}})
+#define QMUIAssert(_condition, _categoryName, ...) ({if (!(_condition)) {QMUILogWarn(_categoryName, __VA_ARGS__);if (!QMUICMIActivated || !ShouldPrintQMUIWarnLogToConsole) {NSAssert(NO, __VA_ARGS__);}}})
 
 #pragma mark - Selector
 
@@ -329,11 +341,12 @@ setterWithGetter(SEL getter) {
 
 /**
  *  某些地方可能会将 CGFLOAT_MIN 作为一个数值参与计算(但其实 CGFLOAT_MIN 更应该被视为一个标志位而不是数值),可能导致一些精度问题,所以提供这个方法快速将 CGFLOAT_MIN 转换为 0
+ *  某些情况可能计算出来是0.0000000x,也靠这个方法抹去尾数。
  *  issue: https://github.com/Tencent/QMUI_iOS/issues/203
  */
 CG_INLINE CGFloat
 removeFloatMin(CGFloat floatValue) {
-    return floatValue == CGFLOAT_MIN ? 0 : floatValue;
+    return fabs(floatValue) <= 0.001 ? 0 : floatValue;
 }
 
 /**
@@ -343,9 +356,19 @@ removeFloatMin(CGFloat floatValue) {
  */
 CG_INLINE CGFloat
 flatSpecificScale(CGFloat floatValue, CGFloat scale) {
+    if (isinf(floatValue) || floatValue == CGFLOAT_MAX) return floatValue;
     floatValue = removeFloatMin(floatValue);
     scale = scale ?: ScreenScale;
-    CGFloat flattedValue = ceil(floatValue * scale) / scale;
+    // 这里因为浮点精度的问题,可能会出现一些偏差,例如 161.66666666666669 算出来可能是162,161.66666666666666 算出来是161.66666666667,为了解决这种场景,这里同时用 ceil 和 round 算一遍再取最接近的那个结果
+    NSInteger pixelValue1 = ceil(floatValue * scale);
+    NSInteger pixelValue2 = round(floatValue * scale);
+    NSInteger pixelValue = 0;
+    if (fabs(pixelValue1 - floatValue) <= fabs(pixelValue2 - floatValue)) {
+        pixelValue = pixelValue1;
+    } else {
+        pixelValue = pixelValue2;
+    }
+    CGFloat flattedValue = pixelValue / scale;
     return flattedValue;
 }
 

+ 0 - 6
Pods/QMUIKit/QMUIKit/QMUICore/QMUIConfiguration.h

@@ -261,15 +261,11 @@ NS_ASSUME_NONNULL_BEGIN
 @property(nonatomic, assign) UIEdgeInsets       badgeContentEdgeInsets;
 @property(nonatomic, assign) CGPoint            badgeOffset;
 @property(nonatomic, assign) CGPoint            badgeOffsetLandscape;
-@property(nonatomic, assign) CGPoint            badgeCenterOffset DEPRECATED_MSG_ATTRIBUTE("请改为使用 badgeOffset");
-@property(nonatomic, assign) CGPoint            badgeCenterOffsetLandscape DEPRECATED_MSG_ATTRIBUTE("请改为使用 badgeOffsetLandscape");
 
 @property(nonatomic, strong, nullable) UIColor  *updatesIndicatorColor;
 @property(nonatomic, assign) CGSize             updatesIndicatorSize;
 @property(nonatomic, assign) CGPoint            updatesIndicatorOffset;
 @property(nonatomic, assign) CGPoint            updatesIndicatorOffsetLandscape;
-@property(nonatomic, assign) CGPoint            updatesIndicatorCenterOffset DEPRECATED_MSG_ATTRIBUTE("请改为使用 updatesIndicatorOffset");
-@property(nonatomic, assign) CGPoint            updatesIndicatorCenterOffsetLandscape DEPRECATED_MSG_ATTRIBUTE("请改为使用 updatesIndicatorOffsetLandscape");
 
 #pragma mark - Others
 
@@ -281,10 +277,8 @@ NS_ASSUME_NONNULL_BEGIN
 @property(nonatomic, assign) BOOL               hidesBottomBarWhenPushedInitially;
 @property(nonatomic, assign) BOOL               preventConcurrentNavigationControllerTransitions;
 @property(nonatomic, assign) BOOL               navigationBarHiddenInitially;
-@property(nonatomic, assign) BOOL               shouldFixTabBarTransitionBugInIPhoneX;
 @property(nonatomic, assign) BOOL               shouldFixTabBarSafeAreaInsetsBug;
 @property(nonatomic, assign) BOOL               shouldFixSearchBarMaskViewLayoutBug;
-@property(nonatomic, assign) BOOL               sendAnalyticsToQMUITeam;
 @property(nonatomic, assign) BOOL               dynamicPreferredValueForIPad;
 @property(nonatomic, assign) BOOL               ignoreKVCAccessProhibited API_AVAILABLE(ios(13.0));
 @property(nonatomic, assign) BOOL               adjustScrollIndicatorInsetsByContentInsetAdjustment API_AVAILABLE(ios(13.0));

+ 68 - 205
Pods/QMUIKit/QMUIKit/QMUICore/QMUIConfiguration.m

@@ -119,31 +119,11 @@ static BOOL QMUI_hasAppliedInitialTemplate;
         }
     }
     
-    if (IS_DEBUG && self.sendAnalyticsToQMUITeam) {
-        [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:[NSOperationQueue new] usingBlock:^(NSNotification * _Nonnull note) {
-            // 这里根据是否能成功获取到 classesref 来统计信息,以供后续确认对 classesref 为 nil 的保护是否真的必要
-            [self sendAnalyticsWithQuery:classes ? @"findByObjc=true" : nil];
-        }];
-    }
-    
     if (classes) free(classes);
     
     QMUI_hasAppliedInitialTemplate = YES;
 }
 
-- (void)sendAnalyticsWithQuery:(NSString *)query {
-    NSString *identifier = [NSBundle mainBundle].bundleIdentifier.qmui_stringByEncodingUserInputQuery;
-    NSString *displayName = ((NSString *)([NSBundle mainBundle].infoDictionary[@"CFBundleDisplayName"] ?: [NSBundle mainBundle].infoDictionary[@"CFBundleName"])).qmui_stringByEncodingUserInputQuery;
-    NSString *QMUIVersion = QMUI_VERSION.qmui_stringByEncodingUserInputQuery;// 如果不以 framework 方式引入 QMUI 的话,是无法通过 CFBundleShortVersionString 获取到 QMUI 所在的 bundle 的版本号的,所以这里改为用脚本生成的变量来获取
-    NSString *queryString = [NSString stringWithFormat:@"appId=%@&appName=%@&version=%@&platform=iOS", identifier, displayName, QMUIVersion];
-    if (query.length > 0) queryString = [NSString stringWithFormat:@"%@&%@", queryString, query];
-    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://qmuiteam.com/analytics/usageReport"]];
-    request.HTTPMethod = @"POST";
-    request.HTTPBody = [queryString dataUsingEncoding:NSUTF8StringEncoding];
-    NSURLSession *session = [NSURLSession sharedSession];
-    [[session dataTaskWithRequest:request] resume];
-}
-
 #pragma mark - Initialize default values
 
 - (void)initDefaultConfiguration {
@@ -193,12 +173,16 @@ static BOOL QMUI_hasAppliedInitialTemplate;
     self.navBarHighlightedAlpha = 0.2f;
     self.navBarDisabledAlpha = 0.2f;
     self.sizeNavBarBackIndicatorImageAutomatically = YES;
-    self.navBarCloseButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavClose size:CGSizeMake(16, 16) tintColor:self.navBarTintColor];
-    
     self.navBarLoadingMarginRight = 3;
     self.navBarAccessoryViewMarginLeft = 5;
     self.navBarActivityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
-    self.navBarAccessoryViewTypeDisclosureIndicatorImage = [[UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:self.navBarTitleColor] qmui_imageWithOrientation:UIImageOrientationDown];
+    
+    // XCTest 会在 dispatch_once 里访问 UIScreen 引发死锁,所以屏蔽掉
+    // https://github.com/Tencent/QMUI_iOS/issues/1479
+    if (!IS_XCTEST) {
+        self.navBarCloseButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavClose size:CGSizeMake(16, 16) tintColor:self.navBarTintColor];
+        self.navBarAccessoryViewTypeDisclosureIndicatorImage = [[UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:self.navBarTitleColor] qmui_imageWithOrientation:UIImageOrientationDown];
+    }
     
     
     #pragma mark - Toolbar
@@ -277,13 +261,14 @@ static BOOL QMUI_hasAppliedInitialTemplate;
     self.shouldPrintDefaultLog = YES;
     self.shouldPrintInfoLog = YES;
     self.shouldPrintWarnLog = YES;
-    self.shouldPrintQMUIWarnLogToConsole = IS_DEBUG;
+    self.shouldPrintQMUIWarnLogToConsole = IS_DEBUG && !IS_XCTEST;
     
     #pragma mark - QMUIBadge
-    self.badgeOffset = QMUIBadgeInvalidateOffset;
-    self.badgeOffsetLandscape = QMUIBadgeInvalidateOffset;
-    self.updatesIndicatorOffset = QMUIBadgeInvalidateOffset;
-    self.updatesIndicatorOffsetLandscape = QMUIBadgeInvalidateOffset;
+    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
     
@@ -291,7 +276,6 @@ static BOOL QMUI_hasAppliedInitialTemplate;
     self.needsBackBarButtonItemTitle = YES;
     self.preventConcurrentNavigationControllerTransitions = YES;
     self.shouldFixTabBarSafeAreaInsetsBug = YES;
-    self.sendAnalyticsToQMUITeam = YES;
 }
 
 #pragma mark - Switch Setter
@@ -868,247 +852,126 @@ static BOOL QMUI_hasAppliedInitialTemplate;
 }
 
 - (void)updateTabBarAppearance {
-    if (@available(iOS 13.0, *)) {
-        if (QMUIHelper.canUpdateAppearance) {
-            UITabBar.qmui_appearanceConfigured.standardAppearance = self.tabBarAppearance;
+    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;
-                }
+        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;
+#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;
-                }
+        if (@available(iOS 15.0, *)) {
+            if (QMUICMIActivated && TabBarUsesStandardAppearanceOnly) {
+                tabBarController.tabBar.scrollEdgeAppearance = self.tabBarAppearance;
             }
+        }
 #endif
-            [tabBarController.tabBar setNeedsLayout];// theme 不跟随系统的情况下切换 Light/Dark,tabBarAppearance.backgroundEffect 虽然值被更新了,但样式被刷新,这里手动触发一下
-        }];
-    }
+        [tabBarController.tabBar setNeedsLayout];// theme 不跟随系统的情况下切换 Light/Dark,tabBarAppearance.backgroundEffect 虽然值被更新了,但样式被刷新,这里手动触发一下
+    }];
 }
 
 - (void)setTabBarBarTintColor:(UIColor *)tabBarBarTintColor {
     [QMUIConfiguration performAction:^{
         _tabBarBarTintColor = tabBarBarTintColor;
-        
-        if (@available(iOS 13.0, *)) {
-            self.tabBarAppearance.backgroundColor = tabBarBarTintColor;
-            [self updateTabBarAppearance];
-        } else {
-            if (QMUIHelper.canUpdateAppearance) {
-                UITabBar.qmui_appearanceConfigured.barTintColor = _tabBarBarTintColor;
-            }
-            [self.appearanceUpdatingTabBarControllers enumerateObjectsUsingBlock:^(UITabBarController * _Nonnull tabBarController, NSUInteger idx, BOOL * _Nonnull stop) {
-                tabBarController.tabBar.barTintColor = _tabBarBarTintColor;
-            }];
-        }
+        self.tabBarAppearance.backgroundColor = tabBarBarTintColor;
+        [self updateTabBarAppearance];
     } ifValueChanged:_tabBarBarTintColor newValue:tabBarBarTintColor];
 }
 
 - (void)setTabBarStyle:(UIBarStyle)tabBarStyle {
     [QMUIConfiguration performAction:^{
         _tabBarStyle = tabBarStyle;
-        if (@available(iOS 13.0, *)) {
-            self.tabBarAppearance.backgroundEffect = [UIBlurEffect effectWithStyle:tabBarStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark];
-            [self updateTabBarAppearance];
-        } else {
-            if (QMUIHelper.canUpdateAppearance) {
-                UITabBar.qmui_appearanceConfigured.barStyle = tabBarStyle;
-            }
-            [self.appearanceUpdatingTabBarControllers enumerateObjectsUsingBlock:^(UITabBarController * _Nonnull tabBarController, NSUInteger idx, BOOL * _Nonnull stop) {
-                tabBarController.tabBar.barStyle = tabBarStyle;
-            }];
-        }
+        self.tabBarAppearance.backgroundEffect = [UIBlurEffect effectWithStyle:tabBarStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark];
+        [self updateTabBarAppearance];
     } ifValueChanged:@(_tabBarStyle) newValue:@(tabBarStyle)];
 }
 
 - (void)setTabBarBackgroundImage:(UIImage *)tabBarBackgroundImage {
     [QMUIConfiguration performAction:^{
         _tabBarBackgroundImage = tabBarBackgroundImage;
-        
-        if (@available(iOS 13.0, *)) {
-            self.tabBarAppearance.backgroundImage = tabBarBackgroundImage;
-            [self updateTabBarAppearance];
-        } else {
-            if (QMUIHelper.canUpdateAppearance) {
-                UITabBar.qmui_appearanceConfigured.backgroundImage = tabBarBackgroundImage;
-            }
-            [self.appearanceUpdatingTabBarControllers enumerateObjectsUsingBlock:^(UITabBarController * _Nonnull tabBarController, NSUInteger idx, BOOL * _Nonnull stop) {
-                tabBarController.tabBar.backgroundImage = tabBarBackgroundImage;
-            }];
-        }
+        self.tabBarAppearance.backgroundImage = tabBarBackgroundImage;
+        [self updateTabBarAppearance];
     } ifValueChanged:_tabBarBackgroundImage newValue:tabBarBackgroundImage];
 }
 
 - (void)setTabBarShadowImageColor:(UIColor *)tabBarShadowImageColor {
     [QMUIConfiguration performAction:^{
         _tabBarShadowImageColor = tabBarShadowImageColor;
-        
-        if (@available(iOS 13.0, *)) {
-            self.tabBarAppearance.shadowColor = tabBarShadowImageColor;
-            [self updateTabBarAppearance];
-        } else {
-            UIImage *shadowImage = [UIImage qmui_imageWithColor:_tabBarShadowImageColor size:CGSizeMake(1, PixelOne) cornerRadius:0];
-            if (QMUIHelper.canUpdateAppearance) {
-                [UITabBar.qmui_appearanceConfigured setShadowImage:shadowImage];
-            }
-            [self.appearanceUpdatingTabBarControllers enumerateObjectsUsingBlock:^(UITabBarController * _Nonnull tabBarController, NSUInteger idx, BOOL * _Nonnull stop) {
-                tabBarController.tabBar.shadowImage = shadowImage;
-            }];
-        }
+        self.tabBarAppearance.shadowColor = tabBarShadowImageColor;
+        [self updateTabBarAppearance];
     } ifValueChanged:_tabBarShadowImageColor newValue:tabBarShadowImageColor];
 }
 
 - (void)setTabBarItemTitleFont:(UIFont *)tabBarItemTitleFont {
     [QMUIConfiguration performAction:^{
         _tabBarItemTitleFont = tabBarItemTitleFont;
-        
-        if (@available(iOS 13.0, *)) {
-            [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) {
-                NSMutableDictionary<NSAttributedStringKey, id> *attributes = itemAppearance.normal.titleTextAttributes.mutableCopy;
-                attributes[NSFontAttributeName] = tabBarItemTitleFont;
-                itemAppearance.normal.titleTextAttributes = attributes.copy;
-            }];
-            [self updateTabBarAppearance];
-        } else {
-            NSMutableDictionary<NSString *, id> *textAttributes = [[NSMutableDictionary alloc] initWithDictionary:[UITabBarItem.qmui_appearanceConfigured titleTextAttributesForState:UIControlStateNormal]];
-            if (_tabBarItemTitleFont) {
-                textAttributes[NSFontAttributeName] = _tabBarItemTitleFont;
-            }
-            if (QMUIHelper.canUpdateAppearance) {
-                [UITabBarItem.qmui_appearanceConfigured setTitleTextAttributes:textAttributes forState:UIControlStateNormal];
-            }
-            [self.appearanceUpdatingTabBarControllers enumerateObjectsUsingBlock:^(UITabBarController * _Nonnull tabBarController, NSUInteger idx, BOOL * _Nonnull stop) {
-                [tabBarController.tabBar.items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
-                    [obj setTitleTextAttributes:textAttributes forState:UIControlStateNormal];
-                }];
-            }];
-        }
+        [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) {
+            NSMutableDictionary<NSAttributedStringKey, id> *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;
-        
-        if (@available(iOS 13.0, *)) {
-            [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) {
-                NSMutableDictionary<NSAttributedStringKey, id> *attributes = itemAppearance.selected.titleTextAttributes.mutableCopy;
-                attributes[NSFontAttributeName] = tabBarItemTitleFontSelected;
-                itemAppearance.selected.titleTextAttributes = attributes.copy;
-            }];
-            [self updateTabBarAppearance];
-        } else {
-            NSMutableDictionary<NSString *, id> *textAttributes = [[NSMutableDictionary alloc] initWithDictionary:[UITabBarItem.qmui_appearanceConfigured titleTextAttributesForState:UIControlStateSelected]];
-            if (tabBarItemTitleFontSelected) {
-                textAttributes[NSFontAttributeName] = tabBarItemTitleFontSelected;
-            }
-            if (QMUIHelper.canUpdateAppearance) {
-                [UITabBarItem.qmui_appearanceConfigured setTitleTextAttributes:textAttributes forState:UIControlStateSelected];
-            }
-            [self.appearanceUpdatingTabBarControllers enumerateObjectsUsingBlock:^(UITabBarController * _Nonnull tabBarController, NSUInteger idx, BOOL * _Nonnull stop) {
-                [tabBarController.tabBar.items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
-                    [obj setTitleTextAttributes:textAttributes forState:UIControlStateSelected];
-                }];
-            }];
-        }
+        [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) {
+            NSMutableDictionary<NSAttributedStringKey, id> *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;
-        
-        if (@available(iOS 13.0, *)) {
-            [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) {
-                NSMutableDictionary<NSAttributedStringKey, id> *attributes = itemAppearance.normal.titleTextAttributes.mutableCopy;
-                attributes[NSForegroundColorAttributeName] = tabBarItemTitleColor;
-                itemAppearance.normal.titleTextAttributes = attributes.copy;
-            }];
-            [self updateTabBarAppearance];
-        } else {
-            NSMutableDictionary<NSString *, id> *textAttributes = [[NSMutableDictionary alloc] initWithDictionary:[UITabBarItem.qmui_appearanceConfigured titleTextAttributesForState:UIControlStateNormal]];
-            textAttributes[NSForegroundColorAttributeName] = _tabBarItemTitleColor;
-            if (QMUIHelper.canUpdateAppearance) {
-                [UITabBarItem.qmui_appearanceConfigured setTitleTextAttributes:textAttributes forState:UIControlStateNormal];
-            }
-            [self.appearanceUpdatingTabBarControllers enumerateObjectsUsingBlock:^(UITabBarController * _Nonnull tabBarController, NSUInteger idx, BOOL * _Nonnull stop) {
-                [tabBarController.tabBar.items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
-                    [obj setTitleTextAttributes:textAttributes forState:UIControlStateNormal];
-                }];
-            }];
-        }
+        [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) {
+            NSMutableDictionary<NSAttributedStringKey, id> *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;
-        
-        if (@available(iOS 13.0, *)) {
-            [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) {
-                NSMutableDictionary<NSAttributedStringKey, id> *attributes = itemAppearance.selected.titleTextAttributes.mutableCopy;
-                attributes[NSForegroundColorAttributeName] = tabBarItemTitleColorSelected;
-                itemAppearance.selected.titleTextAttributes = attributes.copy;
-            }];
-            [self updateTabBarAppearance];
-        } else {
-            NSMutableDictionary<NSString *, id> *textAttributes = [[NSMutableDictionary alloc] initWithDictionary:[UITabBarItem.qmui_appearanceConfigured titleTextAttributesForState:UIControlStateSelected]];
-            textAttributes[NSForegroundColorAttributeName] = _tabBarItemTitleColorSelected;
-            if (QMUIHelper.canUpdateAppearance) {
-                [UITabBarItem.qmui_appearanceConfigured setTitleTextAttributes:textAttributes forState:UIControlStateSelected];
-            }
-            [self.appearanceUpdatingTabBarControllers enumerateObjectsUsingBlock:^(UITabBarController * _Nonnull tabBarController, NSUInteger idx, BOOL * _Nonnull stop) {
-                [tabBarController.tabBar.items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
-                    [obj setTitleTextAttributes:textAttributes forState:UIControlStateSelected];
-                }];
-            }];
-        }
+        [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) {
+            NSMutableDictionary<NSAttributedStringKey, id> *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;
-        
-        if (@available(iOS 13.0, *)) {
-            [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) {
-                itemAppearance.normal.iconColor = tabBarItemImageColor;
-            }];
-            [self updateTabBarAppearance];
-        } else {
-            if (QMUIHelper.canUpdateAppearance) {
-                UITabBar.qmui_appearanceConfigured.unselectedItemTintColor = tabBarItemImageColor;
-            }
-            [self.appearanceUpdatingTabBarControllers enumerateObjectsUsingBlock:^(UITabBarController * _Nonnull tabBarController, NSUInteger idx, BOOL * _Nonnull stop) {
-                tabBarController.tabBar.unselectedItemTintColor = 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;
-        
-        if (@available(iOS 13.0, *)) {
-            [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) {
-                itemAppearance.selected.iconColor = tabBarItemImageColorSelected;
-            }];
-            [self updateTabBarAppearance];
-        } else {
-            // iOS 12 及以下使用 tintColor 实现,但 tintColor 并没有声明 UI_APPEARANCE_SELECTOR,所以暂不使用 appearance 的方式去修改(虽然 appearance 方式实测是生效的)
-            //        UITabBar.qmui_appearanceConfigured.tintColor = tabBarItemImageColorSelected;
-            [self.appearanceUpdatingTabBarControllers enumerateObjectsUsingBlock:^(UITabBarController * _Nonnull tabBarController, NSUInteger idx, BOOL * _Nonnull stop) {
-                tabBarController.tabBar.tintColor = tabBarItemImageColorSelected;
-            }];
-        }
+        [self.tabBarAppearance qmui_applyItemAppearanceWithBlock:^(UITabBarItemAppearance * _Nonnull itemAppearance) {
+            itemAppearance.selected.iconColor = tabBarItemImageColorSelected;
+        }];
+        [self updateTabBarAppearance];
     } ifValueChanged:_tabBarItemImageColorSelected newValue:tabBarItemImageColorSelected];
 }
 

+ 1 - 7
Pods/QMUIKit/QMUIKit/QMUICore/QMUIConfigurationMacros.h

@@ -250,30 +250,24 @@
 #define BadgeContentEdgeInsets                          [QMUICMI badgeContentEdgeInsets]
 #define BadgeOffset                                     [QMUICMI badgeOffset]
 #define BadgeOffsetLandscape                            [QMUICMI badgeOffsetLandscape]
-#define BadgeCenterOffset                               [QMUICMI badgeCenterOffset]
-#define BadgeCenterOffsetLandscape                      [QMUICMI badgeCenterOffsetLandscape]
 
 #define UpdatesIndicatorColor                           [QMUICMI updatesIndicatorColor]
 #define UpdatesIndicatorSize                            [QMUICMI updatesIndicatorSize]
 #define UpdatesIndicatorOffset                          [QMUICMI updatesIndicatorOffset]
 #define UpdatesIndicatorOffsetLandscape                 [QMUICMI updatesIndicatorOffsetLandscape]
-#define UpdatesIndicatorCenterOffset                    [QMUICMI updatesIndicatorCenterOffset]
-#define UpdatesIndicatorCenterOffsetLandscape           [QMUICMI updatesIndicatorCenterOffsetLandscape]
 
 #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,固定黑色则设置为 QMUIStatusBarStyleDarkContent。
+#define DefaultStatusBarStyle                           [QMUICMI defaultStatusBarStyle]      // 默认的状态栏样式,默认值为 UIStatusBarStyleDefault,也即在 iOS 12 及以前是黑色文字,iOS 13 及以后会自动根据当前 App 是否处于 Dark Mode 切换颜色。如果你希望固定为白色,请设置为 UIStatusBarStyleLightContent,固定黑色则设置为 UIStatusBarStyleDarkContent。
 #define NeedsBackBarButtonItemTitle                     [QMUICMI needsBackBarButtonItemTitle]       // 全局是否需要返回按钮的title,不需要则只显示一个返回image
 #define HidesBottomBarWhenPushedInitially               [QMUICMI hidesBottomBarWhenPushedInitially] // QMUICommonViewController.hidesBottomBarWhenPushed 的初始值,默认为 NO,以保持与系统默认值一致,但通常建议改为 YES,因为一般只有 tabBar 首页那几个界面要求为 NO
 #define PreventConcurrentNavigationControllerTransitions [QMUICMI preventConcurrentNavigationControllerTransitions] // PreventConcurrentNavigationControllerTransitions : 自动保护 QMUINavigationController 在上一次 push/pop 尚未结束的时候就进行下一次 push/pop 的行为,避免产生 crash
 #define NavigationBarHiddenInitially                    [QMUICMI navigationBarHiddenInitially]      // preferredNavigationBarHidden 的初始值,默认为NO
-#define ShouldFixTabBarTransitionBugInIPhoneX           [QMUICMI shouldFixTabBarTransitionBugInIPhoneX] // 是否需要自动修复 iOS 11 下,iPhone X 的设备在 push 界面时,tabBar 会瞬间往上跳的 bug
 #define ShouldFixTabBarSafeAreaInsetsBug [QMUICMI shouldFixTabBarSafeAreaInsetsBug] // 是否要对 iOS 11 及以后的版本修复当存在 UITabBar 时,UIScrollView 的 inset.bottom 可能错误的 bug(issue #218 #934),默认为 YES
 #define ShouldFixSearchBarMaskViewLayoutBug             [QMUICMI shouldFixSearchBarMaskViewLayoutBug] // 是否自动修复 UISearchController.searchBar 被当作 tableHeaderView 使用时可能出现的布局 bug(issue #950)
-#define SendAnalyticsToQMUITeam                         [QMUICMI sendAnalyticsToQMUITeam] // 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用
 #define DynamicPreferredValueForIPad                    [QMUICMI dynamicPreferredValueForIPad] // 当 iPad 处于 Slide Over 或 Split View 分屏模式下,宏 `PreferredValueForXXX` 是否把 iPad 视为某种屏幕宽度近似的 iPhone 来取值。
 #define IgnoreKVCAccessProhibited                       [QMUICMI ignoreKVCAccessProhibited] // 是否全局忽略 iOS 13 对 KVC 访问 UIKit 私有属性的限制
 #define AdjustScrollIndicatorInsetsByContentInsetAdjustment [QMUICMI adjustScrollIndicatorInsetsByContentInsetAdjustment] // 当将 UIScrollView.contentInsetAdjustmentBehavior 设为 UIScrollViewContentInsetAdjustmentNever 时,是否自动将 UIScrollView.automaticallyAdjustsScrollIndicatorInsets 设为 NO,以保证原本在 iOS 12 下的代码不用修改就能在 iOS 13 下正常控制滚动条的位置。

+ 34 - 11
Pods/QMUIKit/QMUIKit/QMUICore/QMUIHelper.h

@@ -19,9 +19,6 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
-// TODO: molice 等废弃 qmui_badgeCenterOffset 系列接口后再删除
-extern const CGPoint QMUIBadgeInvalidateOffset;
-
 @interface QMUIHelper : NSObject
 
 + (instancetype)sharedInstance;
@@ -166,6 +163,9 @@ extern const CGPoint QMUIBadgeInvalidateOffset;
 /// @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;
 
@@ -175,9 +175,15 @@ extern const CGPoint QMUIBadgeInvalidateOffset;
 /// 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;
 
@@ -199,9 +205,12 @@ extern const CGPoint QMUIBadgeInvalidateOffset;
 /// 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;
@@ -224,6 +233,10 @@ extern const CGPoint QMUIBadgeInvalidateOffset;
 /// @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 应用运行的窗口大小
@@ -231,10 +244,16 @@ extern const CGPoint QMUIBadgeInvalidateOffset;
 @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)
@@ -249,13 +268,6 @@ extern const CGPoint QMUIBadgeInvalidateOffset;
  */
 + (void)resetDimmedApplicationWindow;
 
-/**
- * 黑色的 StatusBarStyle,用于亮色背景
- * @note 在 iOS 13 以前  UIStatusBarStyleDefault 状态栏内容的颜色固定是黑色的,而在 iOS 13 UIStatusBarStyleDefault 会根据 user interface style 来决定状态栏的颜色,如果你需要一直黑色可以用 QMUIStatusBarStyleDarkContent 来代替以前 UIStatusBarStyleDefault 的写法
- * @return 在 iOS 13 以上返回 UIStatusBarStyleDarkContent,在 iOS 12 及以下返回 UIStatusBarStyleDefault
-*/
-@property(class, nonatomic, readonly) UIStatusBarStyle statusBarStyleDarkContent;
-
 /**
  在非 UIApplicationStateActive 的时机去设置 UIAppearance 可能引发第三方输入法 crash,因此提供这个方法判断当前是否可以更新 UIAppearance。
  详情请见 https://github.com/Tencent/QMUI_iOS/issues/1281
@@ -277,4 +289,15 @@ extern const CGPoint QMUIBadgeInvalidateOffset;
 
 @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

+ 243 - 60
Pods/QMUIKit/QMUIKit/QMUICore/QMUIHelper.m

@@ -20,11 +20,11 @@
 #import "NSString+QMUI.h"
 #import "UIInterface+QMUI.h"
 #import "NSObject+QMUI.h"
+#import "NSArray+QMUI.h"
 #import <AVFoundation/AVFoundation.h>
 #import <math.h>
 #import <sys/utsname.h>
 
-const CGPoint QMUIBadgeInvalidateOffset = {-1000, -1000};
 NSString *const kQMUIResourcesBundleName = @"QMUIResources";
 
 @interface _QMUIPortraitViewController : UIViewController
@@ -145,11 +145,9 @@ QMUISynthesizeCGFloatProperty(lastKeyboardHeight, setLastKeyboardHeight)
 
 + (CGFloat)keyboardHeightWithNotification:(nullable NSNotification *)notification inView:(nullable UIView *)view {
     CGRect keyboardRect = [self keyboardRectWithNotification:notification];
-    if (@available(iOS 13.0, *)) {
-        // iOS 13 分屏键盘 x 不是 0,不知道是系统 BUG 还是故意这样,先这样保护,再观察一下后面的 beta 版本
-        if (IS_SPLIT_SCREEN_IPAD && keyboardRect.origin.x > 0) {
-            keyboardRect.origin.x = 0;
-        }
+    // 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];
@@ -245,10 +243,15 @@ static CGFloat pixelOne = -1.0f;
         return [NSString stringWithFormat:@"%s", getenv("SIMULATOR_MODEL_IDENTIFIER")];
     }
     
-    // See https://www.theiphonewiki.com/wiki/Models for identifiers
-    struct utsname systemInfo;
-    uname(&systemInfo);
-    return [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
+    // See https://gist.github.com/adamawolf/3048717 for identifiers
+    static dispatch_once_t onceToken;
+    static NSString *model;
+    dispatch_once(&onceToken, ^{
+        struct utsname systemInfo;
+        uname(&systemInfo);
+        model = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
+    });
+    return model;
 }
 
 + (NSString *)deviceName {
@@ -262,7 +265,7 @@ static CGFloat pixelOne = -1.0f;
         }
         
         NSDictionary *dict = @{
-            // See https://www.theiphonewiki.com/wiki/Models
+            // See https://gist.github.com/adamawolf/3048717
             @"iPhone1,1" : @"iPhone 1G",
             @"iPhone1,2" : @"iPhone 3G",
             @"iPhone2,1" : @"iPhone 3GS",
@@ -311,6 +314,14 @@ static CGFloat pixelOne = -1.0f;
             @"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)",
@@ -383,6 +394,18 @@ static CGFloat pixelOne = -1.0f;
             @"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",
@@ -421,6 +444,24 @@ static CGFloat pixelOne = -1.0f;
             @"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",
@@ -447,7 +488,7 @@ static NSInteger isIPad = -1;
 + (BOOL)isIPad {
     if (isIPad < 0) {
         // [[[UIDevice currentDevice] model] isEqualToString:@"iPad"] 无法判断模拟器 iPad,所以改为以下方式
-        isIPad = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? 1 : 0;
+        isIPad = UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad ? 1 : 0;
     }
     return isIPad > 0;
 }
@@ -487,52 +528,62 @@ static NSInteger isSimulator = -1;
     if (@available(iOS 14.0, *)) {
         return [NSProcessInfo processInfo].isiOSAppOnMac || [NSProcessInfo processInfo].isMacCatalystApp;
     }
-    if (@available(iOS 13.0, *)) {
-        return [NSProcessInfo processInfo].isMacCatalystApp;
-    }
-    return NO;
+    return [NSProcessInfo processInfo].isMacCatalystApp;
 }
 
 static NSInteger isNotchedScreen = -1;
 + (BOOL)isNotchedScreen {
     if (isNotchedScreen < 0) {
-        if (@available(iOS 12.0, *)) {
-            /*
-             检测方式解释/测试要点:
-             1. iOS 11 与 iOS 12 可能行为不同,所以要分别测试。
-             2. 与触发 [QMUIHelper isNotchedScreen] 方法时的进程有关,例如 https://github.com/Tencent/QMUI_iOS/issues/482#issuecomment-456051738 里提到的 [NSObject performSelectorOnMainThread:withObject:waitUntilDone:NO] 就会导致较多的异常。
-             3. iOS 12 下,在非第2点里提到的情况下,iPhone、iPad 均可通过 UIScreen -_peripheryInsets 方法的返回值区分,但如果满足了第2点,则 iPad 无法使用这个方法,这种情况下要依赖第4点。
-             4. iOS 12 下,不管是否满足第2点,不管是什么设备类型,均可以通过一个满屏的 UIWindow 的 rootViewController.view.frame.origin.y 的值来区分,如果是非全面屏,这个值必定为20,如果是全面屏,则可能是24或44等不同的值。但由于创建 UIWindow、UIViewController 等均属于较大消耗,所以只在前面的步骤无法区分的情况下才会使用第4点。
-             5. 对于第4点,经测试与当前设备的方向、是否有勾选 project 里的 General - Hide status bar、当前是否处于来电模式的状态栏这些都没关系。
-             */
-            SEL peripheryInsetsSelector = NSSelectorFromString([NSString stringWithFormat:@"_%@%@", @"periphery", @"Insets"]);
-            UIEdgeInsets peripheryInsets = UIEdgeInsetsZero;
-            [[UIScreen mainScreen] qmui_performSelector:peripheryInsetsSelector withPrimitiveReturnValue:&peripheryInsets];
+        /*
+         检测方式解释/测试要点:
+         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) {
-                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;
-                    }
+                // 使用一个强制竖屏的 rootViewController,避免一个仅支持竖屏的 App 在横屏启动时会受这里创建的 window 的影响,导致状态栏、safeAreaInsets 等错乱
+                // https://github.com/Tencent/QMUI_iOS/issues/1263
+                _QMUIPortraitViewController *viewController = [_QMUIPortraitViewController new];
+                window.rootViewController = viewController;
+                if (CGRectGetMinY(viewController.view.frame) > 20) {
+                    peripheryInsets.bottom = 1;
                 }
             }
-            isNotchedScreen = peripheryInsets.bottom > 0 ? 1 : 0;
-        } else {
-            isNotchedScreen = [QMUIHelper is58InchScreen] ? 1 : 0;
         }
+        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) {
@@ -559,6 +610,22 @@ static NSInteger is65InchScreen = -1;
     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) {
@@ -625,6 +692,10 @@ static NSInteger is35InchScreen = -1;
     return is35InchScreen > 0;
 }
 
++ (CGSize)screenSizeFor69Inch {
+    return CGSizeMake(440, 956);
+}
+
 + (CGSize)screenSizeFor67InchAndiPhone14Later {
     return CGSizeMake(430, 932);// iPhone 14 Pro Max
 }
@@ -637,10 +708,18 @@ static NSInteger is35InchScreen = -1;
     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);
 }
@@ -700,6 +779,63 @@ static CGFloat preferredLayoutWidth = -1;
     static NSDictionary<NSString *, NSDictionary<NSNumber *, NSValue *> *> *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)],
@@ -737,6 +873,7 @@ static CGFloat preferredLayoutWidth = -1;
                 @(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)],
@@ -836,7 +973,7 @@ static CGFloat preferredLayoutWidth = -1;
     
     NSString *deviceKey = [QMUIHelper deviceModel];
     if (!dict[deviceKey]) {
-        deviceKey = @"iPhone15,2";// 默认按最新的机型处理,因为新出的设备肯定更大概率与上一代设备相似
+        deviceKey = @"iPhone16,1";// 默认按最新的机型处理,因为新出的设备肯定更大概率与上一代设备相似
     }
     if ([QMUIHelper isZoomedMode]) {
         deviceKey = [NSString stringWithFormat:@"%@-Zoom", deviceKey];
@@ -896,6 +1033,20 @@ static NSInteger isHighPerformanceDevice = -1;
     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;
 }
@@ -922,15 +1073,6 @@ static NSInteger isHighPerformanceDevice = -1;
     NSString *deviceModel = [QMUIHelper deviceModel];
     
     if (!UIApplication.sharedApplication.statusBarHidden) {
-#ifndef IOS16_SDK_ALLOWED
-        // Xcode 14 SDK 编译的才能在 iPhone 14 Pro 上读取到正确的值,否则会读到 iPhone 13 Pro 的值,过渡期间做个兼容
-        if (!IS_LANDSCAPE &&
-            ([deviceModel isEqualToString:@"iPhone15,2"] ||
-             [deviceModel isEqualToString:@"iPhone15,3"])
-            ) {
-            return 54;
-        }
-#endif
         return UIApplication.sharedApplication.statusBarFrame.size.height;
     }
     
@@ -947,8 +1089,13 @@ static NSInteger isHighPerformanceDevice = -1;
         // iPhone 13 Mini
         return 48;
     }
-    if ([deviceModel isEqualToString:@"iPhone15,2"] || [deviceModel isEqualToString:@"iPhone15,3"]) {
-        // iPhone 14 Pro & iPhone 14 Pro Max
+    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) {
@@ -957,6 +1104,33 @@ static NSInteger isHighPerformanceDevice = -1;
     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)
@@ -973,13 +1147,6 @@ static NSInteger isHighPerformanceDevice = -1;
     [window tintColorDidChange];
 }
 
-+ (UIStatusBarStyle)statusBarStyleDarkContent {
-    if (@available(iOS 13.0, *))
-        return UIStatusBarStyleDarkContent;
-    else
-        return UIStatusBarStyleDefault;
-}
-
 - (void)handleAppWillEnterForeground:(NSNotification *)notification {
     QMUIHelper.sharedInstance.shouldPreventAppearanceUpdating = NO;
 }
@@ -1042,6 +1209,22 @@ static NSInteger isHighPerformanceDevice = -1;
 
 @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 {

+ 19 - 1
Pods/QMUIKit/QMUIKit/QMUICore/QMUIRuntime.m

@@ -164,11 +164,29 @@ static BOOL strendswith(const char *str, const char *suffix) {
     return strncmp(str + lenstr - lensuffix, suffix, lensuffix) == 0;
 }
 
-static const headerType *getProjectImageHeader() {
+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];

+ 47 - 7
Pods/QMUIKit/QMUIKit/QMUIKit.h

@@ -13,7 +13,7 @@
 #ifndef QMUIKit_h
 #define QMUIKit_h
 
-static NSString * const QMUI_VERSION = @"4.6.0";
+static NSString * const QMUI_VERSION = @"4.8.0";
 
 #if __has_include("CAAnimation+QMUI.h")
 #import "CAAnimation+QMUI.h"
@@ -39,6 +39,10 @@ static NSString * const QMUI_VERSION = @"4.6.0";
 #import "NSCharacterSet+QMUI.h"
 #endif
 
+#if __has_include("NSDictionary+QMUI.h")
+#import "NSDictionary+QMUI.h"
+#endif
+
 #if __has_include("NSMethodSignature+QMUI.h")
 #import "NSMethodSignature+QMUI.h"
 #endif
@@ -63,6 +67,10 @@ static NSString * const QMUI_VERSION = @"4.6.0";
 #import "NSPointerArray+QMUI.h"
 #endif
 
+#if __has_include("NSRegularExpression+QMUI.h")
+#import "NSRegularExpression+QMUI.h"
+#endif
+
 #if __has_include("NSShadow+QMUI.h")
 #import "NSShadow+QMUI.h"
 #endif
@@ -103,6 +111,10 @@ static NSString * const QMUI_VERSION = @"4.6.0";
 #import "QMUIAssetsManager.h"
 #endif
 
+#if __has_include("QMUIBadgeLabel.h")
+#import "QMUIBadgeLabel.h"
+#endif
+
 #if __has_include("QMUIBadgeProtocol.h")
 #import "QMUIBadgeProtocol.h"
 #endif
@@ -127,6 +139,10 @@ static NSString * const QMUI_VERSION = @"4.6.0";
 #import "QMUICellSizeKeyCache.h"
 #endif
 
+#if __has_include("QMUICheckbox.h")
+#import "QMUICheckbox.h"
+#endif
+
 #if __has_include("QMUICollectionViewPagingLayout.h")
 #import "QMUICollectionViewPagingLayout.h"
 #endif
@@ -243,6 +259,22 @@ static NSString * const QMUI_VERSION = @"4.6.0";
 #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
@@ -319,16 +351,16 @@ static NSString * const QMUI_VERSION = @"4.6.0";
 #import "QMUIPopupContainerView.h"
 #endif
 
-#if __has_include("QMUIPopupMenuBaseItem.h")
-#import "QMUIPopupMenuBaseItem.h"
+#if __has_include("QMUIPopupMenuItem.h")
+#import "QMUIPopupMenuItem.h"
 #endif
 
-#if __has_include("QMUIPopupMenuButtonItem.h")
-#import "QMUIPopupMenuButtonItem.h"
+#if __has_include("QMUIPopupMenuItemView.h")
+#import "QMUIPopupMenuItemView.h"
 #endif
 
-#if __has_include("QMUIPopupMenuItemProtocol.h")
-#import "QMUIPopupMenuItemProtocol.h"
+#if __has_include("QMUIPopupMenuItemViewProtocol.h")
+#import "QMUIPopupMenuItemViewProtocol.h"
 #endif
 
 #if __has_include("QMUIPopupMenuView.h")
@@ -355,6 +387,14 @@ static NSString * const QMUI_VERSION = @"4.6.0";
 #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

+ 2 - 2
Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUICommonTableViewController.m

@@ -245,7 +245,7 @@ NSString *const QMUICommonTableViewControllerSectionFooterIdentifier = @"QMUISec
         }
     }
     // 分别测试过 iOS 13 及以下的所有版本,最终总结,对于 Plain 类型的 tableView 而言,要去掉 header / footer 请使用 0,对于 Grouped 类型的 tableView 而言,要去掉 header / footer 请使用 CGFLOAT_MIN
-    return PreferredValueForTableViewStyle(tableView.qmui_style, 0, TableViewGroupedSectionHeaderDefaultHeight, TableViewInsetGroupedSectionHeaderDefaultHeight);
+    return PreferredValueForTableViewStyle(tableView.style, 0, TableViewGroupedSectionHeaderDefaultHeight, TableViewInsetGroupedSectionHeaderDefaultHeight);
 }
 
 - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
@@ -257,7 +257,7 @@ NSString *const QMUICommonTableViewControllerSectionFooterIdentifier = @"QMUISec
         }
     }
     // 分别测试过 iOS 13 及以下的所有版本,最终总结,对于 Plain 类型的 tableView 而言,要去掉 header / footer 请使用 0,对于 Grouped 类型的 tableView 而言,要去掉 header / footer 请使用 CGFLOAT_MIN
-    return PreferredValueForTableViewStyle(tableView.qmui_style, 0, TableViewGroupedSectionFooterDefaultHeight, TableViewInsetGroupedSectionFooterDefaultHeight);
+    return PreferredValueForTableViewStyle(tableView.style, 0, TableViewGroupedSectionFooterDefaultHeight, TableViewInsetGroupedSectionFooterDefaultHeight);
 }
 
 // 是否有定义某个section的header title

+ 11 - 22
Pods/QMUIKit/QMUIKit/QMUIMainFrame/QMUINavigationController.m

@@ -129,6 +129,7 @@ static char kAssociatedObjectKey_qmui_viewWillAppearNotifyDelegate;
 
 - (void)qmui_didInitialize {
     [super qmui_didInitialize];
+    self.qmui_alwaysInvokeAppearanceMethods = YES;
     self.qmui_multipleDelegatesEnabled = YES;
     self.delegator = [[_QMUINavigationControllerDelegator alloc] init];
     self.delegator.navigationController = self;
@@ -313,16 +314,10 @@ static char kAssociatedObjectKey_qmui_viewWillAppearNotifyDelegate;
         animated = NO;
     }
     
-    if (self.isViewLoaded) {
-        if (self.view.window) {
-            // 增加 self.view.window 作为判断条件是因为当 UINavigationController 不可见时(例如上面盖着一个 prenset 起来的 vc,或者 nav 所在的 tabBar 切到别的 tab 去了),pushViewController 会被执行,但 navigationController:didShowViewController:animated: 的 delegate 不会被触发,导致 isViewControllerTransiting 的标志位无法正确恢复,所以做个保护。
-            // https://github.com/Tencent/QMUI_iOS/issues/261
-            if (animated) {
-                self.isViewControllerTransiting = YES;
-            }
-        } else {
-            QMUILogWarn(NSStringFromClass(self.class), @"push 的时候 navigationController 不可见(例如上面盖着一个 prenset vc,或者切到别的 tab,可能导致一些 UINavigationControllerDelegate 不会被调用");
-        }
+    // 增加 self.view.window 作为判断条件是因为当 UINavigationController 不可见时(例如上面盖着一个 present 起来的 vc,或者 nav 所在的 tabBar 切到别的 tab 去了),pushViewController 会被执行,但 navigationController:didShowViewController:animated: 的 delegate 不会被触发,导致 isViewControllerTransiting 的标志位无法正确恢复,所以做个保护。
+    // https://github.com/Tencent/QMUI_iOS/issues/261
+    if (animated && self.isViewLoaded && self.view.window) {
+        self.isViewControllerTransiting = YES;
     }
     
     // 在 push 前先设置好返回按钮的文字
@@ -398,12 +393,12 @@ static char kAssociatedObjectKey_qmui_viewWillAppearNotifyDelegate;
     
     if (state == UIGestureRecognizerStateEnded) {
         if (self.transitionCoordinator.cancelled) {
-            QMUILog(NSStringFromClass(self.class), @"手势返回放弃了");
+            QMUILog(NSStringFromClass(self.class), @"interactivePopGestureRecognizer canceled");
             UIViewController<QMUINavigationControllerTransitionDelegate> *temp = viewControllerWillDisappear;
             viewControllerWillDisappear = viewControllerWillAppear;
             viewControllerWillAppear = temp;
         } else {
-            QMUILog(NSStringFromClass(self.class), @"执行手势返回");
+            QMUILog(NSStringFromClass(self.class), @"interactivePopGestureRecognizer triggered");
         }
     }
     
@@ -457,14 +452,6 @@ static char kAssociatedObjectKey_qmui_viewWillAppearNotifyDelegate;
     // 1. 有 modal present 则优先交给 modal present 的 vc 控制(例如进入搜索状态且没指定 definesPresentationContext 的 UISearchController)
     UIViewController *childViewController = self.visibleViewController;
     
-    // 修复在 root controller 实现了 preferredStatusBarStyle 方法并且在其中调用 childViewControllerForStatusBarStyle 方法的情况下,iOS 12 present 起 AVPlayerViewController 在 dismiss 时会触发 preferredStatusBarStyle 死循环的 bug:因为 AVPlayerViewController 内部的 preferredStatusBarStyle 会转向 presentingViewController 的 preferredStatusBarStyle,而后者又会 return  AVPlayerViewController,于是死循环
-    if (@available(iOS 13.0, *)) {
-    } else {
-        if ([childViewController isKindOfClass:AVPlayerViewController.class]) {
-            return nil;
-        }
-    }
-    
     // 2. 如果 modal present 是一个 UINavigationController,则 self.visibleViewController 拿到的是该 UINavigationController.topViewController,而不是该 UINavigationController 本身,所以这里要特殊处理一下,才能让下文的 beingDismissed 判断生效
     if (childViewController.navigationController && (self.presentedViewController == childViewController.navigationController)) {
         childViewController = childViewController.navigationController;
@@ -643,16 +630,18 @@ QMUISynthesizeIdStrongProperty(qmuibbbt_backItem, setQmuibbbt_backItem);
     dispatch_once(&onceToken, ^{
         // 在先设置了 title 再设置 titleView 时,保证 titleView 的样式能正确。
         OverrideImplementation([UINavigationItem class], @selector(setTitleView:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
-            return ^(UINavigationItem *selfObject, QMUINavigationTitleView *titleView) {
+            return ^(UINavigationItem *selfObject, UIView *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.qmui_useAsNavigationTitleView) {
                     if ([selfObject.qmui_viewController respondsToSelector:@selector(qmui_titleViewTintColor)]) {
                         titleView.tintColor = ((id<QMUINavigationControllerDelegate>)selfObject.qmui_viewController).qmui_titleViewTintColor;
+                    } else if (QMUICMIActivated) {
+                        titleView.tintColor = NavBarTitleColor;
                     }
                 }
             };

+ 3 - 3
Pods/QMUIKit/QMUIKit/QMUIResources/Images.xcassets/Contents.json

@@ -1,6 +1,6 @@
 {
   "info" : {
-    "version" : 1,
-    "author" : "xcode"
+    "author" : "xcode",
+    "version" : 1
   }
-}
+}

+ 12 - 0
Pods/QMUIKit/QMUIKit/UIKitExtensions/CALayer+QMUI.h

@@ -52,6 +52,18 @@ typedef NS_OPTIONS (NSUInteger, QMUICornerMask) {
  */
 @property(nonatomic, strong, nullable) NSShadow *qmui_shadow;
 
+/**
+ 只有当前 layer 里被返回的路径包裹住的内容才能被看到,路径之外的区域被裁剪掉。
+ 该 block 会在 layer 大小发生变化时被调用,所以请根据 aLayer.bounds 计算实时的路径。
+ */
+@property(nonatomic, copy, nullable) UIBezierPath * (^qmui_maskPathBlock)(__kindof CALayer *aLayer);
+
+/**
+ 与 qmui_maskPathBlock 相反,返回的路径会将当前 layer 的内容裁切掉,例如假设返回一个 layer 中间的矩形路径,则这个矩形会被挖空,其他区域正常显示。
+ 该 block 会在 layer 大小发生变化时被调用,所以请根据 aLayer.bounds 计算实时的路径。
+ */
+@property(nonatomic, copy, nullable) UIBezierPath * (^qmui_evenOddMaskPathBlock)(__kindof CALayer *aLayer);
+
 /// 获取指定 name 值的 layer,包括 self 和 self.sublayers,会一直往 sublayers 查找直到找到目标 layer。
 - (nullable __kindof CALayer *)qmui_layerWithName:(NSString *)name;
 

+ 59 - 0
Pods/QMUIKit/QMUIKit/UIKitExtensions/CALayer+QMUI.m

@@ -165,6 +165,65 @@ static char kAssociatedObjectKey_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) {

+ 7 - 2
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSArray+QMUI.h

@@ -48,9 +48,14 @@ NS_ASSUME_NONNULL_BEGIN
 - (ObjectType _Nullable)qmui_firstMatchWithBlock:(BOOL (NS_NOESCAPE^)(ObjectType item))block;
 
 /**
-*  转换数组元素,将每个 item 都经过 block 转换成一遍 返回转换后的新数组
+*  转换数组元素,将每个 item 都经过 block 转换成一遍后返回一个等长的数组。
 */
-- (NSArray *)qmui_mapWithBlock:(id (NS_NOESCAPE^)(ObjectType item))block;
+- (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
 

+ 16 - 2
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSArray+QMUI.m

@@ -96,14 +96,28 @@
     return nil;
 }
 
-- (NSArray *)qmui_mapWithBlock:(id (NS_NOESCAPE^)(id item))block {
+- (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])];
+        [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];
 }

+ 20 - 0
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.h

@@ -68,6 +68,26 @@ UIKIT_EXTERN NSAttributedStringKey const QMUIImageMarginsAttributeName;
  */
 + (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)

+ 36 - 0
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSAttributedString+QMUI.m

@@ -65,6 +65,14 @@ NSString *const kQMUIImageOriginalAttributedStringKey = @"QMUI_attributedString"
     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 - <QMUIStringProtocol>
 
 - (NSUInteger)qmui_lengthWhenCountingNonASCIICharacterAsTwo {
@@ -105,6 +113,34 @@ NSString *const kQMUIImageOriginalAttributedStringKey = @"QMUI_attributedString"
 
 @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 {

+ 30 - 36
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSObject+QMUI.m

@@ -260,25 +260,21 @@
 @implementation NSObject (QMUI_KeyValueCoding)
 
 - (id)qmui_valueForKey:(NSString *)key {
-    if (@available(iOS 13.0, *)) {
-        if ([self isKindOfClass:[UIView class]] && QMUICMIActivated && !IgnoreKVCAccessProhibited) {
-            BeginIgnoreUIKVCAccessProhibited
-            id value = [self valueForKey:key];
-            EndIgnoreUIKVCAccessProhibited
-            return value;
-        }
+    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 (@available(iOS 13.0, *)) {
-        if ([self isKindOfClass:[UIView class]] && QMUICMIActivated && !IgnoreKVCAccessProhibited) {
-            BeginIgnoreUIKVCAccessProhibited
-            [self setValue:value forKey:key];
-            EndIgnoreUIKVCAccessProhibited
-            return;
-        }
+    if ([self isKindOfClass:[UIView class]] && QMUICMIActivated && !IgnoreKVCAccessProhibited) {
+        BeginIgnoreUIKVCAccessProhibited
+        [self setValue:value forKey:key];
+        EndIgnoreUIKVCAccessProhibited
+        return;
     }
     
     [self setValue:value forKey:key];
@@ -508,30 +504,28 @@ QMUISynthesizeBOOLProperty(qmui_shouldIgnoreUIKVCAccessProhibited, setQmui_shoul
 @implementation NSException (QMUI_KVC)
 
 + (void)load {
-    if (@available(iOS 13.0, *)) {
-        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, ...) {
+    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;
                     
-                    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);
-                };
-            });
+                    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

+ 3 - 3
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSString+QMUI.h

@@ -135,7 +135,7 @@ NS_ASSUME_NONNULL_BEGIN
  @param pattern 正则表达式
  @return 匹配到的第一个结果,如果没有匹配成功则返回 nil
  */
-- (NSString *)qmui_stringMatchedByPattern:(NSString *)pattern;
+- (nullable NSString *)qmui_stringMatchedByPattern:(NSString *)pattern;
 
 /**
  用正则表达式匹配字符串,返回匹配到的第一个结果里的指定分组(由参数 index 指定)。
@@ -144,7 +144,7 @@ NS_ASSUME_NONNULL_BEGIN
  @param index 要返回第几个分组,0表示整个正则表达式匹配到的结果,1表示匹配到的结果里的第1个分组(第1个括号)
  @return 返回匹配到的第一个结果里的指定分组,如果 index 超过总分组数则返回 nil。匹配失败也返回 nil。
  */
-- (NSString *)qmui_stringMatchedByPattern:(NSString *)pattern groupIndex:(NSInteger)index;
+- (nullable NSString *)qmui_stringMatchedByPattern:(NSString *)pattern groupIndex:(NSInteger)index;
 
 /**
  用正则表达式匹配字符串,返回匹配到的第一个结果里的指定分组(由参数 name 指定)。
@@ -153,7 +153,7 @@ NS_ASSUME_NONNULL_BEGIN
  @param name 要返回的分组名称,可通过 pattern 里的 ?<name> 语法对分组进行命名。
  @return 返回匹配到的第一个结果里的指定分组,如果 name 不存在则返回 nil。匹配失败也返回 nil。
  */
-- (NSString *)qmui_stringMatchedByPattern:(NSString *)pattern groupName:(NSString *)name;
+- (nullable NSString *)qmui_stringMatchedByPattern:(NSString *)pattern groupName:(NSString *)name;
 
 /**
  *  用正则表达式匹配字符串并将其替换为指定的另一个字符串,大小写不敏感

+ 8 - 7
Pods/QMUIKit/QMUIKit/UIKitExtensions/NSString+QMUI.m

@@ -19,6 +19,7 @@
 #import "NSArray+QMUI.h"
 #import "NSCharacterSet+QMUI.h"
 #import "QMUIStringPrivate.h"
+#import "NSRegularExpression+QMUI.h"
 
 @implementation NSString (QMUI)
 
@@ -58,7 +59,9 @@
 - (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],
@@ -156,8 +159,7 @@
         return self;
     }
     
-    NSError *error = nil;
-    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[\u0300-\u036F]" options:NSRegularExpressionCaseInsensitive error:&error];
+    NSRegularExpression *regex = [NSRegularExpression qmui_cachedRegularExpressionWithPattern:@"[\u0300-\u036F]"];
     NSString *modifiedString = [regex stringByReplacingMatchesInString:self options:NSMatchingReportProgress range:NSMakeRange(0, self.length) withTemplate:@""];
     return modifiedString;
 }
@@ -169,7 +171,7 @@
 - (NSString *)qmui_stringMatchedByPattern:(NSString *)pattern groupIndex:(NSInteger)index {
     if (pattern.length <= 0 || index < 0) return nil;
     
-    NSRegularExpression *regx = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error: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];
@@ -181,7 +183,7 @@
 - (NSString *)qmui_stringMatchedByPattern:(NSString *)pattern groupName:(NSString *)name {
     if (pattern.length <= 0) return nil;
     
-    NSRegularExpression *regx = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error: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];
@@ -195,9 +197,8 @@
 }
 
 - (NSString *)qmui_stringByReplacingPattern:(NSString *)pattern withString:(NSString *)replacement {
-    NSError *error = nil;
-    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
-    if (error) {
+    NSRegularExpression *regex = [NSRegularExpression qmui_cachedRegularExpressionWithPattern:pattern];
+    if (!regex) {
         return self;
     }
     return [regex stringByReplacingMatchesInString:self options:NSMatchingReportCompletion range:NSMakeRange(0, self.length) withTemplate:replacement];

+ 8 - 19
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIBarProtocol/UINavigationBar+QMUIBarProtocol.m

@@ -57,11 +57,7 @@ EndIgnoreClangWarning
 
 - (UIImageView *)qmui_shadowImageView {
     // bar 在 init 完就可以获取到 backgroundView 和 shadowView,无需关心调用时机的问题
-    if (@available(iOS 13, *)) {
-        return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView1"];
-    }
-    // iOS 10 及以后,在 bar 初始化之后就能获取到 backgroundView 和 shadowView 了
-    return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView"];
+    return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView1"];
 }
 
 - (UIVisualEffectView *)qmui_effectView {
@@ -74,20 +70,13 @@ EndIgnoreClangWarning
 - (NSArray<UIVisualEffectView *> *)qmui_effectViews {
     UIView *backgroundView = self.qmui_backgroundView;
     NSMutableArray<UIVisualEffectView *> *result = NSMutableArray.new;
-    if (@available(iOS 13.0, *)) {
-        UIVisualEffectView *backgroundEffectView1 = [backgroundView valueForKey:@"_effectView1"];
-        UIVisualEffectView *backgroundEffectView2 = [backgroundView valueForKey:@"_effectView2"];
-        if (backgroundEffectView1) {
-            [result addObject:backgroundEffectView1];
-        }
-        if (backgroundEffectView2) {
-            [result addObject:backgroundEffectView2];
-        }
-    } else {
-        UIVisualEffectView *backgroundEffectView = [backgroundView qmui_valueForKey:@"_backgroundEffectView"];
-        if (backgroundEffectView) {
-            [result addObject:backgroundEffectView];
-        }
+    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;
 }

+ 8 - 19
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIBarProtocol/UITabBar+QMUIBarProtocol.m

@@ -57,11 +57,7 @@ EndIgnoreClangWarning
 
 - (UIImageView *)qmui_shadowImageView {
     // bar 在 init 完就可以获取到 backgroundView 和 shadowView,无需关心调用时机的问题
-    if (@available(iOS 13, *)) {
-        return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView1"];
-    }
-    // iOS 10 及以后,在 bar 初始化之后就能获取到 backgroundView 和 shadowView 了
-    return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView"];
+    return [self.qmui_backgroundView qmui_valueForKey:@"_shadowView1"];
 }
 
 - (UIVisualEffectView *)qmui_effectView {
@@ -74,20 +70,13 @@ EndIgnoreClangWarning
 - (NSArray<UIVisualEffectView *> *)qmui_effectViews {
     UIView *backgroundView = self.qmui_backgroundView;
     NSMutableArray<UIVisualEffectView *> *result = NSMutableArray.new;
-    if (@available(iOS 13.0, *)) {
-        UIVisualEffectView *backgroundEffectView1 = [backgroundView valueForKey:@"_effectView1"];
-        UIVisualEffectView *backgroundEffectView2 = [backgroundView valueForKey:@"_effectView2"];
-        if (backgroundEffectView1) {
-            [result addObject:backgroundEffectView1];
-        }
-        if (backgroundEffectView2) {
-            [result addObject:backgroundEffectView2];
-        }
-    } else {
-        UIVisualEffectView *backgroundEffectView = [backgroundView qmui_valueForKey:@"_backgroundEffectView"];
-        if (backgroundEffectView) {
-            [result addObject:backgroundEffectView];
-        }
+    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;
 }

+ 89 - 6
Pods/QMUIKit/QMUIKit/UIKitExtensions/QMUIStringPrivate.m

@@ -100,7 +100,12 @@
     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) return @"";
+    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);
@@ -117,8 +122,20 @@
     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) return @"";
-    if (index == length) return [aString copy];// 根据系统 -[NSString substringToIndex:] 的注释,在 index 等于 length 时会返回 self 的 copy。
+    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));
@@ -135,7 +152,12 @@
     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) return @"";
+    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) {
@@ -166,11 +188,54 @@
 + (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) {
@@ -186,8 +251,9 @@
             }
             
             // 保护从 emoji 等 ComposedCharacterSequence 中间裁剪的场景
+            // 系统 emoji 键盘输入过程中一定会调用 substringFromIndex:text.length - 1,导致触发我们这个警告,这里特殊保护一下
             {
-                if (index < selfObject.length) {
+                if (index < selfObject.length && !QMUIAvoidSubstring) {
                     NSRange range = [selfObject rangeOfComposedCharacterSequenceAtIndex:index];
                     BOOL isValidatedIndex = range.location == index || NSMaxRange(range) == index;
                     if (!isValidatedIndex) {
@@ -272,7 +338,7 @@
                 if (NSMaxRange(range) < selfObject.length) {
                     NSRange range2 = [selfObject rangeOfComposedCharacterSequencesForRange:range];
                     BOOL isValidddatedRange = range.length == 0 || NSEqualRanges(range, range2);
-                    if (!isValidddatedRange) {
+                    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;
@@ -288,6 +354,23 @@
             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 {

+ 8 - 11
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.h

@@ -15,19 +15,16 @@
 
 #import <UIKit/UIKit.h>
 
-@interface UIActivityIndicatorView (QMUI)
+NS_ASSUME_NONNULL_BEGIN
 
 /**
- * 创建一个指定大小的UIActivityIndicatorView
- *
- * 系统的UIActivityIndicatorView尺寸是由UIActivityIndicatorViewStyle决定的,固定不变。因此创建后通过CGAffineTransformMakeScale将其缩放到指定大小。self.frame获取的值也是缩放后的值,不影响布局。
- * init 后也可以通过 UIView(QMUI).qmui_size 修改大小。
- *
- * @param style UIActivityIndicatorViewStyle
- * @param size  UIActivityIndicatorView的大小
- *
- * @return UIActivityIndicatorView对象
+ 内部通过重写系统方法来让 UIActivityIndicatorView 支持 setFrame: 方式修改尺寸,业务就像使用一个普通 UIView 一样去使用它即可。
  */
-- (instancetype)initWithActivityIndicatorStyle:(UIActivityIndicatorViewStyle)style size:(CGSize)size;
+@interface UIActivityIndicatorView (QMUI)
+
+/// 内部转圈的那个 imageView
+@property(nonatomic, strong, readonly) UIImageView *qmui_animatingView;
 
 @end
+
+NS_ASSUME_NONNULL_END

+ 62 - 10
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIActivityIndicatorView+QMUI.m

@@ -15,21 +15,73 @@
 
 #import "UIActivityIndicatorView+QMUI.h"
 #import "UIView+QMUI.h"
+#import "QMUICore.h"
+
+@interface UIActivityIndicatorView ()
+@property(nonatomic, assign) CGSize qmuiai_size;
+@end
 
 @implementation UIActivityIndicatorView (QMUI)
 
-- (instancetype)initWithActivityIndicatorStyle:(UIActivityIndicatorViewStyle)style size:(CGSize)size {
-    if (self = [self initWithActivityIndicatorStyle:style]) {
-        self.qmui_size = size;
-    }
-    return self;
+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;
+                }
+            };
+        });
+    });
 }
 
-- (void)setQmui_size:(CGSize)size {
-//    [super setQmui_size:qmui_size];
-    CGSize initialSize = self.bounds.size;
-    CGFloat scale = size.width / initialSize.width;
-    self.transform = CGAffineTransformMakeScale(scale, scale);
+- (UIImageView *)qmui_animatingView {
+    SEL sel = NSSelectorFromString(@"_animatingImageView");
+    if ([self respondsToSelector:sel]) {
+        BeginIgnorePerformSelectorLeaksWarning
+        return [self performSelector:sel];
+        EndIgnorePerformSelectorLeaksWarning
+    }
+    return nil;
 }
 
 @end

+ 2 - 2
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIBarItem+QMUI.m

@@ -28,13 +28,13 @@
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
         
-        // UIBarButtonItem -setView:
+        // -[UIBarButtonItem setView:]
         // @warning 如果作为 UIToolbar.items 使用,则 customView 的情况下,iOS 10 及以下的版本不会调用 setView:,所以那种情况改为在 setToolbarItems:animated: 时调用,代码见下方
         ExtendImplementationOfVoidMethodWithSingleArgument([UIBarButtonItem class], @selector(setView:), UIView *, ^(UIBarButtonItem *selfObject, UIView *firstArgv) {
             [UIBarItem setView:firstArgv inBarButtonItem:selfObject];
         });
         
-        // UITabBarItem -setView:
+        // -[UITabBarItem setView:]
         ExtendImplementationOfVoidMethodWithSingleArgument([UITabBarItem class], @selector(setView:), UIView *, ^(UITabBarItem *selfObject, UIView *firstArgv) {
             [UIBarItem setView:firstArgv inBarItem:selfObject];
         });

+ 6 - 8
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIButton+QMUI.m

@@ -68,14 +68,12 @@ QMUISynthesizeIdStrongProperty(qbt_statesWithImageTintColor, setQbt_statesWithIm
             };
         });
         
-        if (@available(iOS 13, *)) {
-            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];
-                }
-            });
-        }
+        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];
+            }
+        });
     });
 }
 

+ 30 - 32
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIColor+QMUI.m

@@ -320,32 +320,34 @@ NSString *const QMUICGColorOriginalColorBindKey = @"QMUICGColorOriginalColorBind
 + (void)load {
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
-        if (@available(iOS 13.0, *)) {
-            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);
+        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) {
                     
-                    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
-                        result = CGColorCreate(CGColorSpaceCreateDeviceRGB(), (CGFloat[]){color.qmui_red, color.qmui_green, color.qmui_blue, color.qmui_alpha});
-                        
-                        [(__bridge id)(result) qmui_bindObject:selfObject forKey:QMUICGColorOriginalColorBindKey];
-                    }
-                    return result;
-                };
-            });
-        }
+                    // 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;
+            };
+        });
     });
 }
 
@@ -365,13 +367,9 @@ NSString *const QMUICGColorOriginalColorBindKey = @"QMUICGColorOriginalColorBind
 }
 
 - (UIColor *)qmui_rawColor {
-    if (self.qmui_isDynamicColor) {
-        if (@available(iOS 13.0, *)) {
-            if ([self respondsToSelector:@selector(resolvedColorWithTraitCollection:)]) {
-                UIColor *color = [self resolvedColorWithTraitCollection:UITraitCollection.currentTraitCollection];
-                return color.qmui_rawColor;
-            }
-        }
+    if (self.qmui_isDynamicColor && [self respondsToSelector:@selector(resolvedColorWithTraitCollection:)]) {
+        UIColor *color = [self resolvedColorWithTraitCollection:UITraitCollection.currentTraitCollection];
+        return color.qmui_rawColor;
     }
     return self;
 }

+ 4 - 3
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIControl+QMUI.m

@@ -80,13 +80,14 @@ static char kAssociatedObjectKey_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];
-                                [selfObject sendActionsForAllTouchEventsIfCan];
-                                if (selfObject.highlighted) {
-                                    [selfObject setHighlighted:NO];
+                                [weakSelf sendActionsForAllTouchEventsIfCan];
+                                if (weakSelf.highlighted) {
+                                    [weakSelf setHighlighted:NO];
                                 }
                             });
                         } else {

+ 11 - 1
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIGestureRecognizer+QMUI.m

@@ -15,6 +15,7 @@
 
 #import "UIGestureRecognizer+QMUI.h"
 #import "QMUICore.h"
+#import "UIView+QMUI.h"
 
 @implementation UIGestureRecognizer (QMUI)
 
@@ -27,7 +28,16 @@
                 // 检测常见的错误,例如在 viewWillAppear: 里把系统手势返回禁用,会导致从下一个界面手势返回到当前界面的瞬间,手势返回无效,界面处于混乱状态,无法接受任何点击事件
                 // _UIParallaxTransitionPanGestureRecognizer
                 if ([NSStringFromClass(selfObject.class) containsString:@"_UIParallaxTransition"] && selfObject.enabled && !firstArgv && (selfObject.state == UIGestureRecognizerStateBegan || selfObject.state == UIGestureRecognizerStateChanged)) {
-                    QMUIAssert(NO, @"UIGestureRecognizer (QMUI)", @"在手势进行过程中把手势禁用,可能让界面状态出现错乱!");
+                    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

+ 3 - 0
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIImage+QMUI.h

@@ -70,6 +70,9 @@ typedef NS_ENUM(NSInteger, QMUIImageGradientType) {
  */
 + (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;
 

+ 36 - 1
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIImage+QMUI.m

@@ -55,6 +55,22 @@ CGSizeFlatSpecificScale(CGSize size, float scale) {
                 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;
+            };
+        });
     });
 }
 
@@ -74,6 +90,25 @@ CGSizeFlatSpecificScale(CGSize size, float scale) {
     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];
@@ -620,7 +655,7 @@ CGSizeFlatSpecificScale(CGSize size, float scale) {
             cLocations[i] = locations[i].qmui_CGFloatValue;
         }
 
-        CGGradientRef gradient = CGGradientCreateWithColors(spaceRef, (CFArrayRef)[colors qmui_mapWithBlock:^id _Nonnull(UIColor * _Nonnull item) {
+        CGGradientRef gradient = CGGradientCreateWithColors(spaceRef, (CFArrayRef)[colors qmui_mapWithBlock:^id _Nonnull(UIColor * _Nonnull item, NSInteger index) {
             return (id)item.CGColor;
         }], cLocations);
         if (type == QMUIImageGradientTypeRadial) {

+ 1 - 1
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIInterface+QMUI.h

@@ -53,7 +53,7 @@ NS_ASSUME_NONNULL_BEGIN
 /**
  尝试将手机旋转为指定方向。请确保传进来的参数属于 -[UIViewController supportedInterfaceOrientations] 返回的范围内,如不在该范围内会旋转失败。
  @return 旋转成功则返回 YES,旋转失败返回 NO。
- @note 请注意与 @c qmui_setNeedsUpdateOfSupportedInterfaceOrientations 的区别:如果你的界面支持N个方向,而你希望保持对这N个方向的支持的情况下把设备方向旋转为这N个方向里的某一个时,应该调用 @c qmui_rotateToInterfaceOrientation: 。如果你的界面支持N个方向,而某些情况下你希望把N换成M并触发设备的方向刷新,则请修改方向后,调用 @c qmui_setNeedsUpdateOfSupportedInterfaceOrientations 。
+ @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;
 

+ 7 - 0
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIInterface+QMUI.m

@@ -21,6 +21,8 @@
 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;
 }
@@ -178,8 +180,13 @@ QMUISynthesizeNSIntegerProperty(lastOrientationChangedByHelper, setLastOrientati
 }
 
 - (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;

+ 12 - 0
Pods/QMUIKit/QMUIKit/UIKitExtensions/UILabel+QMUI.h

@@ -60,6 +60,18 @@ extern const CGFloat QMUILineHeightIdentity;
  */
 @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上
  *

+ 37 - 12
Pods/QMUIKit/QMUIKit/UIKitExtensions/UILabel+QMUI.m

@@ -42,11 +42,20 @@ const CGFloat QMUILineHeightIdentity = -1000;
         for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); index++) {
             SEL originalSelector = selectors[index];
             SEL swizzledSelector = NSSelectorFromString([@"qmuilb_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
-            ExchangeImplementations([self class], originalSelector, swizzledSelector);
+            ExchangeImplementations([UILabel class], originalSelector, swizzledSelector);
         }
     });
 }
 
+- (instancetype)qmui_initWithFont:(UIFont *)font textColor:(UIColor *)textColor {
+    BeginIgnoreClangWarning(-Wunused-value)
+    [self init];
+    EndIgnoreClangWarning
+    self.font = font;
+    self.textColor = textColor;
+    return self;
+}
+
 - (void)qmuilb_setText:(NSString *)text {
     if (!text) {
         [self qmuilb_setText:text];
@@ -96,7 +105,7 @@ static char kAssociatedObjectKey_textAttributes;
         NSMutableArray *willRemovedAttributes = [NSMutableArray array];
         [string enumerateAttributesInRange:NSMakeRange(0, string.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
             // 如果存在 kern 属性,则只有 range 是第一个字至倒数第二个字,才有可能是通过 qmui_textAttribtus 设置的
-            if (NSEqualRanges(range, NSMakeRange(0, string.length - 1)) && [attrs[NSKernAttributeName] isEqualToNumber:prevTextAttributes[NSKernAttributeName]]) {
+            if (NSEqualRanges(range, NSMakeRange(0, string.length - 1)) && [attrs[NSKernAttributeName] isEqual:prevTextAttributes[NSKernAttributeName]]) {
                 [string removeAttribute:NSKernAttributeName range:NSMakeRange(0, string.length - 1)];
             }
             // 上面排除掉 kern 属性后,如果 range 不是整个字符串,那肯定不是通过 qmui_textAttributes 设置的
@@ -160,7 +169,7 @@ static char kAssociatedObjectKey_textAttributes;
         [attributedString addAttribute:NSParagraphStyleAttributeName value:paraStyle range:NSMakeRange(0, attributedString.length)];
         
         // iOS 默认文字底对齐,改了行高要自己调整才能保证文字一直在 label 里垂直居中
-        CGFloat baselineOffset = (self.qmui_lineHeight - self.font.lineHeight) / 4;// 实际测量得知,baseline + 1,文字会往上移动 2pt,所以这里为了垂直居中,需要 / 4。
+        CGFloat baselineOffset = [QMUIHelper baselineOffsetWhenVerticalAlignCenterInHeight:self.qmui_lineHeight withFont:self.font];
         [attributedString addAttribute:NSBaselineOffsetAttributeName value:@(baselineOffset) range:NSMakeRange(0, attributedString.length)];
     }
     
@@ -240,13 +249,24 @@ static char kAssociatedObjectKey_lineHeight;
     return !!objc_getAssociatedObject(self, &kAssociatedObjectKey_lineHeight);
 }
 
-- (instancetype)qmui_initWithFont:(UIFont *)font textColor:(UIColor *)textColor {
-    BeginIgnoreClangWarning(-Wunused-value)
-    [self init];
-    EndIgnoreClangWarning
-    self.font = font;
-    self.textColor = textColor;
-    return self;
+- (CGFloat)qmui_centerOfCapHeight {
+    NSRange range = NSMakeRange(0, self.attributedText.length);
+    UIFont *font = [self.attributedText attribute:NSFontAttributeName atIndex:0 effectiveRange:&range];
+    if (!font) {
+        font = self.font;
+    }
+    CGFloat center = CGRectGetHeight(self.bounds) + font.descender - font.capHeight / 2;
+    return center;
+}
+
+- (CGFloat)qmui_centerOfXHeight {
+    NSRange range = NSMakeRange(0, self.attributedText.length);
+    UIFont *font = [self.attributedText attribute:NSFontAttributeName atIndex:0 effectiveRange:&range];
+    if (!font) {
+        font = self.font;
+    }
+    CGFloat center = CGRectGetHeight(self.bounds) + font.descender - font.xHeight / 2;
+    return center;
 }
 
 - (void)qmui_setTheSameAppearanceAsLabel:(UILabel *)label {
@@ -293,13 +313,18 @@ static char kAssociatedObjectKey_showPrincipalLines;
         
         if (!self.qmui_layoutSubviewsBlock) {
             self.qmui_layoutSubviewsBlock = ^(UILabel * _Nonnull label) {
+                if (!label.attributedText.length) return;
                 if (!label.qmuilb_principalLineLayer || label.qmuilb_principalLineLayer.hidden)  return;
                 
                 label.qmuilb_principalLineLayer.frame  = label.bounds;
                 
                 NSRange range = NSMakeRange(0, label.attributedText.length);
-                CGFloat baselineOffset = [[label.attributedText attribute:NSBaselineOffsetAttributeName atIndex:0 effectiveRange:&range] doubleValue];
-                CGFloat lineOffset = baselineOffset * 2;
+                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);

+ 91 - 53
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIMenuController+QMUI.m

@@ -14,93 +14,131 @@
 
 #import "UIMenuController+QMUI.h"
 #import "QMUICore.h"
-
-@interface UIMenuController ()
-
-@property(nonatomic, assign) NSInteger qmui_originWindowLevel;
-@property(nonatomic, assign) BOOL qmui_windowLevelChanged;
-
-@end
+#import "NSArray+QMUI.h"
 
 @implementation UIMenuController (QMUI)
 
-QMUISynthesizeNSIntegerProperty(qmui_originWindowLevel, setQmui_originWindowLevel);
-QMUISynthesizeBOOLProperty(qmui_windowLevelChanged, setQmui_windowLevelChanged);
-
 static UIWindow *kMenuControllerWindow = nil;
-static BOOL kHasAddedMenuControllerNotification = NO;
 
 + (void)load {
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
-        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
-                if (@available(iOS 13.0, *)) {
-                    if (!kHasAddedMenuControllerNotification) {
-                        kHasAddedMenuControllerNotification = YES;
-                        [[NSNotificationCenter defaultCenter] addObserver:menuController selector:@selector(handleMenuWillShowNotification:) name:UIMenuControllerWillShowMenuNotification object:nil];
-                        [[NSNotificationCenter defaultCenter] addObserver:menuController selector:@selector(handleMenuWillHideNotification:) name:UIMenuControllerWillHideMenuNotification object:nil];
+        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;
-            };
-        });
+                    
+                    return menuController;
+                };
+            });
+        }
     });
 }
 
-- (void)handleMenuWillShowNotification:(NSNotification *)notification {
-    UIWindow *window = [self menuControllerWindow];
-    UIWindow *targetWindow = [self windowForFirstResponder];
++ (void)qmuimc_handleMenuWillShow {
+    UIWindow *window = [UIMenuController qmuimc_menuControllerWindow];
+    UIWindow *targetWindow = [UIMenuController qmuimc_firstResponderWindowExceptMainWindow];
     if (window && targetWindow && ![QMUIHelper isKeyboardVisible]) {
-        QMUILog(NSStringFromClass(self.class), @"show menu - cur window level = %@, origin window level = %@ target window level = %@", @(window.windowLevel), @(self.qmui_originWindowLevel), @(targetWindow.windowLevel));
-        self.qmui_windowLevelChanged = YES;
-        self.qmui_originWindowLevel = window.windowLevel;
+        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)handleMenuWillHideNotification:(NSNotification *)notification {
-    UIWindow *window = [self menuControllerWindow];
-    if (window && self.qmui_windowLevelChanged) {
-        QMUILog(NSStringFromClass(self.class), @"hide menu - cur window level = %@, origin window level = %@", @(window.windowLevel), @(self.qmui_originWindowLevel));
-        window.windowLevel = self.qmui_originWindowLevel;
-        self.qmui_originWindowLevel = 0;
-        self.qmui_windowLevelChanged = NO;
++ (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 *)menuControllerWindow {
++ (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) {
-            [window.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull subview, NSUInteger idx, BOOL * _Nonnull stop) {
-                NSString *targetView = [NSString stringWithFormat:@"UI%@%@", @"Callout", @"Bar"];
-                if ([subview isKindOfClass:NSClassFromString(targetView)]) {
+            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;
-                    *stop = YES;
                 }
-            }];
+            } 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 *)windowForFirstResponder {
++ (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 = [self findFirstResponderInView:window];
+            UIResponder *responder = [UIMenuController qmuimc_findFirstResponderInView:window];
             if (responder) {
                 resultWindow = window;
                 *stop = YES;
@@ -110,12 +148,12 @@ static BOOL kHasAddedMenuControllerNotification = NO;
     return resultWindow;
 }
 
-- (UIResponder *)findFirstResponderInView:(UIView *)view {
++ (UIResponder *)qmuimc_findFirstResponderInView:(UIView *)view {
     if (view.isFirstResponder) {
         return view;
     }
     for (UIView *subView in view.subviews) {
-        id responder = [self findFirstResponderInView:subView];
+        id responder = [UIMenuController qmuimc_findFirstResponderInView:subView];
         if (responder) {
             return responder;
         }

+ 73 - 69
Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m

@@ -84,81 +84,78 @@ NSString *const kShouldFixTitleViewBugKey = @"kShouldFixTitleViewBugKey";
         
         // [UIKit Bug] iOS 12 及以上的系统,如果设置了自己的 leftBarButtonItem,且 title 很长时,则当 pop 的时候,title 会瞬间跳到左边,与 leftBarButtonItem 重叠
         // https://github.com/Tencent/QMUI_iOS/issues/1217
-        if (@available(iOS 12.0, *)) {
+        // _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) {
             
-            // _UITAMICAdaptorView
-            Class adaptorClass = NSClassFromString([NSString qmui_stringByConcat:@"_", @"UITAMIC", @"Adaptor", @"View", nil]);
+            if (!navigationBar.topItem.leftBarButtonItem) return;
             
-            // -[_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;
-                        };
-                    }
-                };
-            });
+            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;
             
-            void (^boundTitleViewMinXBlock)(UINavigationBar *, BOOL) = ^void(UINavigationBar *navigationBar, BOOL cleanup) {
+            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) {
                 
-                if (!navigationBar.topItem.leftBarButtonItem) return;
+                boundTitleViewMinXBlock(selfObject, NO);
                 
-                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;
+                // 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) {
                 
-                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];
-                }
+                // call super
+                void (*originSelectorIMP)(id, SEL, BOOL, id);
+                originSelectorIMP = (void (*)(id, SEL, BOOL, id))originalIMPProvider();
+                originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv);
+                
+                boundTitleViewMinXBlock(selfObject, YES);
             };
-            
-            // // - [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 对应)。
@@ -344,7 +341,14 @@ NSString *const kShouldFixTitleViewBugKey = @"kShouldFixTitleViewBugKey";
                     originSelectorIMP(selfObject, originCMD, firstArgv);
                     
                     // 这里只希望识别 UINavigationController 自带的 navigationBar,不希望处理业务自己 new 的 bar,所以用 superview 是否为 UILayoutContainerView 来作为判断条件。
-                    if ([NSStringFromClass(selfObject.superview.class) hasPrefix:@"UILayoutContainer"] && !selfObject.window) {
+                    BOOL isSystemBar = [NSStringFromClass(selfObject.superview.class) hasPrefix:@"UILayoutContainer"];
+                    BOOL alreadyMoveToWindow = !!selfObject.window;
+                    BOOL isPresenting = NO;
+                    if (!alreadyMoveToWindow) {
+                        UINavigationController *nav = [selfObject.qmui_viewController isKindOfClass:UINavigationController.class] ? selfObject.qmui_viewController : nil;
+                         isPresenting = nav && nav.presentedViewController;
+                    }
+                    if (isSystemBar && !alreadyMoveToWindow && !isPresenting) {
                         QMUIAssert(NO, @"UINavigationBar (QMUI)", @"试图在 UINavigationBar 尚未添加到 window 上时就修改它的样式,可能导致 UINavigationBar 的样式无法与全局保持一致。");
                     }
                 };

+ 7 - 0
Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h

@@ -57,12 +57,19 @@ typedef void (^QMUINavigationActionDidChangeBlock)(QMUINavigationAction action,
  */
 - (void)qmui_addNavigationActionDidChangeBlock:(QMUINavigationActionDidChangeBlock)block;
 
+/// 系统的设定是当 UINavigationController 不可见时(例如上面盖着一个 present vc,或者切到别的 tab),push/pop 操作均不会调用 vc 的生命周期方法(viewDidLoad 也是在 nav 恢复可视时才触发),所以提供这个属性用于当你希望这种情况下依然调用生命周期方法时,你可以打开它。默认为 NO。
+/// @warning 由于强制在 push/pop 时触发生命周期方法,所以会导致 vc 的 viewDidLoad 等方法比系统默认的更早调用,知悉即可。
+@property(nonatomic, assign) BOOL qmui_alwaysInvokeAppearanceMethods;
+
 /// 是否在 push 的过程中
 @property(nonatomic, readonly) BOOL qmui_isPushing;
 
 /// 是否在 pop 的过程中,包括手势、以及代码触发的 pop
 @property(nonatomic, readonly) BOOL qmui_isPopping;
 
+/// 以系统私有方法的方式去判断当前正在进行 push 动画还是 pop 动画,注意 setViewControllers 直接表现也是 push 或 pop 动画,可以通过 qmui_lastOperation 得知,但 qmui_isPushing、qmui_isPopping 无法区分 setViewControllers 的情况。
+@property(nonatomic, readonly) UINavigationControllerOperation qmui_lastOperation;
+
 /// 获取顶部的 ViewController,相比于系统的方法,这个方法能获取到 pop 的转场过程中顶部还没有完全消失的 ViewController (请注意:这种情况下,获取到的 topViewController 已经不在栈内)
 @property(nullable, nonatomic, readonly) UIViewController *qmui_topViewController;
 

+ 94 - 36
Pods/QMUIKit/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m

@@ -34,6 +34,7 @@
 
 @implementation UINavigationController (QMUI)
 
+QMUISynthesizeBOOLProperty(qmui_alwaysInvokeAppearanceMethods, setQmui_alwaysInvokeAppearanceMethods)
 QMUISynthesizeIdStrongProperty(qmuinc_navigationActionDidChangeBlocks, setQmuinc_navigationActionDidChangeBlocks)
 QMUISynthesizeIdWeakProperty(qmui_endedTransitionTopViewController, setQmui_endedTransitionTopViewController)
 QMUISynthesizeIdWeakProperty(qmui_interactivePopGestureRecognizerDelegate, setQmui_interactivePopGestureRecognizerDelegate)
@@ -72,35 +73,33 @@ QMUISynthesizeIdStrongProperty(qmui_interactiveGestureDelegator, setQmui_interac
         });
         
         // iOS 12 及以前,initWithNavigationBarClass:toolbarClass:、initWithRootViewController: 会调用 initWithNibName:bundle:,所以这两个方法在 iOS 12 下不需要再次调用 qmui_didInitialize 了。
-        if (@available(iOS 13.0, *)) {
-            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;
-                };
-            });
-        }
+        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) {
@@ -177,8 +176,13 @@ QMUISynthesizeIdStrongProperty(qmui_interactiveGestureDelegator, setQmui_interac
         OverrideImplementation([UINavigationController class], @selector(pushViewController:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
             return ^(UINavigationController *selfObject, UIViewController *viewController, BOOL animated) {
                 
-                if (selfObject.presentedViewController) {
-                    QMUILogWarn(NSStringFromClass(originClass), @"push 的时候 UINavigationController 存在一个盖在上面的 presentedViewController,可能导致一些 UINavigationControllerDelegate 不会被调用");
+                BOOL shouldInvokeAppearanceMethod = NO;
+                
+                if (selfObject.isViewLoaded && !selfObject.view.window) {
+                    QMUILogWarn(NSStringFromClass(originClass), @"push 的时候 navigationController 不可见(例如上面盖着一个 present vc,或者切到别的 tab,可能导致 vc 的生命周期方法或者 UINavigationControllerDelegate 不会被调用");
+                    if (selfObject.qmui_alwaysInvokeAppearanceMethods) {
+                        shouldInvokeAppearanceMethod = YES;
+                    }
                 }
                 
                 if ([selfObject.viewControllers containsObject:viewController]) {
@@ -193,9 +197,10 @@ QMUISynthesizeIdStrongProperty(qmui_interactiveGestureDelegator, setQmui_interac
                     originSelectorIMP(selfObject, originCMD, viewController, animated);
                 };
                 
-                BOOL willPushActually = viewController && ![viewController isKindOfClass:UITabBarController.class] && ![selfObject.viewControllers containsObject:viewController];
+                BOOL willPushActually = viewController && ![selfObject.viewControllers containsObject:viewController];
                 
                 if (!willPushActually) {
+                    QMUIAssert(NO, @"UINavigationController (QMUI)", @"调用了 pushViewController 但实际上没 push 成功,viewController:%@", viewController);
                     callSuperBlock();
                     return;
                 }
@@ -205,6 +210,11 @@ QMUISynthesizeIdStrongProperty(qmui_interactiveGestureDelegator, setQmui_interac
                 
                 [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];
@@ -212,6 +222,11 @@ QMUISynthesizeIdStrongProperty(qmui_interactiveGestureDelegator, setQmui_interac
                 [selfObject qmui_animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext>  _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];
+                    }
                 }];
             };
         });
@@ -238,11 +253,25 @@ QMUISynthesizeIdStrongProperty(qmui_interactiveGestureDelegator, setQmui_interac
                     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<UIViewController *> *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
@@ -255,6 +284,11 @@ QMUISynthesizeIdStrongProperty(qmui_interactiveGestureDelegator, setQmui_interac
                 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 并不会被执行,所以这里改为手动调用
@@ -425,18 +459,27 @@ static char kAssociatedObjectKey_navigationAction;
     [self.qmuinc_navigationActionDidChangeBlocks addObject:block];
 }
 
-// TODO: molice 改为用 QMUINavigationAction 判断
 - (BOOL)qmui_isPushing {
     BOOL isPushing = self.qmui_navigationAction > QMUINavigationActionWillPush && self.qmui_navigationAction <= QMUINavigationActionPushCompleted;
     return isPushing;
 }
 
-// TODO: molice 改为用 QMUINavigationAction 判断
 - (BOOL)qmui_isPopping {
     BOOL isPopping = self.qmui_navigationAction > QMUINavigationActionWillPop && self.qmui_navigationAction <= QMUINavigationActionPopCompleted;
     return isPopping;
 }
 
+- (UINavigationControllerOperation)qmui_lastOperation {
+    // -[UINavigationController lastOperation]
+    SEL operationSEL = NSSelectorFromString([NSString qmui_stringByConcat:@"last", @"Operation", nil]);
+    if ([self respondsToSelector:operationSEL]) {
+        UINavigationControllerOperation operation = UINavigationControllerOperationNone;
+        [self qmui_performSelector:operationSEL withPrimitiveReturnValue:&operation];
+        return operation;
+    }
+    return UINavigationControllerOperationNone;
+}
+
 - (UIViewController *)qmui_topViewController {
     if (self.qmui_isPushing) {
         return self.topViewController;
@@ -445,7 +488,17 @@ static char kAssociatedObjectKey_navigationAction;
 }
 
 - (nullable UIViewController *)qmui_rootViewController {
-    return self.viewControllers.firstObject;
+    UIViewController *rootViewController = self.viewControllers.firstObject;
+    // 系统 UINavigationController 的 popToViewController、popToRootViewController、setViewControllers 三种 pop 的方式都有一个共同的特点,假如此时有3个及以上的 vc 例如 [A,B,C],从当前界面 pop 到非相邻的界面,例如C到A,执行完 pop 操作后立马访问 UINavigationController.viewControllers 预期应该得到 [A],实际上会得到 [C,A],过一会(nav.view layoutIfNeeded 之后)才变成正确的 [A]。同理,[A,B,C,D]时从 D pop 到 B,预期得到[A,B],实际得到[D,A,B],也即这种情况它总是会把当前界面塞到 viewControllers 数组里的第一个,这就导致这期间访问基于 viewControllers 数组实现的功能(例如 qmui_rootViewController、qmui_previousViewController),都可能出错,所以这里对上述情况做特殊保护。
+    // 如果 pop 操作时只有2个vc,则没这种问题。
+    if (self.viewControllers.count > 1 && self.qmui_isPopping && self.transitionCoordinator) {
+        id<UIViewControllerTransitionCoordinator> 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 {
@@ -528,9 +581,14 @@ static char kAssociatedObjectKey_navigationAction;
         if ([originGestureDelegate respondsToSelector:_cmd]) {
             BOOL originalValue = YES;
             [originGestureDelegate qmui_performSelector:_cmd withPrimitiveReturnValue:&originalValue arguments:&gestureRecognizer, &event, nil];
-            if (!originalValue && [self.parentViewController shouldForceEnableInteractivePopGestureRecognizer]) {
+            if (!originalValue
+                // 在开启 forceEnableInteractivePopGestureRecognizer 的界面被 push 的过程中快速手势返回,容易导致 App 卡死
+                // https://github.com/Tencent/QMUI_iOS/issues/1498
+                && self.parentViewController.qmui_navigationAction == QMUINavigationActionUnknow
+                && [self.parentViewController shouldForceEnableInteractivePopGestureRecognizer]) {
                 return YES;
             }
+            
             return originalValue;
         }
     }

+ 11 - 1
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIScrollView+QMUI.h

@@ -15,6 +15,13 @@
 
 #import <UIKit/UIKit.h>
 
+typedef NS_ENUM(NSInteger, QMUIScrollPosition) {
+    QMUIScrollPositionNone,    // 滚动到临近的区域(可能是 Top 也可能是 Bottom)
+    QMUIScrollPositionTop,     // 滚动到可视区域最顶部
+    QMUIScrollPositionMiddle,  // 滚动到可视区域中间
+    QMUIScrollPositionBottom,  // 滚动到可视区域底部
+};
+
 @interface UIScrollView (QMUI)
 
 /// 判断UIScrollView是否已经处于顶部(当UIScrollView内容不够多不可滚动时,也认为是在顶部)
@@ -68,7 +75,10 @@
 /// 等同于[self qmui_scrollToBottomAnimated:NO]
 - (void)qmui_scrollToBottom;
 
-// 立即停止滚动,用于那种手指已经离开屏幕但列表还在滚动的情况。
+/// 将 scroll 坐标系内的指定 rect 滚动到指定位置。
+- (void)qmui_scrollToRect:(CGRect)rect atPosition:(QMUIScrollPosition)scrollPosition animated:(BOOL)animated;
+
+/// 立即停止滚动,用于那种手指已经离开屏幕但列表还在滚动的情况。
 - (void)qmui_stopDeceleratingIfNeeded;
 
 /**

+ 38 - 23
Pods/QMUIKit/QMUIKit/UIKitExtensions/UIScrollView+QMUI.m

@@ -42,33 +42,28 @@ QMUISynthesizeBOOLProperty(qmuiscroll_hasSetInitialContentInset, setQmuiscroll_h
                 NSString *result = originSelectorIMP(selfObject, originCMD);
                 
                 if (NSThread.isMainThread) {
-                    result = ([NSString stringWithFormat:@"%@, contentInset = %@", result, NSStringFromUIEdgeInsets(selfObject.contentInset)]);
-                    if (@available(iOS 13.0, *)) {
-                        result = result.mutableCopy;
-                    }
+                    result = ([NSString stringWithFormat:@"%@, contentInset = %@", result, NSStringFromUIEdgeInsets(selfObject.contentInset)]).mutableCopy;
                 }
                 return result;
             };
         });
         
-        if (@available(iOS 13.0, *)) {
-            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;
-                        }
-                    };
-                });
-            }
+        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;
+                    }
+                };
+            });
         }
     });
 }
@@ -86,7 +81,7 @@ QMUISynthesizeBOOLProperty(qmuiscroll_hasSetInitialContentInset, setQmuiscroll_h
         return YES;
     }
     
-    if (((NSInteger)self.contentOffset.y) == ((NSInteger)self.contentSize.height + self.adjustedContentInset.bottom - CGRectGetHeight(self.bounds))) {
+    if (CGFloatEqualToFloat(self.contentOffset.y, self.contentSize.height + self.adjustedContentInset.bottom - CGRectGetHeight(self.bounds))) {
         return YES;
     }
     
@@ -165,4 +160,24 @@ static char kAssociatedObjectKey_initialContentInset;
     } 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

+ 7 - 2
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchBar+QMUI.h

@@ -51,8 +51,9 @@ NS_ASSUME_NONNULL_BEGIN
 /// 支持根据 active 的值的不同来设置不一样的输入框位置偏移,当使用这个 block 后 @c qmui_textFieldMargins 无效。
 @property(nonatomic, copy) UIEdgeInsets (^qmui_textFieldMarginsBlock)(__kindof UISearchBar *searchBar, BOOL active);
 
-/// 获取 searchBar 内部的输入框的引用,在 searchBar 初始化完即可被获取
-@property(nullable, nonatomic, weak, readonly) UITextField *qmui_textField;
+/// 当 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;
@@ -98,6 +99,10 @@ NS_ASSUME_NONNULL_BEGIN
 /// 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;
 
 /// 生成指定颜色的搜索框输入框背景图,大小与系统默认的保持一致,只是颜色不同

+ 139 - 147
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m

@@ -17,6 +17,7 @@
 #import "QMUICore.h"
 #import "UIImage+QMUI.h"
 #import "UIView+QMUI.h"
+#import "UIViewController+QMUI.h"
 
 @interface UISearchBar ()
 
@@ -30,6 +31,7 @@
 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)
@@ -55,30 +57,18 @@ QMUISynthesizeCGFloatProperty(qmuisb_centerPlaceholderCachedWidth2, setQmuisb_ce
             }
         };
         
-        if (@available(iOS 13.0, *)) {
-            // 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);
-            });
-        } else {
-            ExtendImplementationOfVoidMethodWithoutArguments([UISearchBar class], NSSelectorFromString(@"_setupCancelButton"), ^(UISearchBar *selfObject) {
-                setupCancelButtonBlock(selfObject, selfObject.qmui_cancelButton);
-            });
-        }
+        // 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 = nil;
-                if (@available(iOS 13.0, *)) {
-                    searchBar = (UISearchBar *)selfObject.superview.superview.superview;
-                } else {
-                    searchBar = (UISearchBar *)selfObject.superview.superview;
-                }
-                
+                UISearchBar *searchBar = (UISearchBar *)selfObject.superview.superview.superview;;
                 if ([searchBar isKindOfClass:UISearchBar.class] && searchBar.qmui_alwaysEnableCancelButton && !searchBar.qmui_searchController) {
                     firstArgv = YES;
                 }
@@ -92,7 +82,7 @@ QMUISynthesizeCGFloatProperty(qmuisb_centerPlaceholderCachedWidth2, setQmuisb_ce
         
         ExtendImplementationOfVoidMethodWithSingleArgument([UISearchBar class], @selector(setPlaceholder:), NSString *, (^(UISearchBar *selfObject, NSString *placeholder) {
             if (selfObject.qmui_placeholderColor || selfObject.qmui_font) {
-                NSMutableAttributedString *string = selfObject.qmui_textField.attributedPlaceholder.mutableCopy;
+                NSMutableAttributedString *string = selfObject.searchTextField.attributedPlaceholder.mutableCopy;
                 if (selfObject.qmui_placeholderColor) {
                     [string addAttribute:NSForegroundColorAttributeName value:selfObject.qmui_placeholderColor range:NSMakeRange(0, string.length)];
                 }
@@ -101,85 +91,75 @@ QMUISynthesizeCGFloatProperty(qmuisb_centerPlaceholderCachedWidth2, setQmuisb_ce
                 }
                 // 默认移除文字阴影
                 [string removeAttribute:NSShadowAttributeName range:NSMakeRange(0, string.length)];
-                selfObject.qmui_textField.attributedPlaceholder = string.copy;
+                selfObject.searchTextField.attributedPlaceholder = string.copy;
             }
         }));
         
         // iOS 13 下,UISearchBar 内的 UITextField 的 _placeholderLabel 会在 didMoveToWindow 时被重新设置 textColor,导致我们在 searchBar 添加到界面之前设置的 placeholderColor 失效,所以在这里重新设置一遍
         // https://github.com/Tencent/QMUI_iOS/issues/830
-        if (@available(iOS 13.0, *)) {
-            ExtendImplementationOfVoidMethodWithoutArguments([UISearchBar class], @selector(didMoveToWindow), ^(UISearchBar *selfObject) {
-                if (selfObject.qmui_placeholderColor) {
-                    selfObject.placeholder = selfObject.placeholder;
-                }
-            });
-        }
-
-        if (@available(iOS 13.0, *)) {
-            // -[_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);
-                    };
+        ExtendImplementationOfVoidMethodWithoutArguments([UISearchBar class], @selector(didMoveToWindow), ^(UISearchBar *selfObject) {
+            if (selfObject.qmui_placeholderColor) {
+                selfObject.placeholder = selfObject.placeholder;
+            }
+        });
 
-                    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();
-                    }
+        // -[_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;
-                                }
+        });
+        
+        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);
-                    };
-                });
-            }
+                    }
+                    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 = nil;
-                if (@available(iOS 13.0, *)) {
-                    searchBar = (UISearchBar *)textField.superview.superview.superview;
-                } else {
-                    searchBar = (UISearchBar *)textField.superview.superview;
-                }
-                
+                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];
                 }
@@ -263,7 +243,7 @@ QMUISynthesizeCGFloatProperty(qmuisb_centerPlaceholderCachedWidth2, setQmuisb_ce
     self.qmui_alwaysEnableCancelButton = YES;
     self.qmui_showsLeftAccessoryView = YES;
     self.qmui_showsRightAccessoryView = YES;
-    
+    self.qmui_shouldFixSearchResultsContentInset = YES;
     if (QMUICMIActivated && ShouldFixSearchBarMaskViewLayoutBug) {
         self.qmui_fixMaskViewLayoutBugAutomatically = YES;
     }
@@ -275,7 +255,7 @@ static char kAssociatedObjectKey_centerPlaceholder;
     
     __weak __typeof(self)weakSelf = self;
     if (qmui_centerPlaceholder) {
-        self.qmui_textField.qmui_layoutSubviewsBlock = ^(UITextField * _Nonnull textField) {
+        self.searchTextField.qmui_layoutSubviewsBlock = ^(UITextField * _Nonnull textField) {
             
             // 某些中间状态 textField 的宽度会出现负值,但由于 CGRectGetWidth() 一定是返回正值的,所以这里必须用 bounds.size.width 的方式取值,而不是用 CGRectGetWidth()
             if (textField.bounds.size.width <= 0) return;
@@ -301,9 +281,9 @@ static char kAssociatedObjectKey_centerPlaceholder;
                 }
             }
         };
-        [self.qmui_textField setNeedsLayout];
+        [self.searchTextField setNeedsLayout];
     } else {
-        self.qmui_textField.qmui_layoutSubviewsBlock = nil;
+        self.searchTextField.qmui_layoutSubviewsBlock = nil;
         self.qmuisb_centerPlaceholderCachedWidth1 = 0;
         self.qmuisb_centerPlaceholderCachedWidth2 = 0;
         [self setPositionAdjustment:UIOffsetZero forSearchBarIcon:UISearchBarIconSearch];
@@ -330,7 +310,7 @@ static char kAssociatedObjectKey_PlaceholderColor;
 static char kAssociatedObjectKey_TextColor;
 - (void)setQmui_textColor:(UIColor *)qmui_textColor {
     objc_setAssociatedObject(self, &kAssociatedObjectKey_TextColor, qmui_textColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-    self.qmui_textField.textColor = qmui_textColor;
+    self.searchTextField.textColor = qmui_textColor;
 }
 
 - (UIColor *)qmui_textColor {
@@ -346,21 +326,13 @@ static char kAssociatedObjectKey_font;
     }
     
     // 更新输入框的文字样式
-    self.qmui_textField.font = qmui_font;
+    self.searchTextField.font = qmui_font;
 }
 
 - (UIFont *)qmui_font {
     return (UIFont *)objc_getAssociatedObject(self, &kAssociatedObjectKey_font);
 }
 
-- (UITextField *)qmui_textField {
-    if (@available(iOS 13.0, *)) {
-        return self.searchTextField;
-    }
-    UITextField *textField = [self qmui_valueForKey:@"searchField"];
-    return textField;
-}
-
 - (UIButton *)qmui_cancelButton {
     UIButton *cancelButton = [self qmui_valueForKey:@"cancelButton"];
     return cancelButton;
@@ -406,6 +378,35 @@ static char kAssociatedObjectKey_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;
@@ -471,8 +472,8 @@ static char kAssociatedObjectKey_textFieldMarginsBlock;
     // 输入框边框
     UIColor *textFieldBorderColor = SearchBarTextFieldBorderColor;
     if (textFieldBorderColor) {
-        self.qmui_textField.layer.borderWidth = PixelOne;
-        self.qmui_textField.layer.borderColor = textFieldBorderColor.CGColor;
+        self.searchTextField.layer.borderWidth = PixelOne;
+        self.searchTextField.layer.borderColor = textFieldBorderColor.CGColor;
     }
     
     // 整条bar的背景
@@ -511,7 +512,7 @@ static char kAssociatedObjectKey_showsLeftAccessoryView;
     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.qmui_textField.frame, self.qmui_leftAccessoryView.frame));
+            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];
@@ -520,7 +521,10 @@ static char kAssociatedObjectKey_showsLeftAccessoryView;
                 self.qmui_leftAccessoryView.transform = CGAffineTransformMakeTranslation(-CGRectGetMaxX(self.qmui_leftAccessoryView.frame), 0);
                 [self qmuisb_updateCustomTextFieldMargins];
             } completion:^(BOOL finished) {
-                self.qmui_leftAccessoryView.hidden = YES;
+                // 快速在 show/hide 之间切换,容易出现状态错误,所以做个保护
+                if (showsLeftAccessoryView == self.qmui_showsLeftAccessoryView) {
+                    self.qmui_leftAccessoryView.hidden = YES;
+                }
                 self.qmui_leftAccessoryView.transform = CGAffineTransformIdentity;
             }];
         }
@@ -542,7 +546,7 @@ static char kAssociatedObjectKey_leftAccessoryView;
 - (void)setQmui_leftAccessoryView:(UIView *)qmui_leftAccessoryView {
     if (self.qmui_leftAccessoryView != qmui_leftAccessoryView) {
         [self.qmui_leftAccessoryView removeFromSuperview];
-        [self.qmui_textField.superview addSubview:qmui_leftAccessoryView];
+        [self.searchTextField.superview addSubview:qmui_leftAccessoryView];
     }
     objc_setAssociatedObject(self, &kAssociatedObjectKey_leftAccessoryView, qmui_leftAccessoryView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
     
@@ -569,7 +573,7 @@ static char kAssociatedObjectKey_leftAccessoryViewMargins;
 // 这个方法会在 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.qmui_textField.frame) - [UISearchBar qmuisb_textFieldDefaultMargins].left - self.qmui_leftAccessoryViewMargins.right - CGRectGetWidth(self.qmui_leftAccessoryView.frame), CGRectGetMinYVerticallyCenter(self.qmui_textField.frame, self.qmui_leftAccessoryView.frame));
+        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));
     }
 }
 
@@ -582,7 +586,7 @@ static char kAssociatedObjectKey_showsRightAccessoryView;
         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.qmui_textField.frame, self.qmui_rightAccessoryView.frame));
+            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;
             }
@@ -597,7 +601,10 @@ static char kAssociatedObjectKey_showsRightAccessoryView;
                 self.qmui_rightAccessoryView.transform = CGAffineTransformMakeTranslation(CGRectGetWidth(self.qmui_rightAccessoryView.superview.bounds) - CGRectGetMinX(self.qmui_rightAccessoryView.frame), 0);
                 [self qmuisb_updateCustomTextFieldMargins];
             } completion:^(BOOL finished) {
-                self.qmui_rightAccessoryView.hidden = YES;
+                // 快速在 show/hide 之间切换,容易出现状态错误,所以做个保护
+                if (showsRightAccessoryView == self.qmui_showsRightAccessoryView) {
+                    self.qmui_rightAccessoryView.hidden = YES;
+                }
                 self.qmui_rightAccessoryView.transform = CGAffineTransformIdentity;
                 self.qmui_rightAccessoryView.alpha = 1;
             }];
@@ -625,7 +632,7 @@ static char kAssociatedObjectKey_rightAccessoryView;
 - (void)setQmui_rightAccessoryView:(UIView *)qmui_rightAccessoryView {
     if (self.qmui_rightAccessoryView != qmui_rightAccessoryView) {
         [self.qmui_rightAccessoryView removeFromSuperview];
-        [self.qmui_textField.superview addSubview:qmui_rightAccessoryView];
+        [self.searchTextField.superview addSubview:qmui_rightAccessoryView];
     }
     objc_setAssociatedObject(self, &kAssociatedObjectKey_rightAccessoryView, qmui_rightAccessoryView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
     
@@ -665,21 +672,16 @@ static char kAssociatedObjectKey_rightAccessoryViewMargins;
 // 这个方法会在 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.qmui_textField.frame) + [UISearchBar qmuisb_textFieldDefaultMargins].right + self.qmui_textFieldMargins.right + self.qmui_rightAccessoryViewMargins.left, CGRectGetMinYVerticallyCenter(self.qmui_textField.frame, self.qmui_rightAccessoryView.frame));
+        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.qmui_textField && !CGRectIsEmpty(self.qmui_textField.frame)) {
-        if (@available(iOS 13.0, *)) {
-            [self.qmui_textField.superview setNeedsLayout];
-            [self.qmui_textField.superview layoutIfNeeded];
-        } else {
-            [self setNeedsLayout];
-            [self layoutIfNeeded];
-        }
+    if (self.searchTextField && !CGRectIsEmpty(self.searchTextField.frame)) {
+        [self.searchTextField.superview setNeedsLayout];
+        [self.searchTextField.superview layoutIfNeeded];
     }
 }
 
@@ -689,17 +691,9 @@ static char kAssociatedObjectKey_rightAccessoryViewMargins;
 
 - (CGRect)qmuisb_adjustCancelButtonFrame:(CGRect)followingFrame {
     if (self.qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView) {
-        CGRect textFieldFrame = self.qmui_textField.frame;
-        
-        BOOL shouldFixCancelButton = NO;
-        if (@available(iOS 13.0, *)) {
-            shouldFixCancelButton = YES;// iOS 13 当 searchBar 作为 tableHeaderView 使用时,并且非搜索状态下 searchBar.showsCancelButton = YES,则进入搜搜状态后再退出,可看到 cancelButton 下降过程中会有抖动
-        } else {
-            shouldFixCancelButton = self.qmui_isActive;
-        }
-        if (shouldFixCancelButton) {
-            followingFrame = CGRectSetY(followingFrame, CGRectGetMinYVerticallyCenter(textFieldFrame, followingFrame));
-        }
+        CGRect textFieldFrame = self.searchTextField.frame;
+        // iOS 13 当 searchBar 作为 tableHeaderView 使用时,并且非搜索状态下 searchBar.showsCancelButton = YES,则进入搜搜状态后再退出,可看到 cancelButton 下降过程中会有抖动
+        followingFrame = CGRectSetY(followingFrame, CGRectGetMinYVerticallyCenter(textFieldFrame, followingFrame));
     }
     
     if (self.qmui_cancelButtonMarginsBlock) {
@@ -712,8 +706,8 @@ static char kAssociatedObjectKey_rightAccessoryViewMargins;
 - (void)qmuisb_adjustSegmentedControlFrameIfNeeded {
     if (!self.qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView) return;
     if (self.qmui_isActive) {
-        CGRect textFieldFrame = self.qmui_textField.frame;
-        if (self.qmui_segmentedControl.superview.qmui_top < self.qmui_textField.qmui_bottom) {
+        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);
         }
@@ -781,16 +775,11 @@ static char kAssociatedObjectKey_rightAccessoryViewMargins;
             frame.size.height = fixedHeight;
         }
         if (self.qmui_isActive) {
-            BOOL statusBarHidden = NO;
-            if (@available(iOS 13.0, *)) {
-                statusBarHidden = self.window.windowScene.statusBarManager.statusBarHidden;
-            } else {
-                statusBarHidden = UIApplication.sharedApplication.statusBarHidden;
-            }
+            BOOL statusBarHidden = self.window.windowScene.statusBarManager.statusBarHidden;
             CGFloat visibleHeight = statusBarHidden ? 56 : 50;
-            frame.origin.y = (visibleHeight - self.qmui_textField.qmui_height) / 2;
+            frame.origin.y = (visibleHeight - self.searchTextField.qmui_height) / 2;
         } else if (self.qmui_searchController.isBeingDismissed) {
-            frame.origin.y = (56 - self.qmui_textField.qmui_height) / 2;
+            frame.origin.y = (56 - self.searchTextField.qmui_height) / 2;
         }
     }
     
@@ -816,10 +805,10 @@ static char kAssociatedObjectKey_rightAccessoryViewMargins;
     // apply SearchBarTextFieldCornerRadius
     CGFloat textFieldCornerRadius = SearchBarTextFieldCornerRadius;
     if (textFieldCornerRadius != 0) {
-        textFieldCornerRadius = textFieldCornerRadius > 0 ? textFieldCornerRadius : CGRectGetHeight(self.qmui_textField.frame) / 2.0;
+        textFieldCornerRadius = textFieldCornerRadius > 0 ? textFieldCornerRadius : CGRectGetHeight(self.searchTextField.frame) / 2.0;
     }
-    self.qmui_textField.layer.cornerRadius = textFieldCornerRadius;
-    self.qmui_textField.clipsToBounds = textFieldCornerRadius != 0;
+    self.searchTextField.layer.cornerRadius = textFieldCornerRadius;
+    self.searchTextField.clipsToBounds = textFieldCornerRadius != 0;
     
     [self qmuisb_adjustLeftAccessoryViewFrameAfterTextFieldLayout];
     [self qmuisb_adjustRightAccessoryViewFrameAfterTextFieldLayout];
@@ -857,13 +846,17 @@ static char kAssociatedObjectKey_rightAccessoryViewMargins;
                 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) return;
+    if (!self.qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView || !self.qmui_shouldFixSearchResultsContentInset) return;
     if (self.qmui_isActive) {
         UIViewController *searchResultsController = self.qmui_searchController.searchResultsController;
         if (searchResultsController && [searchResultsController isViewLoaded]) {
@@ -872,8 +865,10 @@ static char kAssociatedObjectKey_rightAccessoryViewMargins;
             [view isKindOfClass:UIScrollView.class] ? view :
             [view.subviews.firstObject isKindOfClass:UIScrollView.class] ? view.subviews.firstObject : nil;
             UIView *searchBarContainerView = self.superview;
-            if (scrollView && searchBarContainerView) {
-                scrollView.contentInset = UIEdgeInsetsMake(searchBarContainerView.qmui_height, 0, 0, 0);
+            if (scrollView && scrollView.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentNever && searchBarContainerView) {
+                CGFloat containerHeight = CGRectGetHeight(searchBarContainerView.frame);
+                scrollView.contentInset = UIEdgeInsetsMake(containerHeight, 0, scrollView.safeAreaInsets.bottom, 0);
+                scrollView.scrollIndicatorInsets = scrollView.contentInset;
             }
         }
     }
@@ -882,11 +877,8 @@ static char kAssociatedObjectKey_rightAccessoryViewMargins;
 static CGSize textFieldDefaultSize;
 + (CGSize)qmuisb_textFieldDefaultSize {
     if (CGSizeIsEmpty(textFieldDefaultSize)) {
-        textFieldDefaultSize = CGSizeMake(60, 28);
         // 在 iOS 11 及以上,搜索输入框系统默认高度是 36,iOS 10 及以下的高度是 28
-        if (@available(iOS 11.0, *)) {
-            textFieldDefaultSize.height = 36;
-        }
+        textFieldDefaultSize = CGSizeMake(60, 36);
     }
     return textFieldDefaultSize;
 }

+ 19 - 0
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchController+QMUI.h

@@ -18,6 +18,25 @@ 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

+ 229 - 2
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISearchController+QMUI.m

@@ -13,23 +13,26 @@
 //
 
 #import "UISearchController+QMUI.h"
+#import "QMUICore.h"
 #import "UIViewController+QMUI.h"
 #import "UINavigationController+QMUI.h"
 #import "UIView+QMUI.h"
-#import "QMUICore.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);
                 
-                // 修复 https://github.com/Tencent/QMUI_iOS/issues/680 中提到的问题二:当有一个 TableViewController A,A 的 seachBar 被激活且 searchResultsController 正在显示的情况下,A.navigationController push 一个新的 viewController B,B 用 pop 手势返回到一半松手放弃返回,此时 B 再 push 一个新的 viewController 时,在转场过程中会看到 searchResultsController 的内容。
                 if (selfObject.window && [selfObject.superview isKindOfClass:NSClassFromString(@"UITransitionView")]) {
                     UIView *transitionView = selfObject.superview;
                     UISearchController *searchController = [selfObject qmui_viewController];
@@ -46,7 +49,231 @@
                 
             };
         });
+        
+        // - [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

+ 1 - 1
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISlider+QMUI.m

@@ -376,7 +376,7 @@ static char kAssociatedObjectKey_stepControlConfiguration;
     // 根据当前 thumbView 的位置,控制重叠的那个 stepControl 的事件响应和显隐,由于 slider 可能是 continuous 的,所以这段逻辑必须每次 layout 都调用,不能放在 layoutCachedKey 的保护里
     CGRect thumbRect = self.qmui_thumbView.frame;
     CGRect trackRect = [self trackRectForBounds:self.bounds];
-    NSUInteger step = (CGRectGetMidX(thumbRect) - CGRectGetMinX(trackRect)) / CGRectGetWidth(trackRect) * (count - 1);
+    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;

+ 10 - 14
Pods/QMUIKit/QMUIKit/UIKitExtensions/UISwitch+QMUI.m

@@ -30,8 +30,14 @@
         });
         
         // 设置 qmui_offTintColor 的原理是找到 UISwitch 内部的 switchWellView 并改变它的 backgroundColor,而 switchWellView 在某些时机会重新创建 ,因此需要在这些时机之后对 switchWellView 重新设置一次背景颜色:
-        if (@available(iOS 13.0, *)) {
-            ExtendImplementationOfVoidMethodWithSingleArgument([UISwitch class], @selector(traitCollectionDidChange:), UITraitCollection *, ^(UISwitch *selfObject, UITraitCollection *previousTraitCollection) {
+        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中进行
@@ -39,18 +45,8 @@
                         [selfObject qmui_applyOffTintColorIfNeeded];
                     });
                 }
-            });
-        } else {
-            // iOS 9 - 12 上调用 setOnTintColor: 或 setTintColor: 之后,会在重新创建 switchWellView
-            ExtendImplementationOfVoidMethodWithSingleArgument([UISwitch class], @selector(setTintColor:), UIColor *, ^(UISwitch *selfObject, UIColor *firstArgv) {
-                [selfObject qmui_applyOffTintColorIfNeeded];
-            });
-            ExtendImplementationOfVoidMethodWithSingleArgument([UISwitch class], @selector(setOnTintColor:), UIColor *, ^(UISwitch *selfObject, UIColor *firstArgv) {
-                [selfObject qmui_applyOffTintColorIfNeeded];
-            });
-
-        }
-        
+            };
+        });
     });
 }
 

+ 100 - 282
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITabBar+QMUI.m

@@ -43,62 +43,23 @@ QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouch
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
         
-        ExtendImplementationOfVoidMethodWithoutArguments([UITabBarController class], @selector(viewDidLoad), ^(UITabBarController *selfObject) {
-            if (QMUICMIActivated) {
-                if (@available(iOS 13.0, *)) {
-                    // iOS 13 不使用 tintColor 了,改为用 UITabBarAppearance,具体请看 QMUIConfiguration.m
-                } else {
-                    // 根据 TabBarContainerClasses 的值来决定是否设置 UITabBar.tintColor
-                    // UITabBar.tintColor 没有被添加 UI_APPEARANCE_SELECTOR 标记,所以没有采用 UIAppearance 的方式去实现(虽然它实际上是支持的)
-                    BOOL shouldSetTintColor = NO;
-                    if (TabBarContainerClasses.count) {
-                        for (Class class in TabBarContainerClasses) {
-                            if ([selfObject isKindOfClass:class]) {
-                                shouldSetTintColor = YES;
-                                break;
-                            }
-                        }
-                    } else {
-                        shouldSetTintColor = YES;
-                    }
-                    if (shouldSetTintColor) {
-                        selfObject.tabBar.tintColor = TabBarItemImageColorSelected;
-                    }
-                }
-            }
-        });
-        
-        // iOS 12 及以下,如果 UITabBar backgroundImage 为 nil,则 tabBar 会显示磨砂背景,此时不管怎么修改 shadowImage 都无效,都会显示系统默认的分隔线,导致无法很好地统一不同 iOS 版本的表现(iOS 13 及以上没有这个限制),所以这里做了兼容。
-        if (@available(iOS 13.0, *)) {
-        } else {
-            ExtendImplementationOfVoidMethodWithoutArguments(NSClassFromString(@"_UITabBarVisualProviderLegacyIOS"), NSSelectorFromString(@"_updateBackground"), ^(NSObject *selfObject) {
-                UITabBar *tabBar = [selfObject qmui_valueForKey:@"tabBar"];
-                if (!tabBar) return;
-                UIImage *shadowImage = tabBar.shadowImage;// 就算 tabBar 显示系统的分隔线,但依然能从 shadowImage 属性获取到业务自己设置的图片
-                UIImageView *shadowImageView = tabBar.qmui_shadowImageView;
-                if (shadowImage && shadowImageView && shadowImageView.backgroundColor && !shadowImageView.image) {
-                    shadowImageView.backgroundColor = nil;
-                    shadowImageView.image = shadowImage;
-                }
-            });
-        }
-        
-        OverrideImplementation([UITabBar class], @selector(setItems:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
-            return ^void(UITabBar *selfObject, NSArray<UITabBarItem *> *items, BOOL animated) {
+        // -[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, NSArray<UITabBarItem *> *, BOOL);
-                originSelectorIMP = (void (*)(id, SEL, NSArray<UITabBarItem *> *, BOOL))originalIMPProvider();
-                originSelectorIMP(selfObject, originCMD, items, animated);
+                void (*originSelectorIMP)(id, SEL, UIView *);
+                originSelectorIMP = (void (*)(id, SEL, UIView *))originalIMPProvider();
+                originSelectorIMP(selfObject, originCMD, firstArgv);
                 
-                [items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) {
-                    // 双击 tabBarItem 的功能需要在设置完 item 后才能获取到 qmui_view 来实现
-                    UIControl *itemView = (UIControl *)item.qmui_view;
-                    [itemView addTarget:selfObject action:@selector(handleTabBarItemViewEvent:) forControlEvents:UIControlEventTouchUpInside];
-                }];
+                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) {
                 
@@ -115,174 +76,36 @@ QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouch
             };
         });
         
-        OverrideImplementation([UITabBar class], @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
-            return ^(UITabBar *selfObject, CGRect frame) {
+        // 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) {
                 
-                if (UIApplication.sharedApplication.qmui_didFinishLaunching) {
-                    if (QMUICMIActivated && ShouldFixTabBarTransitionBugInIPhoneX && IOS_VERSION < 11.2 && IS_58INCH_SCREEN) {
-                        if (CGRectGetHeight(frame) == TabBarHeight && CGRectGetMaxY(frame) < CGRectGetHeight(selfObject.superview.bounds)) {
-                            // iOS 11 在界面 push 的过程中 tabBar 会瞬间往上跳,所以做这个修复。这个 bug 在 iOS 11.2 里已被系统修复。
-                            // https://github.com/Tencent/QMUI_iOS/issues/217
-                            frame = CGRectSetY(frame, CGRectGetHeight(selfObject.superview.bounds) - CGRectGetHeight(frame));
-                        }
-                    }
-                    
-                    // [UIKit Bug] iOS 11-12,opaque 的 tabBar 在某些情况下会高度塌陷
-                    // https://github.com/Tencent/QMUI_iOS/issues/309
-                    // [UIKit Bug] iOS 11-12,全面屏设备下,带 TabBar 的界面在 push/pop 后,UIScrollView 的滚动位置可能发生变化
-                    // https://github.com/Tencent/QMUI_iOS/issues/934
-                    if (@available(iOS 13.0, *)) {
-                    } else if (IS_NOTCHED_SCREEN && ((CGRectGetHeight(frame) == 49 || CGRectGetHeight(frame) == 32))) {// 只关注全面屏设备下的这两种非正常的 tabBar 高度即可
-                        CGFloat bottomSafeAreaInsets = selfObject.safeAreaInsets.bottom > 0 ? selfObject.safeAreaInsets.bottom : selfObject.superview.safeAreaInsets.bottom;// 注意,如果只是拿 selfObject.safeAreaInsets 判断,会肉眼看到高度的跳变,因此引入 superview 的值(虽然理论上 tabBar 不一定都会布局到 UITabBarController.view 的底部)
-                        if (bottomSafeAreaInsets == CGRectGetHeight(selfObject.frame)) {
-                            return;// 由于这个系统 bug https://github.com/Tencent/QMUI_iOS/issues/446,这里先暂时屏蔽本次 frame 变化
-                        }
-                        frame.size.height += bottomSafeAreaInsets;
-                        frame.origin.y -= bottomSafeAreaInsets;
+                // 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];
                 }
-                
-                // call super
-                void (*originSelectorIMP)(id, SEL, CGRect);
-                originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider();
-                originSelectorIMP(selfObject, originCMD, frame);
             };
         });
         
-        // 以下代码修复两个仅存在于 12.1.0 版本的系统 bug,实测 12.1.1 苹果已经修复
-        if (@available(iOS 12.1, *)) {
-            if (@available(iOS 12.1.1, *)) {
-            } else {
-                OverrideImplementation(NSClassFromString(@"UITabBarButton"), @selector(setFrame:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
-                    return ^(UIView *selfObject, CGRect firstArgv) {
-                        
-                        // Fixed: UITabBar layout is broken on iOS 12.1
-                        // https://github.com/Tencent/QMUI_iOS/issues/410
-                        
-                        if (!CGRectIsEmpty(selfObject.frame) && CGRectIsEmpty(firstArgv)) {
-                            return;
-                        }
-                        
-                        // Fixed: iOS 12.1 UITabBarItem positioning issue during swipe back gesture (when UINavigationBar is hidden)
-                        // https://github.com/Tencent/QMUI_iOS/issues/422
-                        if (IS_NOTCHED_SCREEN) {
-                            if ((CGRectGetHeight(selfObject.frame) == 48 && CGRectGetHeight(firstArgv) == 33) || (CGRectGetHeight(selfObject.frame) == 31 && CGRectGetHeight(firstArgv) == 20)) {
-                                return;
-                            }
-                        }
-                        
-                        // call super
-                        void (*originSelectorIMP)(id, SEL, CGRect);
-                        originSelectorIMP = (void (*)(id, SEL, CGRect))originalIMPProvider();
-                        originSelectorIMP(selfObject, originCMD, firstArgv);
-                    };
-                });
-            }
-        }
-        
-        if (@available(iOS 13.0, *)) {
-        } else {
-            Class tabBarButtonLabelClass = NSClassFromString(@"UITabBarButtonLabel");
-            
-            UITabBarItem *(^tabBarItemOfLabelBlock)(UILabel *label) = ^UITabBarItem *(UILabel *label) {
-                UIControl *tabBarButton = [label qmui_valueForKey:@"_tabBarButton"];
-                UITabBar *tabBar = [tabBarButton qmui_valueForKey:@"tabBar"];
-                __block UITabBarItem *tabBarItem = nil;
-                if (!tabBar) {
-                    return nil;
-                }
-                [tabBar.items enumerateObjectsUsingBlock:^(UITabBarItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
-                    if (obj.qmui_view == tabBarButton) {
-                        tabBarItem = obj;
-                        *stop = YES;
-                    }
-                }];
-                return tabBarItem;
-            };
-            
-            // iOS 12,如果用 UIAppearance 的方式设置了 UITabBar.appearance.unselectedItemTintColor,此时不管以 appearance 方式修改 UITabBarItem titleTextAttributes 的 NSForegroundColorAttributeName,或是直接修改 UITabBarItem 实例,均会被 unselectedItemTintColor 覆盖,所以这里做个保护
-            OverrideImplementation(tabBarButtonLabelClass, NSSelectorFromString(@"_setUnselectedTintColor:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
-                return ^(UILabel *selfObject, UIColor *firstArgv) {
-                    
-                    UITabBarItem *item = tabBarItemOfLabelBlock(selfObject);
-                    if (item) {
-                        UITabBar *tabBar = [[selfObject qmui_valueForKey:@"_tabBarButton"] qmui_valueForKey:@"tabBar"];
-                        NSDictionary<NSAttributedStringKey,id> *normalAttributes = [item titleTextAttributesForState:UIControlStateNormal] ?: [UITabBarItem.qmui_appearanceConfigured titleTextAttributesForState:UIControlStateNormal];
-                        UIColor *normalColor = normalAttributes[NSForegroundColorAttributeName];
-                        UIColor *unselectedTintColor = tabBar.unselectedItemTintColor;
-                        if (normalColor && [unselectedTintColor isEqual:firstArgv] && ![normalColor isEqual:unselectedTintColor]) {
-                            return;
-                        }
-                    }
-                    
-                    // call super
-                    void (*originSelectorIMP)(id, SEL, UIColor *);
-                    originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider();
-                    originSelectorIMP(selfObject, originCMD, firstArgv);
-                };
-            });
-            
-            // 修复系统在 iOS 12 及以下,通过 [UITabBarItem setTitleTextAttributes:forState:] 设置的 selected 字体无法生效的 bug(selected 的颜色是可以生效的)
-            OverrideImplementation(tabBarButtonLabelClass, @selector(setSelected:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
-                return ^(UILabel *selfObject, BOOL selected) {
-                    
-                    // call super
-                    void (*originSelectorIMP)(id, SEL, BOOL);
-                    originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider();
-                    originSelectorIMP(selfObject, originCMD, selected);
-                    
-                    UITabBarItem *item = tabBarItemOfLabelBlock(selfObject);
-                    if (!item) {
-                        return;
-                    }
-                    
-                    NSDictionary<NSAttributedStringKey,id> *normalAttributes = [item titleTextAttributesForState:UIControlStateNormal] ?: [UITabBarItem.qmui_appearanceConfigured titleTextAttributesForState:UIControlStateNormal];
-                    NSDictionary<NSAttributedStringKey,id> *selectedAttributes = [item titleTextAttributesForState:UIControlStateSelected] ?: [UITabBarItem.qmui_appearanceConfigured titleTextAttributesForState:UIControlStateSelected];
-                    if (normalAttributes[NSFontAttributeName] && selectedAttributes[NSFontAttributeName]) {
-                        if (selected) {
-                            selfObject.font = selectedAttributes[NSFontAttributeName];
-                        } else {
-                            selfObject.font = normalAttributes[NSFontAttributeName];
-                        }
-                        [selfObject sizeToFit];
-                        [selfObject.superview setNeedsLayout];
-                    }
-                };
-            });   
-        }
-        
-        if (@available(iOS 13.0, *)) {
-            // 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
@@ -355,76 +178,73 @@ QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouch
         
         // 以下是将 iOS 12 修改 UITabBar 样式的接口转换成用 iOS 13 的新接口去设置(因为新旧方法是互斥的,所以统一在新系统都用新方法)
         // 但这样有个风险,因为 QMUIConfiguration 配置表里都是用 appearance 的方式去设置 standardAppearance,所以如果在 UITabBar 实例被添加到 window 之前修改过旧版任意一个样式接口,就会导致一个新的 UITabBarAppearance 对象被设置给 standardAppearance 属性,这样系统就会认为你这个 UITabBar 实例自定义了 standardAppearance,那么当它被 moveToWindow 时就不会自动应用 appearance 的值了,因此需要保证在添加到 window 前不要自行修改属性
-        if (@available(iOS 13.0, *)) {
+        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;
             
-            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;
+            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;
-                    }
+            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<NSAttributedStringKey, id> *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(setTintColor:), UIColor *, ^(UITabBar *selfObject, UIColor *tintColor) {
+            syncAppearance(selfObject, nil, ^void(UITabBarItemAppearance *itemAppearance) {
+                itemAppearance.selected.iconColor = tintColor;
+                
+                NSMutableDictionary<NSAttributedStringKey, id> *textAttributes = itemAppearance.selected.titleTextAttributes.mutableCopy;
+                textAttributes[NSForegroundColorAttributeName] = tintColor;
+                itemAppearance.selected.titleTextAttributes = textAttributes.copy;
             });
-            
-            ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setBarStyle:), UIBarStyle, ^(UITabBar *selfObject, UIBarStyle barStyle) {
-                syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) {
-                    appearance.backgroundEffect = [UIBlurEffect effectWithStyle:barStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark];
-                }, nil);
+        });
+        
+        ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setBarTintColor:), UIColor *, ^(UITabBar *selfObject, UIColor *barTintColor) {
+            syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) {
+                appearance.backgroundColor = barTintColor;
+            }, nil);
+        });
+        
+        ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setUnselectedItemTintColor:), UIColor *, ^(UITabBar *selfObject, UIColor *tintColor) {
+            syncAppearance(selfObject, nil, ^void(UITabBarItemAppearance *itemAppearance) {
+                itemAppearance.normal.iconColor = tintColor;
+                
+                NSMutableDictionary *textAttributes = itemAppearance.normal.titleTextAttributes.mutableCopy;
+                textAttributes[NSForegroundColorAttributeName] = tintColor;
+                itemAppearance.normal.titleTextAttributes = textAttributes.copy;
             });
-        }
+        });
+        
+        ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setBackgroundImage:), UIImage *, ^(UITabBar *selfObject, UIImage *image) {
+            syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) {
+                appearance.backgroundImage = image;
+            }, nil);
+        });
+        
+        ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setShadowImage:), UIImage *, ^(UITabBar *selfObject, UIImage *shadowImage) {
+            syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) {
+                appearance.shadowImage = shadowImage;
+            }, nil);
+        });
+        
+        ExtendImplementationOfVoidMethodWithSingleArgument([UITabBar class], @selector(setBarStyle:), UIBarStyle, ^(UITabBar *selfObject, UIBarStyle barStyle) {
+            syncAppearance(selfObject, ^void(UITabBarAppearance *appearance) {
+                appearance.backgroundEffect = [UIBlurEffect effectWithStyle:barStyle == UIBarStyleDefault ? UIBlurEffectStyleSystemChromeMaterialLight : UIBlurEffectStyleSystemChromeMaterialDark];
+            }, nil);
+        });
     });
 }
 
-- (void)handleTabBarItemViewEvent:(UIControl *)itemView {
+- (void)qmuitb_handleTabBarItemViewEvent:(UIControl *)itemView {
     
     if (!self.canItemRespondDoubleTouch) {
         return;
@@ -436,7 +256,7 @@ QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouch
     
     // 如果一定时间后仍未触发双击,则废弃当前的点击状态
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
-        [self revertTabBarItemTouch];
+        [self qmuitb_revertTabBarItemTouch];
     });
     
     NSInteger selectedIndex = [self.items indexOfObject:self.selectedItem];
@@ -446,7 +266,7 @@ QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouch
         self.lastTouchedTabBarItemViewIndex = selectedIndex;
     } else if (self.lastTouchedTabBarItemViewIndex != selectedIndex) {
         // 后续的点击如果与第一次点击的 index 不一致,则认为是重新开始一次新的点击
-        [self revertTabBarItemTouch];
+        [self qmuitb_revertTabBarItemTouch];
         self.lastTouchedTabBarItemViewIndex = selectedIndex;
         return;
     }
@@ -458,11 +278,11 @@ QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouch
         if (item.qmui_doubleTapBlock) {
             item.qmui_doubleTapBlock(item, selectedIndex);
         }
-        [self revertTabBarItemTouch];
+        [self qmuitb_revertTabBarItemTouch];
     }
 }
 
-- (void)revertTabBarItemTouch {
+- (void)qmuitb_revertTabBarItemTouch {
     self.lastTouchedTabBarItemViewIndex = kLastTouchedTabBarItemIndexNone;
     self.tabBarItemViewTouchCount = 0;
 }
@@ -472,11 +292,9 @@ QMUISynthesizeNSIntegerProperty(tabBarItemViewTouchCount, setTabBarItemViewTouch
 @implementation UITabBarAppearance (QMUI)
 
 - (void)qmui_applyItemAppearanceWithBlock:(void (^)(UITabBarItemAppearance * _Nonnull))block {
-    if (@available(iOS 13.0, *)) {
-        block(self.stackedLayoutAppearance);
-        block(self.inlineLayoutAppearance);
-        block(self.compactInlineLayoutAppearance);
-    }
+    block(self.stackedLayoutAppearance);
+    block(self.inlineLayoutAppearance);
+    block(self.compactInlineLayoutAppearance);
 }
 
 @end

+ 2 - 2
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.h

@@ -21,8 +21,8 @@ NS_ASSUME_NONNULL_BEGIN
 
 /**
  *  双击 tabBarItem 时的回调,默认为 nil。
- *  @arg tabBarItem 被双击的 UITabBarItem
- *  @arg index      被双击的 UITabBarItem 的序号
+ *  @param tabBarItem 被双击的 UITabBarItem,若需要拿到当前的 view 则通过 qmui_view 获取。
+ *  @param index 被双击的 UITabBarItem 的序号
  */
 @property(nonatomic, copy, nullable) void (^qmui_doubleTapBlock)(UITabBarItem *tabBarItem, NSInteger index);
 

+ 1 - 4
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITabBarItem+QMUI.m

@@ -30,10 +30,7 @@ QMUISynthesizeIdCopyProperty(qmui_doubleTapBlock, setQmui_doubleTapBlock)
     if (!tabBarButton) {
         return nil;
     }
-    if (@available(iOS 13.0, *)) {
-        return [tabBarButton qmui_valueForKey:@"_imageView"];
-    }
-    return [tabBarButton qmui_valueForKey:@"_info"];
+    return [tabBarButton qmui_valueForKey:@"_imageView"];
 }
 
 @end

+ 5 - 19
Pods/QMUIKit/QMUIKit/UIKitExtensions/UITableView+QMUI.h

@@ -18,7 +18,7 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
-#define PreferredValueForTableViewStyle(_style, _plain, _grouped, _insetGrouped) (_style == UITableViewStyleGrouped ? _grouped : (_style == QMUITableViewStyleInsetGrouped ? _insetGrouped : _plain))
+#define PreferredValueForTableViewStyle(_style, _plain, _grouped, _insetGrouped) (_style == UITableViewStyleGrouped ? _grouped : (_style == UITableViewStyleInsetGrouped ? _insetGrouped : _plain))
 
 /// cell 在当前 section 里的位置,注意判断时要用 (var & xxx) == xxx 的方式
 typedef NS_OPTIONS(NSInteger, QMUITableViewCellPosition) {
@@ -126,35 +126,21 @@ typedef NS_OPTIONS(NSInteger, QMUITableViewCellPosition) {
 
 @end
 
-
-extern const UITableViewStyle QMUITableViewStyleInsetGrouped;
-
 /**
  系统在 iOS 13 新增了 UITableViewStyleInsetGrouped 类型用于展示往内缩进、cell 带圆角的列表,而这个 Category 让 iOS 12 及以下的系统也能支持这种样式,iOS 13 也可以通过这个 Category 修改左右的缩进值和 cell 的圆角。
  使用方式:
- 对于 UITableView,通过 -[UITableView initWithStyle:QMUITableViewStyleInsetGrouped] 初始化 tableView。
- 对于 UITableViewController,通过 -[UITableViewController initWithStyle:QMUITableViewStyleInsetGrouped] 初始化 tableViewController。
+ 对于 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)
 
-/**
- 对于代码的使用场景,通过这个属性可以获取当前 UITableView 的 style(如果当前 tableView 没有使用 InsetGrouped 则可以忽略这个属性的存在)。
- 对于 Interface Builder 的使用场景,如果你的 App 最低版本从 iOS 13 开始,则直接用系统自带的 style 选项框去修改 style 即可,但如果你的 App
- 最低版本包含 iOS 12 及以下,则需要在 Interface Builder 里把 qmui_style 修改为“2”来使用 QMUITableViewStyleInsetGrouped(选中 TableView 节点后在“User Defined Runtime Attributes”里添加名为“qmui_style”,类型为“Number”,值为“2”的条目)。
- */
-#if TARGET_INTERFACE_BUILDER
-@property(nonatomic, assign, readwrite) IBInspectable UITableViewStyle qmui_style;
-#else
-@property(nonatomic, assign, readonly) UITableViewStyle qmui_style;
-#endif
-
-/// 当使用 QMUITableViewStyleInsetGrouped 时可通过这个属性修改 cell 的圆角值,默认值为 10,也即 iOS 13 系统默认表现。如果要为不同 indexPath 指定不同圆角值,可在 -[UITableViewDelegate tableView:willDisplayCell:forRowAtIndexPath:] 内修改 cell.layer.cornerRadius 的值。
+/// 当使用 UITableViewStyleInsetGrouped 时可通过这个属性修改 cell 的圆角值,默认值为 10,也即 iOS 13 系统默认表现。如果要为不同 indexPath 指定不同圆角值,可在 -[UITableViewDelegate tableView:willDisplayCell:forRowAtIndexPath:] 内修改 cell.layer.cornerRadius 的值。
 @property(nonatomic, assign) CGFloat qmui_insetGroupedCornerRadius UI_APPEARANCE_SELECTOR;
 
-/// 当使用 QMUITableViewStyleInsetGrouped 时可通过这个属性修改列表的左右缩进值,默认值为 20,也即 iOS 13 系统默认表现。
+/// 当使用 UITableViewStyleInsetGrouped 时可通过这个属性修改列表的左右缩进值,默认值为 20,也即 iOS 13 系统默认表现。
 @property(nonatomic, assign) CGFloat qmui_insetGroupedHorizontalInset UI_APPEARANCE_SELECTOR;
 
 @end

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä