session-ios/SessionMessagingKit/Utilities/OWSWindowManager.m

548 lines
17 KiB
Mathematica
Raw Permalink Normal View History

//
2019-01-08 17:47:40 +01:00
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSWindowManager.h"
2020-11-26 00:37:56 +01:00
#import "Environment.h"
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
NS_ASSUME_NONNULL_BEGIN
NSString *const OWSWindowManagerCallDidChangeNotification = @"OWSWindowManagerCallDidChangeNotification";
NSString *const IsScreenBlockActiveDidChangeNotification = @"IsScreenBlockActiveDidChangeNotification";
2018-07-17 21:40:54 +02:00
const CGFloat OWSWindowManagerCallBannerHeight(void)
{
if (@available(iOS 11.4, *)) {
return CurrentAppContext().statusBarHeight + 20;
}
if (![UIDevice currentDevice].hasIPhoneXNotch) {
return CurrentAppContext().statusBarHeight + 20;
}
// Hardcode CallBanner height for iPhone X's on older iOS.
//
// As of iOS11.4 and iOS12, this no longer seems to be an issue, but previously statusBarHeight returned
// something like 20pts (IIRC), meaning our call banner did not extend sufficiently past the iPhone X notch.
//
// Before noticing that this behavior changed, I actually assumed that notch height was intentionally excluded from
// the statusBarHeight, and that this was not a bug, else I'd have taken better notes.
return 64;
}
2018-04-18 18:48:31 +02:00
// Behind everything, especially the root window.
const UIWindowLevel UIWindowLevel_Background = -1.f;
2018-04-18 18:48:31 +02:00
const UIWindowLevel UIWindowLevel_ReturnToCall(void);
const UIWindowLevel UIWindowLevel_ReturnToCall(void)
{
return UIWindowLevelStatusBar - 1;
}
2018-05-16 16:14:11 +02:00
2018-04-18 18:48:31 +02:00
// In front of the root window, behind the screen blocking window.
const UIWindowLevel UIWindowLevel_CallView(void);
const UIWindowLevel UIWindowLevel_CallView(void)
{
2018-04-18 18:48:31 +02:00
return UIWindowLevelNormal + 1.f;
}
2018-05-16 16:14:11 +02:00
// In front of the status bar and CallView
const UIWindowLevel UIWindowLevel_ScreenBlocking(void);
const UIWindowLevel UIWindowLevel_ScreenBlocking(void)
{
2018-04-18 18:48:31 +02:00
return UIWindowLevelStatusBar + 2.f;
}
// In front of everything
2018-07-10 23:34:22 +02:00
const UIWindowLevel UIWindowLevel_MessageActions(void);
const UIWindowLevel UIWindowLevel_MessageActions(void)
{
// Note: To cover the keyboard, this is higher than the ScreenBlocking level,
// but this window is hidden when screen protection is shown.
return CGFLOAT_MAX - 100;
2018-07-10 23:34:22 +02:00
}
2019-01-08 17:47:40 +01:00
#pragma mark -
2018-07-12 16:58:20 +02:00
@interface MessageActionsWindow : UIWindow
@end
2019-01-08 17:47:40 +01:00
#pragma mark -
2018-07-12 16:58:20 +02:00
@implementation MessageActionsWindow
- (UIWindowLevel)windowLevel
{
// As of iOS11, setWindowLevel clamps the value below
// the height of the keyboard window.
// Because we want to display above the keyboard, we hardcode
// the `windowLevel` getter.
return UIWindowLevel_MessageActions();
}
@end
2019-01-08 17:47:40 +01:00
#pragma mark -
@implementation OWSWindowRootViewController
- (BOOL)canBecomeFirstResponder
{
return YES;
}
2018-10-25 19:02:30 +02:00
#pragma mark - Orientation
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskAllButUpsideDown;
2018-10-25 19:02:30 +02:00
}
@end
2019-01-08 17:47:40 +01:00
#pragma mark -
@interface OWSWindowRootNavigationViewController : UINavigationController
@end
2019-01-08 17:47:40 +01:00
#pragma mark -
@implementation OWSWindowRootNavigationViewController : UINavigationController
#pragma mark - Orientation
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskAllButUpsideDown;
}
@end
#pragma mark -
2020-11-16 00:34:47 +01:00
@interface OWSWindowManager ()
// UIWindowLevelNormal
@property (nonatomic) UIWindow *rootWindow;
// UIWindowLevel_CallView
@property (nonatomic) UIWindow *callViewWindow;
@property (nonatomic) UINavigationController *callNavigationController;
2018-07-10 23:34:22 +02:00
// UIWindowLevel_MessageActions
@property (nonatomic) UIWindow *menuActionsWindow;
@property (nonatomic, nullable) UIViewController *menuActionsViewController;
2018-07-10 23:34:22 +02:00
// UIWindowLevel_Background if inactive,
// UIWindowLevel_ScreenBlocking() if active.
@property (nonatomic) UIWindow *screenBlockingWindow;
2018-05-16 16:23:23 +02:00
@property (nonatomic) BOOL shouldShowCallView;
@property (nonatomic, nullable) UIViewController *callViewController;
@end
#pragma mark -
@implementation OWSWindowManager
+ (instancetype)sharedManager
{
2018-10-12 22:39:40 +02:00
return Environment.shared.windowManager;
}
- (instancetype)initDefault
{
self = [super init];
if (!self) {
return self;
}
return self;
}
- (void)setupWithRootWindow:(UIWindow *)rootWindow screenBlockingWindow:(UIWindow *)screenBlockingWindow
{
self.rootWindow = rootWindow;
self.screenBlockingWindow = screenBlockingWindow;
2018-04-18 18:48:31 +02:00
self.callViewWindow = [self createCallViewWindow:rootWindow];
self.menuActionsWindow = [self createMenuActionsWindowWithRoowWindow:rootWindow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didChangeStatusBarFrame:)
name:UIApplicationDidChangeStatusBarFrameNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActive:)
name:OWSApplicationWillResignActiveNotification
object:nil];
[self ensureWindowState];
}
- (void)didChangeStatusBarFrame:(NSNotification *)notification
{
2020-11-16 00:34:47 +01:00
}
- (void)applicationWillResignActive:(NSNotification *)notification
{
[self hideMenuActionsWindow];
}
- (UIWindow *)createMenuActionsWindowWithRoowWindow:(UIWindow *)rootWindow
2018-07-10 23:34:22 +02:00
{
2018-07-12 16:58:20 +02:00
UIWindow *window;
if (@available(iOS 11, *)) {
// On iOS11, setting the windowLevel is insufficient, so we override
// the `windowLevel` getter.
window = [[MessageActionsWindow alloc] initWithFrame:rootWindow.bounds];
} else {
// On iOS9, 10 overriding the `windowLevel` getter does not cause the
// window to be displayed above the keyboard, but setting the window
// level works.
window = [[UIWindow alloc] initWithFrame:rootWindow.bounds];
window.windowLevel = UIWindowLevel_MessageActions();
}
2018-07-10 23:34:22 +02:00
window.hidden = YES;
window.backgroundColor = UIColor.clearColor;
return window;
}
2018-04-18 18:48:31 +02:00
- (UIWindow *)createCallViewWindow:(UIWindow *)rootWindow
{
UIWindow *window = [[UIWindow alloc] initWithFrame:rootWindow.bounds];
window.hidden = YES;
window.windowLevel = UIWindowLevel_CallView();
window.opaque = YES;
2018-04-18 18:48:31 +02:00
// TODO: What's the right color to use here?
2020-11-26 00:37:56 +01:00
window.backgroundColor = [UIColor blackColor];
UIViewController *viewController = [OWSWindowRootViewController new];
2020-11-26 00:37:56 +01:00
viewController.view.backgroundColor = [UIColor blackColor];
// NOTE: Do not use OWSNavigationController for call window.
2018-05-30 18:35:55 +02:00
// It adjusts the size of the navigation bar to reflect the
// call window. We don't want those adjustments made within
// the call window itself.
OWSWindowRootNavigationViewController *navigationController =
[[OWSWindowRootNavigationViewController alloc] initWithRootViewController:viewController];
navigationController.navigationBarHidden = YES;
2018-04-18 18:48:31 +02:00
self.callNavigationController = navigationController;
window.rootViewController = navigationController;
return window;
}
- (void)setIsScreenBlockActive:(BOOL)isScreenBlockActive
{
_isScreenBlockActive = isScreenBlockActive;
[self ensureWindowState];
[[NSNotificationCenter defaultCenter] postNotificationName:IsScreenBlockActiveDidChangeNotification
object:nil
userInfo:nil];
}
2019-01-09 20:32:52 +01:00
- (BOOL)isAppWindow:(UIWindow *)window
{
2020-11-16 00:34:47 +01:00
return (window == self.rootWindow || window == self.callViewWindow
2019-01-09 20:32:52 +01:00
|| window == self.menuActionsWindow || window == self.screenBlockingWindow);
}
#pragma mark - Message Actions
- (BOOL)isPresentingMenuActions
{
return self.menuActionsViewController != nil;
}
- (void)showMenuActionsWindow:(UIViewController *)menuActionsViewController
2018-07-10 23:34:22 +02:00
{
self.menuActionsViewController = menuActionsViewController;
self.menuActionsWindow.rootViewController = menuActionsViewController;
2018-07-10 23:34:22 +02:00
[self ensureWindowState];
}
- (void)hideMenuActionsWindow
2018-07-10 23:34:22 +02:00
{
self.menuActionsWindow.rootViewController = nil;
self.menuActionsViewController = nil;
2018-07-10 23:34:22 +02:00
[self ensureWindowState];
}
#pragma mark - Calls
- (void)setCallViewController:(nullable UIViewController *)callViewController
{
if (callViewController == _callViewController) {
return;
}
_callViewController = callViewController;
[NSNotificationCenter.defaultCenter postNotificationName:OWSWindowManagerCallDidChangeNotification object:nil];
}
- (void)startCall:(UIViewController *)callViewController
{
self.callViewController = callViewController;
2018-04-18 19:33:29 +02:00
// Attach callViewController to window.
[self.callNavigationController popToRootViewControllerAnimated:NO];
2018-04-18 18:48:31 +02:00
[self.callNavigationController pushViewController:callViewController animated:NO];
2018-05-16 16:23:23 +02:00
self.shouldShowCallView = YES;
// CallViewController only supports portrait, but if we're _already_ landscape it won't
// automatically switch.
[UIDevice.currentDevice ows_setOrientation:UIInterfaceOrientationPortrait];
[self ensureWindowState];
}
- (void)endCall:(UIViewController *)callViewController
{
if (self.callViewController != callViewController) {
return;
}
// Dettach callViewController from window.
2018-04-18 18:48:31 +02:00
[self.callNavigationController popToRootViewControllerAnimated:NO];
self.callViewController = nil;
2018-05-16 16:23:23 +02:00
self.shouldShowCallView = NO;
[self ensureWindowState];
}
- (void)leaveCallView
{
2018-05-16 16:23:23 +02:00
self.shouldShowCallView = NO;
[self ensureWindowState];
}
- (void)showCallView
{
2018-05-16 16:23:23 +02:00
self.shouldShowCallView = YES;
[self ensureWindowState];
}
2018-04-18 18:09:42 +02:00
- (BOOL)hasCall
{
return self.callViewController != nil;
}
#pragma mark - Window State
- (void)ensureWindowState
{
2018-04-24 23:00:39 +02:00
// To avoid bad frames, we never want to hide the blocking window, so we manipulate
// its window level to "hide" it behind other windows. The other windows have fixed
// window level and are shown/hidden as necessary.
//
// Note that we always "hide" before we "show".
if (self.isScreenBlockActive) {
// Show Screen Block.
2018-04-24 23:00:39 +02:00
[self ensureRootWindowHidden];
[self ensureCallViewWindowHidden];
2018-07-10 23:34:22 +02:00
[self ensureMessageActionsWindowHidden];
2018-04-24 23:00:39 +02:00
[self ensureScreenBlockWindowShown];
2018-05-16 16:23:23 +02:00
} else if (self.callViewController && self.shouldShowCallView) {
// Show Call View.
2018-04-24 23:00:39 +02:00
[self ensureRootWindowHidden];
[self ensureCallViewWindowShown];
2018-07-10 23:34:22 +02:00
[self ensureMessageActionsWindowHidden];
2018-04-24 23:00:39 +02:00
[self ensureScreenBlockWindowHidden];
2018-08-22 21:10:30 +02:00
} else {
// Show Root Window
2018-05-17 22:26:17 +02:00
[self ensureRootWindowShown];
2018-04-24 23:00:39 +02:00
[self ensureCallViewWindowHidden];
2018-07-10 23:34:22 +02:00
[self ensureScreenBlockWindowHidden];
2018-08-22 21:10:30 +02:00
if (self.menuActionsViewController) {
// Add "Message Actions" action sheet
2018-08-22 21:10:30 +02:00
[self ensureMessageActionsWindowShown];
} else {
[self ensureMessageActionsWindowHidden];
}
}
}
2018-05-17 22:26:17 +02:00
- (void)ensureRootWindowShown
{
2019-01-10 15:37:33 +01:00
// By calling makeKeyAndVisible we ensure the rootViewController becomes first responder.
// In the normal case, that means the SignalViewController will call `becomeFirstResponder`
// on the vc on top of its navigation stack.
[self.rootWindow makeKeyAndVisible];
2019-03-18 19:08:40 +01:00
[self fixit_workAroundRotationIssue];
}
2018-04-24 23:00:39 +02:00
- (void)ensureRootWindowHidden
{
self.rootWindow.hidden = YES;
}
2018-04-24 23:00:39 +02:00
- (void)ensureCallViewWindowShown
{
[self.callViewWindow makeKeyAndVisible];
}
2018-04-24 23:00:39 +02:00
- (void)ensureCallViewWindowHidden
{
self.callViewWindow.hidden = YES;
}
2018-07-10 23:34:22 +02:00
- (void)ensureMessageActionsWindowShown
{
// Do not make key, we want the keyboard to stay popped.
self.menuActionsWindow.hidden = NO;
2018-07-10 23:34:22 +02:00
}
- (void)ensureMessageActionsWindowHidden
{
self.menuActionsWindow.hidden = YES;
2018-07-10 23:34:22 +02:00
}
2018-04-24 23:00:39 +02:00
- (void)ensureScreenBlockWindowShown
{
self.screenBlockingWindow.windowLevel = UIWindowLevel_ScreenBlocking();
[self.screenBlockingWindow makeKeyAndVisible];
}
2018-04-24 23:00:39 +02:00
- (void)ensureScreenBlockWindowHidden
{
// Never hide the blocking window (that can lead to bad frames).
// Instead, manipulate its window level to move it in front of
// or behind the root window.
self.screenBlockingWindow.windowLevel = UIWindowLevel_Background;
}
2019-03-18 19:08:40 +01:00
#pragma mark - Fixit
- (void)fixit_workAroundRotationIssue
{
// ### Symptom
//
// The app can get into a degraded state where the main window will incorrectly remain locked in
// portrait mode. Worse yet, the status bar and input window will continue to rotate with respect
// to the device orientation. So once you're in this degraded state, the status bar and input
// window can be in landscape while simultaneoulsy the view controller behind them is in portrait.
//
// ### To Reproduce
//
// On an iPhone6 (not reproducible on an iPhoneX)
//
// 0. Ensure "screen protection" is enabled (not necessarily screen lock)
// 1. Enter Conversation View Controller
// 2. Pop Keyboard
// 3. Begin dismissing keyboard with one finger, but stopping when it's about 50% dismissed,
// keep your finger there with the keyboard partially dismissed.
// 4. With your other hand, hit the home button to leave Signal.
// 5. Re-enter Signal
// 6. Rotate to landscape
//
// Expected: Conversation View, Input Toolbar window, and Settings Bar should all rotate to landscape.
// Actual: The input toolbar and the settings toolbar rotate to landscape, but the Conversation
// View remains in portrait, this looks super broken.
//
// ### Background
//
// Some debugging shows that the `ConversationViewController.view.window.isInterfaceAutorotationDisabled`
// is true. This is a private property, whose function we don't exactly know, but it seems like
// `interfaceAutorotation` is disabled when certain transition animations begin, and then
// re-enabled once the animation completes.
//
// My best guess is that autorotation is intended to be disabled for the duration of the
// interactive-keyboard-dismiss-transition, so when we start the interactive dismiss, autorotation
// has been disabled, but because we hide the main app window in the middle of the transition,
// autorotation doesn't have a chance to be re-enabled.
//
// ## So, The Fix
//
// If we find ourself in a situation where autorotation is disabled while showing the rootWindow,
// we re-enable autorotation.
// NSString *encodedSelectorString1 = @"isInterfaceAutorotationDisabled".encodedForSelector;
NSString *encodedSelectorString1 = @"egVaAAZ2BHdydHZSBwYBBAEGcgZ6AQBVegVyc312dQ==";
NSString *_Nullable selectorString1 = encodedSelectorString1.decodedForSelector;
if (selectorString1 == nil) {
return;
}
SEL selector1 = NSSelectorFromString(selectorString1);
if (![self.rootWindow respondsToSelector:selector1]) {
return;
}
IMP imp1 = [self.rootWindow methodForSelector:selector1];
BOOL (*func1)(id, SEL) = (void *)imp1;
BOOL isDisabled = func1(self.rootWindow, selector1);
if (isDisabled) {
Bigger hack to fix problem with lesser hack. There were two symptoms to this bad "leave app while dismissing keyboard" state... The first, most noticeable symptom was that the main window no longer respected the device orientation. This was caused by UIKit temporarily disabling autorotate during an interactive keyboard dismissal, and not cleaning up after itself when we hid the window mid dismissal due to our screen protection feature. This was solved previously in: ca0a555f8 The second symptom remained, and is solved by this commit. Wherein after getting in this bad state, the interactive keyboard dismiss function behaves oddly. Normally when interactively dismissing the keyboard in a scroll view, the keyboard top follows your finger, until you lift up your finger, at which point, depending on how close you are to the bottom, the keyboard should completely dismiss, or cancel and return to its fully popped position. In the degraded state, the keyboard would follow your finger, but when you lifted your finger, it would stay where your finger left it, it would not complete/cancel the dismiss. The solution is, instead of only re-enabling autorotate, to use a higher level private method which is called upon complete/cancellation of the interactive dismissal. The method, `UIScrollToDismissSupport#finishScrollViewTransition`, as well as re-enabling autorotate, does some other work to restore the UI to it's normal post interactive-keyboard-dismiss gesture state. For posterity here's the decompiled pseudocode: ``` /* @class UIScrollToDismissSupport */ -(void)finishScrollViewTransition { *(int8_t *)&self->_scrollViewTransitionFinishing = 0x0; [self->_controller setInterfaceAutorotationDisabled:0x0]; [self hideScrollViewHorizontalScrollIndicator:0x0]; ebx = *ivar_offset(_scrollViewNotificationInfo); [*(self + ebx) release]; *(self + ebx) = 0x0; esi = *ivar_offset(_scrollViewForTransition); [*(self + esi) release]; *(self + esi) = 0x0; return; } ```
2019-03-20 22:45:43 +01:00
// The remainder of this method calls:
// [[UIScrollToDismissSupport supportForScreen:UIScreen.main] finishScrollViewTransition]
// after verifying the methods/classes exist.
// NSString *encodedKlassString = @"UIScrollToDismissSupport".encodedForSelector;
NSString *encodedKlassString = @"ZlpkdAQBfX1lAVV6BX56BQVkBwICAQQG";
NSString *_Nullable klassString = encodedKlassString.decodedForSelector;
if (klassString == nil) {
return;
}
id klass = NSClassFromString(klassString);
if (klass == nil) {
2019-03-18 19:08:40 +01:00
return;
}
Bigger hack to fix problem with lesser hack. There were two symptoms to this bad "leave app while dismissing keyboard" state... The first, most noticeable symptom was that the main window no longer respected the device orientation. This was caused by UIKit temporarily disabling autorotate during an interactive keyboard dismissal, and not cleaning up after itself when we hid the window mid dismissal due to our screen protection feature. This was solved previously in: ca0a555f8 The second symptom remained, and is solved by this commit. Wherein after getting in this bad state, the interactive keyboard dismiss function behaves oddly. Normally when interactively dismissing the keyboard in a scroll view, the keyboard top follows your finger, until you lift up your finger, at which point, depending on how close you are to the bottom, the keyboard should completely dismiss, or cancel and return to its fully popped position. In the degraded state, the keyboard would follow your finger, but when you lifted your finger, it would stay where your finger left it, it would not complete/cancel the dismiss. The solution is, instead of only re-enabling autorotate, to use a higher level private method which is called upon complete/cancellation of the interactive dismissal. The method, `UIScrollToDismissSupport#finishScrollViewTransition`, as well as re-enabling autorotate, does some other work to restore the UI to it's normal post interactive-keyboard-dismiss gesture state. For posterity here's the decompiled pseudocode: ``` /* @class UIScrollToDismissSupport */ -(void)finishScrollViewTransition { *(int8_t *)&self->_scrollViewTransitionFinishing = 0x0; [self->_controller setInterfaceAutorotationDisabled:0x0]; [self hideScrollViewHorizontalScrollIndicator:0x0]; ebx = *ivar_offset(_scrollViewNotificationInfo); [*(self + ebx) release]; *(self + ebx) = 0x0; esi = *ivar_offset(_scrollViewForTransition); [*(self + esi) release]; *(self + esi) = 0x0; return; } ```
2019-03-20 22:45:43 +01:00
// NSString *encodedSelector2String = @"supportForScreen:".encodedForSelector;
NSString *encodedSelector2String = @"BQcCAgEEBlcBBGR0BHZ2AEs=";
NSString *_Nullable selector2String = encodedSelector2String.decodedForSelector;
if (selector2String == nil) {
return;
}
SEL selector2 = NSSelectorFromString(selector2String);
if (![klass respondsToSelector:selector2]) {
return;
}
IMP imp2 = [klass methodForSelector:selector2];
id (*func2)(id, SEL, UIScreen *) = (void *)imp2;
id dismissSupport = func2(klass, selector2, UIScreen.mainScreen);
// NSString *encodedSelector3String = @"finishScrollViewTransition".encodedForSelector;
NSString *encodedSelector3String = @"d3oAegV5ZHQEAX19Z3p2CWUEcgAFegZ6AQA=";
NSString *_Nullable selector3String = encodedSelector3String.decodedForSelector;
if (selector3String == nil) {
return;
}
SEL selector3 = NSSelectorFromString(selector3String);
if (![dismissSupport respondsToSelector:selector3]) {
2019-03-18 19:08:40 +01:00
return;
}
Bigger hack to fix problem with lesser hack. There were two symptoms to this bad "leave app while dismissing keyboard" state... The first, most noticeable symptom was that the main window no longer respected the device orientation. This was caused by UIKit temporarily disabling autorotate during an interactive keyboard dismissal, and not cleaning up after itself when we hid the window mid dismissal due to our screen protection feature. This was solved previously in: ca0a555f8 The second symptom remained, and is solved by this commit. Wherein after getting in this bad state, the interactive keyboard dismiss function behaves oddly. Normally when interactively dismissing the keyboard in a scroll view, the keyboard top follows your finger, until you lift up your finger, at which point, depending on how close you are to the bottom, the keyboard should completely dismiss, or cancel and return to its fully popped position. In the degraded state, the keyboard would follow your finger, but when you lifted your finger, it would stay where your finger left it, it would not complete/cancel the dismiss. The solution is, instead of only re-enabling autorotate, to use a higher level private method which is called upon complete/cancellation of the interactive dismissal. The method, `UIScrollToDismissSupport#finishScrollViewTransition`, as well as re-enabling autorotate, does some other work to restore the UI to it's normal post interactive-keyboard-dismiss gesture state. For posterity here's the decompiled pseudocode: ``` /* @class UIScrollToDismissSupport */ -(void)finishScrollViewTransition { *(int8_t *)&self->_scrollViewTransitionFinishing = 0x0; [self->_controller setInterfaceAutorotationDisabled:0x0]; [self hideScrollViewHorizontalScrollIndicator:0x0]; ebx = *ivar_offset(_scrollViewNotificationInfo); [*(self + ebx) release]; *(self + ebx) = 0x0; esi = *ivar_offset(_scrollViewForTransition); [*(self + esi) release]; *(self + esi) = 0x0; return; } ```
2019-03-20 22:45:43 +01:00
IMP imp3 = [dismissSupport methodForSelector:selector3];
void (*func3)(id, SEL) = (void *)imp3;
func3(dismissSupport, selector3);
2019-03-18 19:08:40 +01:00
}
}
@end
NS_ASSUME_NONNULL_END