Dynamically change the root view controller, so that when we transition from a portrait-only to a landscape-friendly view.

This allows to support both orientations for the iPhone.

We now use a RootViewController for this, instead of relying only on the PhoneMainView. The root view controller is in charge of swapping the portrait-only and the landscape PhoneMainView when needed.
It also inherits the view stack management, since we now have 2 phone main views.

This is a bit convoluted, but necessary to handle the ChatRoom and the InCall views for the iPhone
This commit is contained in:
Guillaume BIENKOWSKI 2014-10-02 16:27:23 +02:00
parent 8595c6a6fa
commit dc04af32b4
8 changed files with 162 additions and 72 deletions

View file

@ -108,7 +108,7 @@ static UICompositeViewDescription *compositeDescription = nil;
tabBar:/*@"UIMainBar"*/nil
tabBarEnabled:false /*to keep room for chat*/
fullscreen:false
landscapeMode:[LinphoneManager runningOnIpad]
landscapeMode:true
portraitMode:true];
}
return compositeDescription;

View file

@ -130,9 +130,6 @@ static UICompositeViewDescription *compositeDescription = nil;
[hideControlsTimer invalidate];
hideControlsTimer = nil;
}
if ([[UIDevice currentDevice].systemVersion doubleValue] < 5.0) {
[callTableController viewWillDisappear:animated];
}
if( hiddenVolume ) {
[[PhoneMainView instance] setVolumeHidden:FALSE];
@ -147,10 +144,6 @@ static UICompositeViewDescription *compositeDescription = nil;
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if ([[UIDevice currentDevice].systemVersion doubleValue] < 5.0) {
[callTableController viewWillAppear:animated];
}
// Set observer
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(callUpdateEvent:)
@ -161,11 +154,7 @@ static UICompositeViewDescription *compositeDescription = nil;
LinphoneCall* call = linphone_core_get_current_call([LinphoneManager getLc]);
LinphoneCallState state = (call != NULL)?linphone_call_get_state(call): 0;
[self callUpdate:call state:state animated:FALSE];
if ([[UIDevice currentDevice].systemVersion doubleValue] < 5.0) {
[callTableController viewDidAppear:animated];
}
// Set windows (warn memory leaks)
linphone_core_set_native_video_window_id([LinphoneManager getLc], (unsigned long)videoView);
linphone_core_set_native_preview_window_id([LinphoneManager getLc], (unsigned long)videoPreview);
@ -180,11 +169,7 @@ static UICompositeViewDescription *compositeDescription = nil;
[[UIApplication sharedApplication] setIdleTimerDisabled:false];
UIDevice *device = [UIDevice currentDevice];
device.proximityMonitoringEnabled = NO;
if ([[UIDevice currentDevice].systemVersion doubleValue] < 5.0) {
[callTableController viewDidDisappear:animated];
}
[[PhoneMainView instance] fullScreen:false];
// Disable tap
[singleFingerTap setEnabled:FALSE];

View file

@ -25,7 +25,6 @@
@interface LinphoneAppDelegate : NSObject <UIApplicationDelegate,UIAlertViewDelegate> {
@private
UIWindow *window;
UIBackgroundTaskIdentifier bgStartId;
BOOL started;
int savedMaxCall;

View file

@ -231,6 +231,7 @@
if(!started) {
started = TRUE;
[self.window makeKeyAndVisible];
[RootViewManager setupWithPortrait:(PhoneMainView*)self.window.rootViewController];
[[PhoneMainView instance] startUp];
}
}

View file

@ -308,7 +308,7 @@
firstResponder = [UICompositeViewController findFirstResponder:controller.view];
}
// [[UIApplication sharedApplication] setStatusBarOrientation:orientation animated:animated];
[[UIApplication sharedApplication] setStatusBarOrientation:orientation animated:animated];
if(firstResponder) {
[firstResponder resignFirstResponder];
[firstResponder becomeFirstResponder];
@ -343,11 +343,6 @@
}
if(remove) {
[LinphoneLogger log:LinphoneLoggerLog format:@"Free cached view: %@", key];
UIViewController *vc = [viewControllerCache objectForKey:key];
if ([[UIDevice currentDevice].systemVersion doubleValue] >= 5.0) {
[vc viewWillUnload];
}
[vc viewDidUnload];
[viewControllerCache removeObjectForKey:key];
}
}
@ -506,7 +501,7 @@
self.tabBarViewController = newTabBarViewController;
// Update rotation
UIInterfaceOrientation correctOrientation = [self getCorrectInterfaceOrientation:[[UIDevice currentDevice] orientation]];
UIInterfaceOrientation correctOrientation = [self getCorrectInterfaceOrientation:(UIDeviceOrientation)[UIApplication sharedApplication].statusBarOrientation];
if(currentOrientation != correctOrientation) {
[UICompositeViewController setOrientation:correctOrientation animated:currentOrientation!=UIDeviceOrientationUnknown];
if( UIInterfaceOrientationIsLandscape(correctOrientation) ){

View file

@ -23,6 +23,7 @@
#import "LinphoneManager.h"
#import "UICompositeViewController.h"
/* These imports are here so that we can import PhoneMainView.h without bothering to import all the rest of the view headers */
#import "AboutViewController.h"
#import "FirstLoginViewController.h"
#import "IncomingCallViewController.h"
@ -43,10 +44,23 @@
#import "ConsoleViewController.h"
#import "ImageViewController.h"
@class PhoneMainView;
@interface RootViewManager : NSObject
@property (nonatomic, retain) PhoneMainView* portraitViewController;
@property (nonatomic, retain) PhoneMainView* rotatingViewController;
@property (nonatomic, retain) NSMutableArray* viewDescriptionStack;
+(RootViewManager*)instance;
+ (void)setupWithPortrait:(PhoneMainView*)portrait;
- (PhoneMainView*)currentView;
@end
@interface PhoneMainView : UIViewController<IncomingCallViewDelegate> {
@private
int loadCount;
NSMutableArray *viewStack;
NSMutableArray *inhibitedEvents;
NSTimer *batteryTimer;
}
@ -54,6 +68,7 @@
@property (nonatomic, retain) IBOutlet UIView *statusBarBG;
@property (nonatomic, retain) IBOutlet UICompositeViewController *mainViewController;
@property (nonatomic, retain) NSString* name;
@property (readonly) UICompositeViewDescription *currentView;
@property (readonly, retain) MPVolumeView* volumeView;

View file

@ -25,7 +25,83 @@
#import "Utils.h"
#import "DTActionSheet.h"
static PhoneMainView* phoneMainViewInstance=nil;
static RootViewManager* rootViewManagerInstance = nil;
@implementation RootViewManager {
PhoneMainView* currentViewController;
}
+ (void)setupWithPortrait:(PhoneMainView*)portrait {
assert(rootViewManagerInstance == nil);
rootViewManagerInstance = [[RootViewManager alloc]initWithPortrait:portrait];
}
- (instancetype)initWithPortrait:(PhoneMainView*)portrait {
self = [super init];
if ( self ){
self.portraitViewController = portrait;
self.rotatingViewController = [[[PhoneMainView alloc] init] autorelease];
self.portraitViewController.name = @"Portrait";
self.rotatingViewController.name = @"Rotating";
currentViewController = portrait;
self.viewDescriptionStack = [NSMutableArray array];
}
return self;
}
+ (RootViewManager *)instance {
if( !rootViewManagerInstance ){
@throw [NSException exceptionWithName:@"RootViewManager" reason:@"nil instance" userInfo:nil];
}
return rootViewManagerInstance;
}
- (PhoneMainView*)currentView {
return currentViewController;
}
- (PhoneMainView*)setViewControllerForDescription:(UICompositeViewDescription*)description {
PhoneMainView* newMainView = description.landscapeMode ? self.rotatingViewController : self.portraitViewController;
if( [LinphoneManager runningOnIpad] ) return currentViewController;
if( newMainView != currentViewController )
{
PhoneMainView* previousMainView = currentViewController;
UIInterfaceOrientation nextViewOrientation = newMainView.interfaceOrientation;
UIInterfaceOrientation previousOrientation = currentViewController.interfaceOrientation;
Linphone_err(@"Changing rootViewController: %@ -> %@", currentViewController.name, newMainView.name);
currentViewController = newMainView;
LinphoneAppDelegate* delegate = (LinphoneAppDelegate*)[UIApplication sharedApplication].delegate;
[UIView transitionWithView:delegate.window
duration:0.3
options:UIViewAnimationOptionTransitionFlipFromLeft|UIViewAnimationOptionAllowAnimatedContent
animations:^{
delegate.window.rootViewController = newMainView;
// when going to landscape-enabled view, we have to get the current portrait frame and orientation,
// because it could still have landscape-based size
if( nextViewOrientation != previousOrientation && newMainView == self.rotatingViewController ){
newMainView.view.frame = previousMainView.view.frame;
[newMainView.mainViewController.view setFrame:previousMainView.mainViewController.view.frame];
[newMainView willRotateToInterfaceOrientation:previousOrientation duration:0.3];
[newMainView willAnimateRotationToInterfaceOrientation:previousOrientation duration:0.3];
[newMainView didRotateFromInterfaceOrientation:nextViewOrientation];
}
}
completion:^(BOOL finished) {
}];
}
return currentViewController;
}
@end
@implementation PhoneMainView
@ -38,11 +114,7 @@ static PhoneMainView* phoneMainViewInstance=nil;
#pragma mark - Lifecycle Functions
- (void)initPhoneMainView {
assert (!phoneMainViewInstance);
phoneMainViewInstance = self;
currentView = nil;
viewStack = [[NSMutableArray alloc] init];
loadCount = 0; // For avoiding IOS 4 bug
inhibitedEvents = [[NSMutableArray alloc] init];
}
@ -75,7 +147,6 @@ static PhoneMainView* phoneMainViewInstance=nil;
[mainViewController release];
[inhibitedEvents release];
[viewStack release];
[super dealloc];
}
@ -84,10 +155,6 @@ static PhoneMainView* phoneMainViewInstance=nil;
#pragma mark - ViewController Functions
- (void)viewDidLoad {
// Avoid IOS 4 bug
if(loadCount++ > 0)
return;
[super viewDidLoad];
volumeView = [[MPVolumeView alloc] initWithFrame: CGRectMake(-100,-100,16,16)];
@ -173,9 +240,6 @@ static PhoneMainView* phoneMainViewInstance=nil;
- (void)viewDidUnload {
[super viewDidUnload];
// Avoid IOS 4 bug
loadCount--;
}
- (void)setVolumeHidden:(BOOL)hidden {
@ -194,15 +258,11 @@ static PhoneMainView* phoneMainViewInstance=nil;
- (NSUInteger)supportedInterfaceOrientations {
static NSUInteger supported = -1;
if (supported == -1) {
if( [LinphoneManager runningOnIpad ] )
supported = UIInterfaceOrientationMaskAll;
else
supported = UIInterfaceOrientationMaskPortrait;
if( [LinphoneManager runningOnIpad ] || [mainViewController currentViewSupportsLandscape] )
return UIInterfaceOrientationMaskAll;
else {
return UIInterfaceOrientationMaskPortrait;
}
return supported;
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
@ -227,7 +287,7 @@ static PhoneMainView* phoneMainViewInstance=nil;
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
[mainViewController clearCache:viewStack];
[mainViewController clearCache:[RootViewManager instance].viewDescriptionStack];
}
#pragma mark - Event Functions
@ -451,7 +511,7 @@ static PhoneMainView* phoneMainViewInstance=nil;
}
+ (PhoneMainView *) instance {
return phoneMainViewInstance;
return [[RootViewManager instance] currentView];
}
- (void) showTabBar:(BOOL)show {
@ -462,18 +522,11 @@ static PhoneMainView* phoneMainViewInstance=nil;
[mainViewController setStateBarHidden:!show];
}
+ (BOOL)isDarkBackgroundView:(UICompositeViewDescription*)view {
return ( [view equal:[DialerViewController compositeViewDescription]] ||
[view equal:[IncomingCallViewController compositeViewDescription]] ||
[view equal:[InCallViewController compositeViewDescription]] ||
[view equal:[WizardViewController compositeViewDescription]]);
}
- (void)updateStatusBar:(UICompositeViewDescription*)to_view {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
// In iOS7, the app has a black background on dialer, incoming and incall, so we have to adjust the
// status bar style for each transition to/from these views
BOOL toLightStatus = (to_view != NULL) && ![PhoneMainView isDarkBackgroundView:to_view];
BOOL toLightStatus = (to_view != NULL) && ![to_view darkBackground];
if( !toLightStatus ) {
// black bg: white text on black background
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
@ -504,7 +557,8 @@ static PhoneMainView* phoneMainViewInstance=nil;
- (UIViewController*)changeCurrentView:(UICompositeViewDescription*)view push:(BOOL)push {
BOOL force = push;
if(!push) {
NSMutableArray* viewStack = [RootViewManager instance].viewDescriptionStack;
if(!push ) {
force = [viewStack count] > 1;
[viewStack removeAllObjects];
}
@ -514,27 +568,32 @@ static PhoneMainView* phoneMainViewInstance=nil;
- (UIViewController*)_changeCurrentView:(UICompositeViewDescription*)view transition:(CATransition*)transition force:(BOOL)force {
[LinphoneLogger logc:LinphoneLoggerLog format:"PhoneMainView: Change current view to %@", [view name]];
if(force || ![view equal: currentView]) {
PhoneMainView* vc = [[RootViewManager instance] setViewControllerForDescription:view];
if(force || ![view equal:vc.currentView] || vc != self) {
if(transition == nil)
transition = [PhoneMainView getTransition:currentView new:view];
transition = [PhoneMainView getTransition:vc.currentView new:view];
if ([[LinphoneManager instance] lpConfigBoolForKey:@"animations_preference"] == true) {
[mainViewController setViewTransition:transition];
[vc.mainViewController setViewTransition:transition];
} else {
[mainViewController setViewTransition:nil];
[vc.mainViewController setViewTransition:nil];
}
[self updateStatusBar:view];
[mainViewController changeView:view];
currentView = view;
[vc updateStatusBar:view];
[vc.mainViewController changeView:view];
vc->currentView = view;
}
//[[RootViewManager instance] setViewControllerForDescription:view];
NSDictionary* mdict = [NSMutableDictionary dictionaryWithObject:currentView forKey:@"view"];
NSDictionary* mdict = [NSMutableDictionary dictionaryWithObject:vc->currentView forKey:@"view"];
[[NSNotificationCenter defaultCenter] postNotificationName:kLinphoneMainViewChange object:self userInfo:mdict];
return [mainViewController getCurrentViewController];
return [vc->mainViewController getCurrentViewController];
}
- (void)popToView:(UICompositeViewDescription*)view {
NSMutableArray* viewStack = [RootViewManager instance].viewDescriptionStack;
while([viewStack count] > 1 && ![[viewStack lastObject] equal:view]) {
[viewStack removeLastObject];
}
@ -543,7 +602,8 @@ static PhoneMainView* phoneMainViewInstance=nil;
- (UICompositeViewDescription *)firstView {
UICompositeViewDescription *view = nil;
if([viewStack count]) {
NSArray* viewStack = [RootViewManager instance].viewDescriptionStack;
if([viewStack count]) {
view = [viewStack objectAtIndex:0];
}
return view;
@ -551,7 +611,8 @@ static PhoneMainView* phoneMainViewInstance=nil;
- (UIViewController*)popCurrentView {
[LinphoneLogger logc:LinphoneLoggerLog format:"PhoneMainView: Pop view"];
if([viewStack count] > 1) {
NSMutableArray* viewStack = [RootViewManager instance].viewDescriptionStack;
if([viewStack count] > 1) {
[viewStack removeLastObject];
[self _changeCurrentView:[viewStack lastObject] transition:[PhoneMainView getBackwardTransition] force:TRUE];
return [mainViewController getCurrentViewController];

34
UI.md Normal file
View file

@ -0,0 +1,34 @@
Quick UI reference for Linphone iOS:
- The app is contained in a window, which resides in the MainStoryboard file.
- The delegate is set to LinphoneAppDelegate in main.m, in the UIApplicationMain() by passing its class
- Basic layout:
MainStoryboard
|
| (rootViewController)
|
PhoneMainView ---> view #--> app background
| |
| #--> statusbar background
|
| (mainViewController)
|
UICompositeViewController : TPMultilayout
|
#---> view #--> stateBar
|
#--> contentView
|
#--> tabBar
When the app is started, the phoneMainView gets asked to transition to the Dialer view or the Wizard view.
PhoneMainView exposes the -changeCurrentView: method, which will setup its
Any Linphone view is actually presented in the UICompositeViewController, with or without a stateBar and tabBar.
The UICompositeViewController consists of 3 areas laid out vertically. From top to bottom: StateBar, Content and TabBar.
The TabBar is usually the UIMainBar, which is used as a navigation controller: clicking on each of the buttons will trigger
a transition to another "view".