644 lines
21 KiB
Objective-C
644 lines
21 KiB
Objective-C
/*
|
|
Copyright 2018-2024 New Vector Ltd.
|
|
Copyright 2015 OpenMarket Ltd
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
#import "MXKViewController.h"
|
|
|
|
#import "UIViewController+MatrixKit.h"
|
|
#import "MXSession+MatrixKit.h"
|
|
|
|
const CGFloat MXKViewControllerMaxExternalKeyboardHeight = 80;
|
|
|
|
@interface MXKViewController ()
|
|
{
|
|
/**
|
|
Array of `MXSession` instances.
|
|
*/
|
|
NSMutableArray *mxSessionArray;
|
|
|
|
/**
|
|
Keep reference on the pushed view controllers to release them correctly
|
|
*/
|
|
NSMutableArray *childViewControllers;
|
|
}
|
|
@end
|
|
|
|
@implementation MXKViewController
|
|
@synthesize defaultBarTintColor, enableBarTintColorStatusChange;
|
|
@synthesize barTitleColor;
|
|
@synthesize mainSession;
|
|
@synthesize rageShakeManager;
|
|
@synthesize childViewControllers;
|
|
|
|
#pragma mark -
|
|
|
|
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
|
|
{
|
|
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
|
if (self)
|
|
{
|
|
[self finalizeInit];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
|
{
|
|
self = [super initWithCoder:aDecoder];
|
|
if (self)
|
|
{
|
|
[self finalizeInit];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)finalizeInit
|
|
{
|
|
// Set default properties values
|
|
defaultBarTintColor = nil;
|
|
barTitleColor = nil;
|
|
enableBarTintColorStatusChange = YES;
|
|
rageShakeManager = nil;
|
|
|
|
mxSessionArray = [NSMutableArray array];
|
|
childViewControllers = [NSMutableArray array];
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)viewWillAppear:(BOOL)animated
|
|
{
|
|
[super viewWillAppear:animated];
|
|
|
|
if (self.rageShakeManager)
|
|
{
|
|
[self.rageShakeManager cancel:self];
|
|
}
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
|
|
|
|
// Update UI according to mxSession state, and add observer (if need)
|
|
if (mxSessionArray.count)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMatrixSessionStateDidChange:) name:kMXSessionStateDidChangeNotification object:nil];
|
|
}
|
|
[self onMatrixSessionChange];
|
|
}
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated
|
|
{
|
|
[super viewWillDisappear:animated];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionStateDidChangeNotification object:nil];
|
|
|
|
[self.activityIndicator stopAnimating];
|
|
|
|
if (self.rageShakeManager)
|
|
{
|
|
[self.rageShakeManager cancel:self];
|
|
}
|
|
|
|
// Remove keyboard view (if any)
|
|
self.keyboardView = nil;
|
|
self.keyboardHeight = 0;
|
|
}
|
|
|
|
- (void)viewDidAppear:(BOOL)animated
|
|
{
|
|
[super viewDidAppear:animated];
|
|
|
|
MXLogDebug(@"[MXKViewController] %@ viewDidAppear", self.class);
|
|
|
|
// Release properly pushed and/or presented view controller
|
|
if (childViewControllers.count)
|
|
{
|
|
for (id viewController in childViewControllers)
|
|
{
|
|
if ([viewController isKindOfClass:[UINavigationController class]])
|
|
{
|
|
UINavigationController *navigationController = (UINavigationController*)viewController;
|
|
for (id subViewController in navigationController.viewControllers)
|
|
{
|
|
if ([subViewController respondsToSelector:@selector(destroy)])
|
|
{
|
|
[subViewController destroy];
|
|
}
|
|
}
|
|
}
|
|
else if ([viewController respondsToSelector:@selector(destroy)])
|
|
{
|
|
[viewController destroy];
|
|
}
|
|
}
|
|
|
|
[childViewControllers removeAllObjects];
|
|
}
|
|
}
|
|
|
|
- (void)viewDidDisappear:(BOOL)animated
|
|
{
|
|
[super viewDidDisappear:animated];
|
|
|
|
MXLogDebug(@"[MXKViewController] %@ viewDidDisappear", self.class);
|
|
}
|
|
|
|
- (void)setEnableBarTintColorStatusChange:(BOOL)enable
|
|
{
|
|
if (enableBarTintColorStatusChange != enable)
|
|
{
|
|
enableBarTintColorStatusChange = enable;
|
|
|
|
[self onMatrixSessionChange];
|
|
}
|
|
}
|
|
|
|
- (void)setDefaultBarTintColor:(UIColor *)barTintColor
|
|
{
|
|
defaultBarTintColor = barTintColor;
|
|
|
|
if (enableBarTintColorStatusChange)
|
|
{
|
|
// Force update by taking into account the matrix session state.
|
|
[self onMatrixSessionChange];
|
|
}
|
|
else
|
|
{
|
|
// Set default tintColor
|
|
self.navigationController.navigationBar.barTintColor = defaultBarTintColor;
|
|
self.mxk_mainNavigationController.navigationBar.barTintColor = defaultBarTintColor;
|
|
}
|
|
}
|
|
|
|
- (void)setBarTitleColor:(UIColor *)titleColor
|
|
{
|
|
barTitleColor = titleColor;
|
|
|
|
// Retrieve the main navigation controller if the current view controller is embedded inside a split view controller.
|
|
UINavigationController *mainNavigationController = self.mxk_mainNavigationController;
|
|
|
|
// Set navigation bar title color
|
|
NSDictionary<NSString *,id> *titleTextAttributes = self.navigationController.navigationBar.titleTextAttributes;
|
|
if (titleTextAttributes)
|
|
{
|
|
NSMutableDictionary *textAttributes = [NSMutableDictionary dictionaryWithDictionary:titleTextAttributes];
|
|
textAttributes[NSForegroundColorAttributeName] = barTitleColor;
|
|
self.navigationController.navigationBar.titleTextAttributes = textAttributes;
|
|
}
|
|
else if (barTitleColor)
|
|
{
|
|
self.navigationController.navigationBar.titleTextAttributes = @{NSForegroundColorAttributeName: barTitleColor};
|
|
}
|
|
|
|
if (mainNavigationController)
|
|
{
|
|
titleTextAttributes = mainNavigationController.navigationBar.titleTextAttributes;
|
|
if (titleTextAttributes)
|
|
{
|
|
NSMutableDictionary *textAttributes = [NSMutableDictionary dictionaryWithDictionary:titleTextAttributes];
|
|
textAttributes[NSForegroundColorAttributeName] = barTitleColor;
|
|
mainNavigationController.navigationBar.titleTextAttributes = textAttributes;
|
|
}
|
|
else if (barTitleColor)
|
|
{
|
|
mainNavigationController.navigationBar.titleTextAttributes = @{NSForegroundColorAttributeName: barTitleColor};
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)setView:(UIView *)view
|
|
{
|
|
[super setView:view];
|
|
|
|
// Keep the activity indicator (if any)
|
|
if (self.activityIndicator)
|
|
{
|
|
self.activityIndicator.center = self.view.center;
|
|
[self.view addSubview:self.activityIndicator];
|
|
}
|
|
}
|
|
|
|
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
|
|
{
|
|
// Keep ref on destinationViewController
|
|
[childViewControllers addObject:segue.destinationViewController];
|
|
}
|
|
|
|
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
|
|
{
|
|
// Keep ref on presented view controller
|
|
[childViewControllers addObject:viewControllerToPresent];
|
|
|
|
[super presentViewController:viewControllerToPresent animated:flag completion:completion];
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)addMatrixSession:(MXSession*)mxSession
|
|
{
|
|
if (!mxSession || mxSession.state == MXSessionStateClosed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!mxSessionArray.count)
|
|
{
|
|
[mxSessionArray addObject:mxSession];
|
|
|
|
// Add matrix sessions observer on first added session
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMatrixSessionStateDidChange:) name:kMXSessionStateDidChangeNotification object:nil];
|
|
}
|
|
else if ([mxSessionArray indexOfObject:mxSession] == NSNotFound)
|
|
{
|
|
[mxSessionArray addObject:mxSession];
|
|
}
|
|
|
|
// Force update
|
|
[self onMatrixSessionChange];
|
|
}
|
|
|
|
- (void)removeMatrixSession:(MXSession*)mxSession
|
|
{
|
|
if (!mxSession)
|
|
{
|
|
return;
|
|
}
|
|
|
|
NSUInteger index = [mxSessionArray indexOfObject:mxSession];
|
|
if (index != NSNotFound)
|
|
{
|
|
[mxSessionArray removeObjectAtIndex:index];
|
|
|
|
if (!mxSessionArray.count)
|
|
{
|
|
// Remove matrix sessions observer
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionStateDidChangeNotification object:nil];
|
|
}
|
|
}
|
|
|
|
// Force update
|
|
[self onMatrixSessionChange];
|
|
}
|
|
|
|
- (NSArray*)mxSessions
|
|
{
|
|
return [NSArray arrayWithArray:mxSessionArray];
|
|
}
|
|
|
|
- (MXSession*)mainSession
|
|
{
|
|
// We consider the first added session as the main one.
|
|
if (mxSessionArray.count)
|
|
{
|
|
return [mxSessionArray firstObject];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
- (void)withdrawViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
|
|
{
|
|
// Check whether the view controller is embedded inside a navigation controller.
|
|
if (self.navigationController)
|
|
{
|
|
[self popViewController:self navigationController:self.navigationController animated:animated completion:completion];
|
|
}
|
|
else
|
|
{
|
|
// Suppose here the view controller has been presented modally. We dismiss it
|
|
[self dismissViewControllerAnimated:animated completion:completion];
|
|
}
|
|
}
|
|
|
|
- (void)popViewController:(UIViewController*)viewController navigationController:(UINavigationController*)navigationController animated:(BOOL)animated completion:(void (^)(void))completion
|
|
{
|
|
// We pop the view controller (except if it is the root view controller).
|
|
NSUInteger index = [navigationController.viewControllers indexOfObject:viewController];
|
|
if (index != NSNotFound)
|
|
{
|
|
if (index > 0)
|
|
{
|
|
UIViewController *previousViewController = [navigationController.viewControllers objectAtIndex:(index - 1)];
|
|
[navigationController popToViewController:previousViewController animated:animated];
|
|
|
|
if (completion)
|
|
{
|
|
completion();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Check whether the navigation controller is embedded inside a navigation controller, to pop it.
|
|
if (navigationController.navigationController)
|
|
{
|
|
[self popViewController:navigationController navigationController:navigationController.navigationController animated:animated completion:completion];
|
|
}
|
|
else
|
|
{
|
|
// Remove the root view controller
|
|
navigationController.viewControllers = @[];
|
|
// Suppose here the navigation controller has been presented modally. We dismiss it
|
|
[navigationController dismissViewControllerAnimated:animated completion:completion];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)destroy
|
|
{
|
|
// Remove properly keyboard view (remove related key observers)
|
|
self.keyboardView = nil;
|
|
|
|
// Remove observers
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
|
|
mxSessionArray = nil;
|
|
childViewControllers = nil;
|
|
}
|
|
|
|
#pragma mark - Sessions handling
|
|
|
|
- (void)onMatrixSessionStateDidChange:(NSNotification *)notif
|
|
{
|
|
MXSession *mxSession = notif.object;
|
|
|
|
NSUInteger index = [mxSessionArray indexOfObject:mxSession];
|
|
if (index != NSNotFound)
|
|
{
|
|
if (mxSession.state == MXSessionStateClosed)
|
|
{
|
|
// Call here the dedicated method which may be overridden
|
|
[self removeMatrixSession:mxSession];
|
|
}
|
|
else
|
|
{
|
|
[self onMatrixSessionChange];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)onMatrixSessionChange
|
|
{
|
|
// This method is called to refresh view controller appearance on session state change,
|
|
// It is called when the view will appear to update session array by removing closed sessions.
|
|
// Indeed 'kMXSessionStateDidChangeNotification' are observed only when the view controller is visible.
|
|
|
|
// Retrieve the main navigation controller if the current view controller is embedded inside a split view controller.
|
|
UINavigationController *mainNavigationController = self.mxk_mainNavigationController;
|
|
|
|
if (mxSessionArray.count)
|
|
{
|
|
// Check each session state.
|
|
UIColor *barTintColor = defaultBarTintColor;
|
|
BOOL allHomeserverNotReachable = YES;
|
|
BOOL isActivityInProgress = NO;
|
|
for (NSUInteger index = 0; index < mxSessionArray.count;)
|
|
{
|
|
MXSession *mxSession = mxSessionArray[index];
|
|
|
|
// Remove here closed sessions
|
|
if (mxSession.state == MXSessionStateClosed)
|
|
{
|
|
// Call here the dedicated method which may be overridden.
|
|
// This method will call again [onMatrixSessionChange] when session is removed.
|
|
[self removeMatrixSession:mxSession];
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if (mxSession.state == MXSessionStateHomeserverNotReachable)
|
|
{
|
|
barTintColor = [UIColor orangeColor];
|
|
}
|
|
else
|
|
{
|
|
allHomeserverNotReachable = NO;
|
|
isActivityInProgress = mxSession.shouldShowActivityIndicator;
|
|
}
|
|
|
|
index++;
|
|
}
|
|
}
|
|
|
|
// Check whether the navigation bar color depends on homeserver reachability.
|
|
if (enableBarTintColorStatusChange)
|
|
{
|
|
// The navigation bar tintColor reflects the matrix homeserver reachability status.
|
|
if (allHomeserverNotReachable)
|
|
{
|
|
self.navigationController.navigationBar.barTintColor = [UIColor redColor];
|
|
if (mainNavigationController)
|
|
{
|
|
mainNavigationController.navigationBar.barTintColor = [UIColor redColor];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self.navigationController.navigationBar.barTintColor = barTintColor;
|
|
if (mainNavigationController)
|
|
{
|
|
mainNavigationController.navigationBar.barTintColor = barTintColor;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run activity indicator if need
|
|
if (isActivityInProgress)
|
|
{
|
|
[self startActivityIndicator];
|
|
}
|
|
else
|
|
{
|
|
[self stopActivityIndicator];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Hide potential activity indicator
|
|
[self stopActivityIndicator];
|
|
|
|
// Check whether the navigation bar color depends on homeserver reachability.
|
|
if (enableBarTintColorStatusChange)
|
|
{
|
|
// Restore default tintColor
|
|
self.navigationController.navigationBar.barTintColor = defaultBarTintColor;
|
|
if (mainNavigationController)
|
|
{
|
|
mainNavigationController.navigationBar.barTintColor = defaultBarTintColor;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - Activity indicator
|
|
|
|
- (BOOL)canStopActivityIndicator {
|
|
// Check whether all conditions are satisfied before stopping loading wheel
|
|
for (MXSession *mxSession in mxSessionArray)
|
|
{
|
|
if (mxSession.shouldShowActivityIndicator)
|
|
{
|
|
return NO;
|
|
}
|
|
}
|
|
return [super canStopActivityIndicator];
|
|
}
|
|
|
|
#pragma mark - Shake handling
|
|
|
|
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
|
|
{
|
|
if (motion == UIEventSubtypeMotionShake && self.rageShakeManager)
|
|
{
|
|
[self.rageShakeManager startShaking:self];
|
|
}
|
|
}
|
|
|
|
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event
|
|
{
|
|
[self motionEnded:motion withEvent:event];
|
|
}
|
|
|
|
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
|
|
{
|
|
if (self.rageShakeManager)
|
|
{
|
|
[self.rageShakeManager stopShaking:self];
|
|
}
|
|
}
|
|
|
|
- (BOOL)canBecomeFirstResponder
|
|
{
|
|
return (self.rageShakeManager != nil);
|
|
}
|
|
|
|
#pragma mark - Keyboard handling
|
|
|
|
- (void)onKeyboardShowAnimationComplete
|
|
{
|
|
// Do nothing here - `MXKViewController-inherited` instance must override this method.
|
|
}
|
|
|
|
- (void)setKeyboardView:(UIView *)keyboardView
|
|
{
|
|
// Remove previous keyboardView if any
|
|
if (_keyboardView)
|
|
{
|
|
// Restore UIKeyboardWillShowNotification observer
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
|
|
|
|
// Remove keyboard view observers
|
|
[_keyboardView removeObserver:self forKeyPath:NSStringFromSelector(@selector(frame))];
|
|
[_keyboardView removeObserver:self forKeyPath:NSStringFromSelector(@selector(center))];
|
|
|
|
_keyboardView = nil;
|
|
}
|
|
|
|
if (keyboardView)
|
|
{
|
|
// Add observers to detect keyboard drag down
|
|
[keyboardView addObserver:self forKeyPath:NSStringFromSelector(@selector(frame)) options:0 context:nil];
|
|
[keyboardView addObserver:self forKeyPath:NSStringFromSelector(@selector(center)) options:0 context:nil];
|
|
|
|
// Remove UIKeyboardWillShowNotification observer to ignore this notification until keyboard is dismissed.
|
|
// Note: UIKeyboardWillShowNotification may be triggered several times before keyboard is dismissed,
|
|
// because the keyboard height is updated (switch to a Chinese keyboard for example).
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
|
|
|
|
_keyboardView = keyboardView;
|
|
}
|
|
}
|
|
|
|
- (void)onKeyboardWillShow:(NSNotification *)notif
|
|
{
|
|
MXLogDebug(@"[MXKViewController] %@ onKeyboardWillShow", self.class);
|
|
|
|
// Get the keyboard size
|
|
NSValue *rectVal = notif.userInfo[UIKeyboardFrameEndUserInfoKey];
|
|
CGRect endRect = rectVal.CGRectValue;
|
|
|
|
// IOS 8 triggers some unexpected keyboard events
|
|
if ((endRect.size.height == 0) || (endRect.size.width == 0))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Detect if an external keyboard is used
|
|
CGRect keyboard = [self.view convertRect:endRect fromView:self.view.window];
|
|
CGFloat height = self.view.frame.size.height;
|
|
BOOL hasExternalKeyboard = keyboard.size.height <= MXKViewControllerMaxExternalKeyboardHeight;
|
|
|
|
// Get the animation info
|
|
NSNumber *curveValue = [[notif userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
|
|
UIViewAnimationCurve animationCurve = curveValue.intValue;
|
|
// The duration is ignored but it is better to define it
|
|
double animationDuration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
|
|
|
// Apply keyboard animation
|
|
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (animationCurve << 16) animations:^{
|
|
if (!hasExternalKeyboard)
|
|
{
|
|
// Set the new virtual keyboard height by checking screen orientation
|
|
self.keyboardHeight = (endRect.origin.y == 0) ? endRect.size.width : endRect.size.height;
|
|
}
|
|
else
|
|
{
|
|
// The virtual keyboard is not shown on the screen but its toolbar is still displayed.
|
|
// Manage the height of this one
|
|
self.keyboardHeight = height - keyboard.origin.y;
|
|
}
|
|
} completion:^(BOOL finished)
|
|
{
|
|
[self onKeyboardShowAnimationComplete];
|
|
}];
|
|
}
|
|
|
|
- (void)onKeyboardWillHide:(NSNotification *)notif
|
|
{
|
|
MXLogDebug(@"[MXKViewController] %@ onKeyboardWillHide", self.class);
|
|
|
|
// Remove keyboard view
|
|
self.keyboardView = nil;
|
|
|
|
// Get the animation info
|
|
NSNumber *curveValue = [[notif userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
|
|
UIViewAnimationCurve animationCurve = curveValue.intValue;
|
|
// the duration is ignored but it is better to define it
|
|
double animationDuration = [[[notif userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
|
|
|
|
// Apply keyboard animation
|
|
[UIView animateWithDuration:animationDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (animationCurve << 16) animations:^{
|
|
self.keyboardHeight = 0;
|
|
} completion:^(BOOL finished)
|
|
{
|
|
}];
|
|
}
|
|
|
|
#pragma mark - KVO
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
|
{
|
|
if ((object == _keyboardView) && ([keyPath isEqualToString:NSStringFromSelector(@selector(frame))] || [keyPath isEqualToString:NSStringFromSelector(@selector(center))]))
|
|
{
|
|
|
|
// The keyboard view has been modified (Maybe the user drag it down), we update the input toolbar bottom constraint to adjust layout.
|
|
|
|
// Compute keyboard height (on IOS 8 and later, the screen size is oriented)
|
|
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
|
|
self.keyboardHeight = screenSize.height - _keyboardView.frame.origin.y;
|
|
}
|
|
}
|
|
|
|
@end
|