element-ios/Riot/Modules/MatrixKit/Controllers/MXKViewController.m

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