Skip to content

Commit

Permalink
兼容 iPad 分屏
Browse files Browse the repository at this point in the history
  • Loading branch information
MoLice committed Jun 25, 2019
1 parent b63df1d commit 12a7550
Show file tree
Hide file tree
Showing 12 changed files with 416 additions and 17 deletions.
1 change: 1 addition & 0 deletions QMUIConfigurationTemplate/QMUIConfigurationTemplate.m
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ - (void)applyConfigurationTemplate {
QMUICMI.shouldFixTabBarButtonBugForAll = NO; // ShouldFixTabBarButtonBugForAll : 是否要对 iOS 12.1.1 及以后的版本也修复手势返回时 tabBarButton 布局错误的 bug(issue #410),默认为 NO
QMUICMI.shouldPrintQMUIWarnLogToConsole = IS_DEBUG; // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上
QMUICMI.sendAnalyticsToQMUITeam = YES; // SendAnalyticsToQMUITeam : 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用
QMUICMI.dynamicPreferredValueForIPad = NO; // 当 iPad 处于 Slide Over 或 Split View 分屏模式下,宏 `PreferredValueForXXX` 是否把 iPad 视为某种屏幕宽度近似的 iPhone 来取值。
QMUICMI.ignoreKVCAccessProhibited = NO; // IgnoreKVCAccessProhibited : 是否全局忽略 iOS 13 对 KVC 访问 UIKit 私有属性的限制
}

Expand Down
4 changes: 4 additions & 0 deletions QMUIKit.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Pod::Spec.new do |s|
sss.dependency 'QMUIKit/QMUIComponents/QMUIMultipleDelegates'
end

ss.subspec 'QMUIAnimation' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUIAnimation'
end

ss.subspec 'QMUINavigationTitleView' do |sss|
sss.source_files = 'QMUIKit/QMUIComponents/QMUINavigationTitleView.{h,m}'
sss.dependency 'QMUIKit/QMUIComponents/QMUIButton'
Expand Down
48 changes: 48 additions & 0 deletions QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// QMUIWindowSizeMonitor.h
// qmuidemo
//
// Created by ziezheng on 2019/5/27.
// Copyright © 2019 QMUI Team. All rights reserved.
//

#import <UIKit/UIkit.h>
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@protocol QMUIWindowSizeMonitorProtocol <NSObject>

@optional

/**
当继承自 UIResponder 的对象,比如 UIView 或 UIViewController 实现了这个方法时,其所属的 window 在大小发生改变后在这个方法回调。
@note 类似系统的 [-viewWillTransitionToSize:withTransitionCoordinator:],但是系统这个方法回调时 window 的大小实际上还未发生改变,如果你需要在 window 大小发生之后且在 layout 之前来处理一些逻辑时,可以放到这个方法去实现。
@param size 所属窗口的新大小
*/

- (void)windowDidTransitionToSize:(CGSize)size;

@end

typedef void (^QMUIWindowSizeObserverHandler)(CGSize newWindowSize);

@interface NSObject (QMUIWindowSizeMonitor)

/**
为当前对象添加主窗口 (UIApplication Delegate Window)的大小变化的监听,同一对象可重复添加多个监听,当对象销毁时监听自动失效。
@param handler 窗口大小发生改变时的回调
*/
- (void)qmui_addSizeObserverForMainWindow:(QMUIWindowSizeObserverHandler)handler;
/**
为当前对象添加指定窗口的大小变化监听,同一对象可重复添加多个监听,当对象销毁时监听自动失效。
@param window 要监听的窗口
@param handler 窗口大小发生改变时的回调
*/
- (void)qmui_addSizeObserverForWindow:(UIWindow *)window handler:(QMUIWindowSizeObserverHandler)handler;

@end

NS_ASSUME_NONNULL_END
189 changes: 189 additions & 0 deletions QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//
// QMUIWindowSizeMonitor.m
// qmuidemo
//
// Created by ziezheng on 2019/5/27.
// Copyright © 2019 QMUI Team. All rights reserved.
//

#import "QMUIWindowSizeMonitor.h"
#import "QMUICore.h"
#import "NSPointerArray+QMUI.h"

@interface NSObject (QMUIWindowSizeMonitor_Private)

@property(nonatomic, readonly) NSMutableArray <QMUIWindowSizeObserverHandler> *qwsm_windowSizeChangeHandlers;

@end

@interface UIResponder (QMUIWindowSizeMonitor_Private)

@property(nonatomic, weak) UIWindow *qwsm_previousWindow;

@end


@interface UIWindow (QMUIWindowSizeMonitor_Private)

@property(nonatomic, assign) CGSize qwsm_previousSzie;
@property(nonatomic, readonly) NSPointerArray *qwsm_sizeObservers;
@property(nonatomic, readonly) NSPointerArray *qwsm_canReceiveWindowDidTransitionToSizeResponders;

- (void)qwsm_addSizeObserver:(NSObject *)observer;

@end



@implementation NSObject (QMUIWindowSizeMonitor)

- (void)qmui_addSizeObserverForMainWindow:(QMUIWindowSizeObserverHandler)handler {
[self qmui_addSizeObserverForWindow:[UIApplication sharedApplication].delegate.window handler:handler];
}

- (void)qmui_addSizeObserverForWindow:(UIWindow *)window handler:(QMUIWindowSizeObserverHandler)handler {
NSAssert(window != nil, @"window is nil!");

struct Block_literal {
void *isa;
int flags;
int reserved;
void (*__FuncPtr)(void *, ...);
};

void * blockFuncPtr = ((__bridge struct Block_literal *)handler)->__FuncPtr;
for (QMUIWindowSizeObserverHandler handler in self.qwsm_windowSizeChangeHandlers) {
// 由于利用 block 的 __FuncPtr 指针来判断同一个实现的 block 过滤掉,防止重复添加监听
if (((__bridge struct Block_literal *)handler)->__FuncPtr == blockFuncPtr) {
return;
}
}

[self.qwsm_windowSizeChangeHandlers addObject:handler];
[window qwsm_addSizeObserver:self];
}

- (NSMutableArray<QMUIWindowSizeObserverHandler> *)qwsm_windowSizeChangeHandlers {
NSMutableArray *_handlers = objc_getAssociatedObject(self, _cmd);
if (!_handlers) {
_handlers = [NSMutableArray array];
objc_setAssociatedObject(self, _cmd, _handlers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return _handlers;
}

@end

@implementation UIWindow (QMUIWindowSizeMonitor)

QMUISynthesizeCGSizeProperty(qwsm_previousSzie, setQwsm_previousSzie)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
OverrideImplementation([UIWindow class], @selector(layoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^void(UIWindow *selfObject) {

// call super
void (*originSelectorIMP)(id, SEL);
originSelectorIMP = (void (*)(id, SEL))originalIMPProvider();
originSelectorIMP(selfObject, originCMD);

CGSize newSize = selfObject.bounds.size;
if (!CGSizeEqualToSize(newSize, selfObject.qwsm_previousSzie)) {
if (!CGSizeEqualToSize(selfObject.qwsm_previousSzie, CGSizeZero)) {
NSLog(@"%@ :change size from %@ to %@",NSStringFromClass(selfObject.class), NSStringFromCGSize(selfObject.qwsm_previousSzie),NSStringFromCGSize(newSize));
[selfObject qwsm_notifyObserversWithNewSize:newSize];
}
selfObject.qwsm_previousSzie = selfObject.bounds.size;

}

};
});

OverrideImplementation([UIView class], @selector(willMoveToWindow:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) {
return ^void(UIView *selfObject, UIWindow *newWindow) {

// call super
void (*originSelectorIMP)(id, SEL, UIWindow *);
originSelectorIMP = (void (*)(id, SEL, UIWindow *))originalIMPProvider();
originSelectorIMP(selfObject, originCMD, newWindow);

if (newWindow) {
if ([selfObject respondsToSelector:@selector(windowDidTransitionToSize:)]) {
[newWindow qwsm_addDidTransitionToSizeMethodReceiver:selfObject];
}
UIResponder *nextResponder = [selfObject nextResponder];
if ([nextResponder isKindOfClass:[UIViewController class]] && [nextResponder respondsToSelector:@selector(windowDidTransitionToSize:)]) {
[newWindow qwsm_addDidTransitionToSizeMethodReceiver:nextResponder];
}

}

};
});
});
}

- (void)qwsm_notifyObserversWithNewSize:(CGSize)newSize {
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);
}
}

for (NSUInteger i = 0, count = self.qwsm_canReceiveWindowDidTransitionToSizeResponders.count; i < count; i++) {
UIResponder <QMUIWindowSizeMonitorProtocol>*responder = [self.qwsm_canReceiveWindowDidTransitionToSizeResponders pointerAtIndex:i];
[responder windowDidTransitionToSize:self.bounds.size];
}
}

- (void)qwsm_removeSizeObserver:(NSObject *)observer {
NSUInteger index = [self.qwsm_sizeObservers qmui_indexOfPointer:(__bridge void *)observer];
if (index != NSNotFound) {
[self.qwsm_sizeObservers removePointerAtIndex:index];
}
}

- (void)qwsm_addDidTransitionToSizeMethodReceiver:(UIResponder *)receiver {
if ([self.qwsm_canReceiveWindowDidTransitionToSizeResponders qmui_containsPointer:(__bridge void *)(receiver)]) return;
if (receiver.qwsm_previousWindow && receiver.qwsm_previousWindow != self) {
[receiver.qwsm_previousWindow qwsm_removeSizeObserver:receiver];
}
receiver.qwsm_previousWindow = self;
[self.qwsm_canReceiveWindowDidTransitionToSizeResponders addPointer:(__bridge void *)(receiver)];
}

- (void)qwsm_addSizeObserver:(NSObject *)observer {
if ([self.qwsm_sizeObservers qmui_containsPointer:(__bridge void *)(observer)]) return;
[self.qwsm_sizeObservers addPointer:(__bridge void *)(observer)];
}

- (NSPointerArray *)qwsm_sizeObservers {
NSPointerArray *qwsm_sizeObservers = objc_getAssociatedObject(self, _cmd);
if (!qwsm_sizeObservers) {
qwsm_sizeObservers = [NSPointerArray weakObjectsPointerArray];
objc_setAssociatedObject(self, _cmd, qwsm_sizeObservers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return qwsm_sizeObservers;
}

- (NSPointerArray *)qwsm_canReceiveWindowDidTransitionToSizeResponders {
NSPointerArray *qwsm_responders = objc_getAssociatedObject(self, _cmd);
if (!qwsm_responders) {
qwsm_responders = [NSPointerArray weakObjectsPointerArray];
objc_setAssociatedObject(self, _cmd, qwsm_responders, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return qwsm_responders;
}

@end

@implementation UIResponder (QMUIWindowSizeMonitor)

QMUISynthesizeIdWeakProperty(qwsm_previousWindow, setQwsm_previousWindow)

@end
54 changes: 45 additions & 9 deletions QMUIKit/QMUICore/QMUICommonDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
#define IOS12_SDK_ALLOWED YES
#endif

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
/// 当前编译使用的 Base SDK 版本为 iOS 13.0 及以上
#define IOS13_SDK_ALLOWED YES
#endif

#pragma mark - Clang

#define ArgumentToString(macro) #macro
Expand All @@ -66,12 +71,11 @@
#define BeginIgnoreDeprecatedWarning BeginIgnoreClangWarning(-Wdeprecated-declarations)
#define EndIgnoreDeprecatedWarning EndIgnoreClangWarning

#pragma mark - 忽略 iOS13 KVC 访问私有属性限制
#pragma mark - 忽略 iOS 13 KVC 访问私有属性限制

#define QMUIIgnoreUIKVCAccessProhibitedKey @"QMUIIgnoreUIKVCAccessProhibitedKey"
/// 将 KVC 代码包裹在这个宏中,可忽略系统的 KVC 访问限制
#define BeginIgnoreUIKVCAccessProhibited [[NSThread currentThread] qmui_bindBOOL:YES forKey:QMUIIgnoreUIKVCAccessProhibitedKey];
#define EndIgnoreUIKVCAccessProhibited [[NSThread currentThread] qmui_bindBOOL:NO forKey:QMUIIgnoreUIKVCAccessProhibitedKey];
#define BeginIgnoreUIKVCAccessProhibited if (@available(iOS 13.0, *)) NSThread.currentThread.qmui_shouldIgnoreUIKVCAccessProhibited = YES;
#define EndIgnoreUIKVCAccessProhibited if (@available(iOS 13.0, *)) NSThread.currentThread.qmui_shouldIgnoreUIKVCAccessProhibited = NO;

#pragma mark - 变量-设备相关

Expand Down Expand Up @@ -105,6 +109,12 @@
/// 屏幕高度,跟横竖屏无关
#define DEVICE_HEIGHT (IS_LANDSCAPE ? [[UIScreen mainScreen] bounds].size.width : [[UIScreen mainScreen] bounds].size.height)

/// 在 iPad 分屏模式下等于 app 实际运行宽度,否则等同于 SCREEN_WIDTH
#define APPLICATION_WIDTH [QMUIHelper applicationSize].width

/// 在 iPad 分屏模式下等于 app 实际运行宽度,否则等同于 DEVICE_HEIGHT
#define APPLICATION_HEIGHT [QMUIHelper applicationSize].height

/// 是否全面屏设备
#define IS_NOTCHED_SCREEN [QMUIHelper isNotchedScreen]
/// iPhone XS Max
Expand Down Expand Up @@ -142,10 +152,10 @@
#define ScreenNativeScale ([[UIScreen mainScreen] nativeScale])

/// toolBar相关frame
#define ToolBarHeight (IS_IPAD ? (IS_NOTCHED_SCREEN ? 65 : 50) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44) + PreferredValueForNotchedDevice(39, 0))
#define ToolBarHeight (IS_IPAD ? (IS_NOTCHED_SCREEN ? 70 : (IOS_VERSION >= 12.0 ? 50 : 44)) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44) + SafeAreaInsetsConstantForDeviceWithNotch.bottom)

/// tabBar相关frame
#define TabBarHeight (IS_IPAD ? (IS_NOTCHED_SCREEN ? 65 : 50) : PreferredValueForNotchedDevice(IS_LANDSCAPE ? 32 : 49, 49) + SafeAreaInsetsConstantForDeviceWithNotch.bottom)
#define TabBarHeight (IS_IPAD ? (IS_NOTCHED_SCREEN ? 65 : (IOS_VERSION >= 12.0 ? 50 : 49)) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(49, 32) : 49) + SafeAreaInsetsConstantForDeviceWithNotch.bottom)

/// 状态栏高度(来电等情况下,状态栏高度会发生变化,所以应该实时计算)
#define StatusBarHeight ([UIApplication sharedApplication].statusBarHidden ? 0 : [[UIApplication sharedApplication] statusBarFrame].size.height)
Expand All @@ -154,7 +164,7 @@
#define StatusBarHeightConstant ([UIApplication sharedApplication].statusBarHidden ? (IS_IPAD ? (IS_NOTCHED_SCREEN ? 24 : 20) : PreferredValueForNotchedDevice(IS_LANDSCAPE ? 0 : 44, 20)) : [[UIApplication sharedApplication] statusBarFrame].size.height)

/// navigationBar 的静态高度
#define NavigationBarHeight (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44)
#define NavigationBarHeight (IS_IPAD ? (IOS_VERSION >= 12.0 ? 50 : 44) : (IS_LANDSCAPE ? PreferredValueForVisualDevice(44, 32) : 44))

/// 代表(导航栏+状态栏),这里用于获取其高度
/// @warn 如果是用于 viewController,请使用 UIViewController(QMUI) qmui_navigationBarMaxYInViewCoordinator 代替
Expand All @@ -179,8 +189,34 @@
/// 将所有屏幕按照宽松/紧凑分类,其中 iPad、iPhone XS Max/XR/Plus 均为宽松屏幕,但开启了放大模式的设备均会视为紧凑屏幕
#define PreferredValueForVisualDevice(_regular, _compact) ([QMUIHelper isRegularScreen] ? _regular : _compact)

/// 区分全部的设备类型
#define PreferredValueForAll(_iPad, _65inch, _61inch, _58inch, _55inch, _47inch, _40inch, _35inch) (IS_IPAD ? _iPad : (IS_35INCH_SCREEN ? _35inch : (IS_40INCH_SCREEN ? _40inch : ((IS_47INCH_SCREEN || (IS_55INCH_SCREEN && IS_ZOOMEDMODE)) ? _47inch : (IS_55INCH_SCREEN ? _55inch : ((IS_58INCH_SCREEN || ((IS_61INCH_SCREEN || IS_65INCH_SCREEN) && IS_ZOOMEDMODE)) ? _58inch : (IS_61INCH_SCREEN ? _61inch : _65inch)))))))
/// 判断当前是否是处于分屏模式的 iPad
#define IS_SPLIT_SCREEN_IPAD (IS_IPAD && APPLICATION_WIDTH != SCREEN_WIDTH)

/// 若 iPad 处于分屏模式下,返回 iPad 接近 iPhone 宽度(320、375、414)中近似的一种,方便屏幕适配。
#define IPAD_SIMILAR_SCREEN_WIDTH [QMUIHelper preferredLayoutAsSimilarScreenWidthForIPad]

#define _40INCH_WIDTH [QMUIHelper screenSizeFor40Inch].width
#define _58INCH_WIDTH [QMUIHelper screenSizeFor58Inch].width
#define _65INCH_WIDTH [QMUIHelper screenSizeFor65Inch].width

#define AS_IPAD (DynamicPreferredValueForIPad ? ((IS_IPAD && !IS_SPLIT_SCREEN_IPAD) || (IS_SPLIT_SCREEN_IPAD && APPLICATION_WIDTH >= 768)) : IS_IPAD)
#define AS_65INCH_SCREEN (IS_65INCH_SCREEN || (DynamicPreferredValueForIPad && IPAD_SIMILAR_SCREEN_WIDTH == _65INCH_WIDTH))
#define AS_61INCH_SCREEN IS_61INCH_SCREEN
#define AS_58INCH_SCREEN (IS_58INCH_SCREEN || ((AS_61INCH_SCREEN || AS_65INCH_SCREEN) && IS_ZOOMEDMODE) || (DynamicPreferredValueForIPad && IPAD_SIMILAR_SCREEN_WIDTH == _58INCH_WIDTH))
#define AS_55INCH_SCREEN IS_55INCH_SCREEN
#define AS_47INCH_SCREEN (IS_47INCH_SCREEN || (IS_55INCH_SCREEN && IS_ZOOMEDMODE))
#define AS_40INCH_SCREEN (IS_40INCH_SCREEN || (DynamicPreferredValueForIPad && IPAD_SIMILAR_SCREEN_WIDTH == _40INCH_WIDTH))
#define AS_35INCH_SCREEN IS_35INCH_SCREEN
#define AS_320WIDTH_SCREEN IS_320WIDTH_SCREEN

#define PreferredValueForAll(_iPad, _65inch, _61inch, _58inch, _55inch, _47inch, _40inch, _35inch) \
(AS_IPAD ? _iPad :\
(AS_35INCH_SCREEN ? _35inch :\
(AS_40INCH_SCREEN ? _40inch :\
(AS_47INCH_SCREEN ? _47inch :\
(AS_55INCH_SCREEN ? _55inch :\
(AS_58INCH_SCREEN ? _58inch :\
(AS_61INCH_SCREEN ? _61inch : _65inch)))))))

#pragma mark - 方法-创建器

Expand Down
1 change: 1 addition & 0 deletions QMUIKit/QMUICore/QMUIConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, assign) BOOL shouldFixTabBarButtonBugForAll;
@property(nonatomic, assign) BOOL shouldPrintQMUIWarnLogToConsole;
@property(nonatomic, assign) BOOL sendAnalyticsToQMUITeam;
@property(nonatomic, assign) BOOL dynamicPreferredValueForIPad;
@property(nonatomic, assign) BOOL ignoreKVCAccessProhibited;

NS_ASSUME_NONNULL_END
Expand Down
1 change: 1 addition & 0 deletions QMUIKit/QMUICore/QMUIConfigurationMacros.h
Original file line number Diff line number Diff line change
Expand Up @@ -234,5 +234,6 @@
#define ShouldFixTabBarButtonBugForAll [QMUICMI shouldFixTabBarButtonBugForAll] // 是否要对 iOS 12.1.2 及以后的版本也修复手势返回时 tabBarButton 布局错误的 bug(issue #410),默认为 NO
#define ShouldPrintQMUIWarnLogToConsole [QMUICMI shouldPrintQMUIWarnLogToConsole] // 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上
#define SendAnalyticsToQMUITeam [QMUICMI sendAnalyticsToQMUITeam] // 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用
#define DynamicPreferredValueForIPad [QMUICMI dynamicPreferredValueForIPad] // 当 iPad 处于 Slide Over 或 Split View 分屏模式下,宏 `PreferredValueForXXX` 是否把 iPad 视为某种屏幕宽度近似的 iPhone 来取值。
#define IgnoreKVCAccessProhibited [QMUICMI ignoreKVCAccessProhibited] // 是否全局忽略 iOS 13 对 KVC 访问 UIKit 私有属性的限制

6 changes: 6 additions & 0 deletions QMUIKit/QMUICore/QMUIHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ extern NSString *const _Nonnull QMUIResourcesMainBundleName;
/// 系统设置里是否开启了“放大显示-试图-放大”,支持放大模式的 iPhone 设备可在官方文档中查询 https://support.apple.com/zh-cn/guide/iphone/iphd6804774e/ios
+ (BOOL)isZoomedMode;

/**
在 iPad 分屏模式下可获得实际运行区域的窗口大小,如需适配 iPad 分屏,建议用这个方法来代替 [UIScreen mainScreen].bounds.size
@return 应用运行的窗口大小
*/
+ (CGSize)applicationSize;

@end

@interface QMUIHelper (UIApplication)
Expand Down
Loading

0 comments on commit 12a7550

Please sign in to comment.