UIInterfaceOrientation not yet updated when UIDeviceOrientationDidChangeNotification caught in UIView - ios

I'm working on an open source library to provide an iMessage-like text input view - MessageComposerView (relevant to question). The view will stick to the keyboard like an inputAccessoryView and grow with text, but won't disappear when the keyboard does.
I have recently run into a maddening issue when rotating this custom UIView that only appears if the view has been instantiated via initWithFrame. Basically at the point when a UIDeviceOrientationDidChangeNotification notification has been caught if the view has been instantiated via initWithFrame, the UIInterfaceOrientation and frame have NOT yet been updated. Here are both ways instantiating.
loadNibNamed:
self.messageComposerView = [[NSBundle mainBundle] loadNibNamed:#"MessageComposerView" owner:nil options:nil][0];
self.messageComposerView.frame = CGRectMake(0,
self.view.frame.size.height - self.messageComposerView.frame.size.height,
self.messageComposerView.frame.size.width,
self.messageComposerView.frame.size.height);
initWithFrame:
self.messageComposerView = [[MessageComposerView alloc] initWithFrame:CGRectMake(0, self.view.frame.size.height-54, 320, 54)];
When init via loadNibNamed and rotated to landscape, upon receiving a UIDeviceOrientationDidChangeNotification notification:
UIDeviceOrientation = [[notification object] orientation] = UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientation = [UIApplication sharedApplication].statusBarOrientation = UIDeviceOrientationLandscapeRight
self.frame.size = (width=480, height=90)
Important to note here is that both the interface and device orientations are landscape and the width has already been changed to landscape width (testing on iPhone 6.1 simulator). Now performing the same test but using initWithFrame:
UIDeviceOrientation = [[notification object] orientation] = UIDeviceOrientationLandscapeRight
UIInterfaceOrientation = [UIApplication sharedApplication].statusBarOrientation = UIInterfaceOrientationPortrait
self.frame.size = (width=320, height=54)
This time notice that the interface orientation is still portrait and that the frame width has NOT changed to landscape width. If I set a breakpoint in the setFrame:(CGRect)frame method I can see that the frame is set to the Landscape frame AFTER the UIDeviceOrientationDidChangeNotification has been caught and handled.
Both init methods have almost identical setup:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.frame = frame;
self.sendButton = [[UIButton alloc] initWithFrame:CGRectZero];
self.messageTextView = [[UITextView alloc] initWithFrame:CGRectZero];
[self setup];
[self addSubview:self.sendButton];
[self addSubview:self.messageTextView];
}
return self;
}
- (void)awakeFromNib {
[super awakeFromNib];
[self setup];
}
With setup doing necessary view tweaks:
- (void)setup {
self.backgroundColor = [UIColor lightGrayColor];
self.autoresizesSubviews = YES;
self.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleWidth;
self.userInteractionEnabled = YES;
self.multipleTouchEnabled = NO;
CGRect sendButtonFrame = self.bounds;
sendButtonFrame.size.width = 50;
sendButtonFrame.size.height = 34;
sendButtonFrame.origin.x = self.frame.size.width - kComposerBackgroundRightPadding - sendButtonFrame.size.width;
sendButtonFrame.origin.y = kComposerBackgroundRightPadding;
self.sendButton.frame = sendButtonFrame;
self.sendButton.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin;
self.sendButton.layer.cornerRadius = 5;
[self.sendButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
[self.sendButton setTitleColor:[UIColor colorWithRed:210/255.0 green:210/255.0 blue:210/255.0 alpha:1.0] forState:UIControlStateHighlighted];
[self.sendButton setTitleColor:[UIColor grayColor] forState:UIControlStateSelected];
[self.sendButton setBackgroundColor:[UIColor orangeColor]];
[self.sendButton setTitle:#"Send" forState:UIControlStateNormal];
self.sendButton.titleLabel.font = [UIFont boldSystemFontOfSize:14];
CGRect messageTextViewFrame = self.bounds;
messageTextViewFrame.origin.x = kComposerBackgroundLeftPadding;
messageTextViewFrame.origin.y = kComposerBackgroundTopPadding;
messageTextViewFrame.size.width = self.frame.size.width - kComposerBackgroundLeftPadding - kComposerTextViewButtonBetweenPadding - sendButtonFrame.size.width - kComposerBackgroundRightPadding;
messageTextViewFrame.size.height = 34;
self.messageTextView.frame = messageTextViewFrame;
self.messageTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin;
self.messageTextView.showsHorizontalScrollIndicator = NO;
self.messageTextView.layer.cornerRadius = 5;
self.messageTextView.font = [UIFont systemFontOfSize:14];
self.messageTextView.delegate = self;
[self addNotifications];
[self resizeTextViewForText:#"" animated:NO];
}
So my question is, why does my custom UIView init via initWithFrame still have the portrait frame and interface orientation and the time when the UIDeviceOrientationDidChangeNotification is received. I use this notification to do my frame adjustment and need the width to have already been updated to the landscape width.
I'm hoping there is some kind of autorotation property that I'm missing in the programmatical setup that is buried somewhere in the XIB but just can't find it. I've gone through probably a dozen UIDeviceOrientationDidChangeNotification stackoverflow posts without finding a solution.

MessageComposerView looks neat, thanks for sharing.
Instead of listening to the UIDeviceOrientationDidChangeNotification, you should implement layoutSubviews. It is called in response to rotation and anything else the system would need the UIView to reposition its subviews (https://stackoverflow.com/a/5330162/1181421). There you can position your subviews relative to the UIView's frame, and you can be sure that the frame will be correct.

Related

How to change the frame of a UIView subclass on device orientation which is programmatically created?

I have a UIView subclass which is created programmatically.
I have portrait and landscape mode in my application which uses autolayout.
Initially the UIView frame is set using initWithFrame, but when the orientation changes to landscape the CGRect of UIView is stuck with the portrait mode.
How can I alert the UIView subclass to change the CGRect on device orientation?
This is my initial frame setup code for the UIView:
- (id)initWithFrame:(CGRect)frame videoUrl:(NSURL *)videoUrl{
self = [super initWithFrame:frame];
if (self) {
_frame_width = frame.size.width;
int thumbWidth = ceil(frame.size.width*0.05);
_bgView = [[UIControl alloc] initWithFrame:CGRectMake(thumbWidth-BG_VIEW_BORDERS_SIZE, 0, frame.size.width-(thumbWidth*2)+BG_VIEW_BORDERS_SIZE*2, frame.size.height)];
_bgView.layer.borderColor = [UIColor grayColor].CGColor;
_bgView.layer.borderWidth = BG_VIEW_BORDERS_SIZE;
[self addSubview:_bgView];
}
}
Just detect the orientation of device whether it is in landscape or portrait mode and then change the size of UIView accordingly see below code:
To detect device orientation:
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
if ([[UIDevice currentDevice] orientation] == UIDeviceOrientationLandscapeLeft || [[UIDevice currentDevice] orientation ]== UIDeviceOrientationLandscapeRight)
{
UIView *view = [classObject setupView:xPos yPos:yPos width:widthValue height:heightValue];
yourView.frame = view.frame;
NSLog(#"device orientation Landscapse");
}
if([[UIDevice currentDevice] orientation] == UIDeviceOrientationPortrait || [[UIDevice currentDevice] orientation] == UIDeviceOrientationPortraitUpsideDown )
{
UIView *view = [classObject setupView:xPos yPos:yPos width:widthValue height:heightValue];
yourView.frame = view.frame;
NSLog(#"device orientation portrait");
}
}
Write this method inside your class, no need to call from anywhere it will detect orientation when user rotate.
Create custom method inside your class which is returning UIView with complete frame and call by sending x,y position and its width and height when user changes orientation see below code:
-(UIView *) setupView:(float)x yPos:(float)y width:(float)width height:(float) height{
UIView *yourView = [[UIView alloc] initWithFrame:CGRectMake(x,y,width,height)];
yourView.backgroundColor=[UIColor clearColor];
return yourView;
}
You can use autoresizingMask for both of your view and it's background view to support orientation changes. These masks will be translated to auto layout constrains automatically because translatesAutoresizingMaskIntoConstraints is enabled by default e.g.:
#interface MyView : UIView
#property (nonatomic) UIControl* bgView;
#end
#implementation MyView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = UIColor.redColor;
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
int thumbWidth = ceil(frame.size.width*0.05);
self.bgView = [[UIControl alloc] initWithFrame:CGRectMake(thumbWidth-BG_VIEW_BORDERS_SIZE, 0, frame.size.width-(thumbWidth*2)+BG_VIEW_BORDERS_SIZE*2, frame.size.height)];
self.bgView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.bgView.layer.borderColor = [UIColor grayColor].CGColor;
self.bgView.layer.borderWidth = BG_VIEW_BORDERS_SIZE;
[self addSubview:_bgView];
}
return self;
}
#end

UIVisualEffectView built in storyboard is opaque

I've built an UIView in storyboard that is composed like this:
UIView (called _view)
UIVisualEffetView (dark)
UIVIew
Button
I've done like this for the ability to reuse it between several Navigation Controller. So for this I've set everything and I can show my view in my main controller called "ViewController" that is embedded in a navigation controller. But the issue is that the blur is opaque, for example the blue navigation bar of my view controller is not visible under the UIVIsualAffectView !
Here is the code I use for setting up the custom view :
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
- (void)setup {
_view.backgroundColor = [UIColor clearColor];
UIView *rootView = [[[NSBundle mainBundle] loadNibNamed:#"SideMenuView" owner:self options:nil] objectAtIndex:0];
CGRect newFrame = rootView.frame;
newFrame.size.width = [[UIScreen mainScreen] bounds].size.width / 2;
newFrame.size.height = [[UIScreen mainScreen] bounds].size.height;
[rootView setFrame:newFrame];
UIWindow* currentWindow = [UIApplication sharedApplication].keyWindow;
currentWindow.backgroundColor = [UIColor clearColor];
currentWindow.windowLevel = UIWindowLevelNormal;
[currentWindow addSubview:rootView];
}
And here is the code I use to call the view from my main controller:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self.leftBarButton setTarget:self];
[self.leftBarButton setAction: #selector(test)];
}
- (void)test {
SideMenuView *test = [[SideMenuView alloc] init];
[self.view addSubview:test];
test.translatesAutoresizingMaskIntoConstraints = NO;
}
I've also tried to force the property "setOpaque" to No in my SideMenuView.m but without effect
Here is a screenshot of what it do:
Do you know guys what is wrong with my code ?
I've also added a button in the middle of the screen with a blue background but it's not visible under the blur so the issue is not navigation bar specific
Thanks in advance for your help !
I've resolved my issue by simply creating an UIView with clear background.
What I do for adding the blur effect is to create the UIVisualEffectView programmatically (doing this way it work perfectly !).
After that I'm adding my UIView as a subview of my UIVisualEffectView and voila :)

Cannot see added UIScrollView in UIViewController

I added a UIScrollView to my main UIViewController but for some reason i can't see anything. I painted the scroll view in grey in order to see the actual size, but all i get is a white screen.
Here is my very simple code:
-(void) viewDidLoad{
[super viewDidLoad];
UIScrollView *scroll;
[scroll setBackgroundColor:[UIColor grayColor]];
CGRect screenRect = [[UIScreen mainScreen] bounds];
_orientation = [UIDevice currentDevice].orientation;
if (UIDeviceOrientationIsPortrait(_orientation)){
scroll.showsHorizontalScrollIndicator = YES;
}else{
scroll.showsVerticalScrollIndicator = YES;
}
scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(screenRect.origin.x,
screenRect.origin.y,
screenRect.size.width,
screenRect.size.height)];
[self.view addSubview:scroll];
}
The view size might change, i'm working on a changing iOS device size.

Making a list of UIViews that slide up and down when touched

I'm trying to figure out an approach to build something like the image below, which is a list of items that when a section is clicked slides out content. It's a really common UX on most websites and what not. My idea is to have each gray box (button) slide out a UIView containing some other items. I'm still new to iOS development but I'm struggling to find how you can animate a UIView to slide down and push the content below it down as well. Hoping some one can give me a good starting point or point to some info outside the realm of the apple docs.
Thanks!
So if you just have a few views, I would not recommend the UITableView approach, since it is not so easy to customize with animations and table views usually want to fill the whole screen with cells. Instead write a expandable UIView subclass that has the desired two states. Add a method to switch between extended and collapsed state. On expanding/collapsing adjust their positions so that they always have enough space.
I provide you an example of views adjusting their frames. I guess it should be easy to do the same with auto layout constraints: give the views a fixed height constraint and change this on collapsing/expanding. The same way set the constraints between the views to be 0 so that they are stacked on top of each other.
Expandable View:
#interface ExpandingView(){
UIView *_expandedView;
UIView *_seperatorView;
BOOL _expanded;
}
#end
#implementation ExpandingView
- (id)init
{
self = [super initWithFrame:CGRectMake(15, 0, 290, 50)];
if (self) {
_expanded = NO;
self.clipsToBounds = YES;
_headerView = [[UIView alloc] initWithFrame:self.bounds];
_headerView.backgroundColor = [UIColor colorWithWhite:0.8 alpha:1];
[self addSubview:_headerView];
_seperatorView = [[UIView alloc] initWithFrame:CGRectMake(0, self.bounds.size.height-1, self.bounds.size.width, 1)];
_seperatorView.backgroundColor = [UIColor lightGrayColor];
[self addSubview:_seperatorView];
_expandedView = [[UIView alloc] initWithFrame:CGRectOffset(self.bounds, 0, self.bounds.size.height)];
_expandedView.backgroundColor = [UIColor blueColor];
[self addSubview:_expandedView];
}
return self;
}
- (void)layoutSubviews{
[self adjustLayout];
}
- (void)adjustLayout{
_headerView.frame = CGRectMake(0, 0, self.bounds.size.width, 50);
_seperatorView.frame = CGRectMake(0, 49, self.bounds.size.width, 1);
_expandedView.frame = CGRectMake(0, 50, self.bounds.size.width, self.bounds.size.height-50);
}
- (void)toggleExpandedState{
_expanded = !_expanded;
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, _expanded?200:50);
[self adjustLayout];
}
#end
ViewController:
#interface ExpandingViewController (){
NSArray *_expandingViews;
}
#end
#implementation ExpandingViewController
- (void)viewDidLoad
{
[super viewDidLoad];
_expandingViews = #[
[[ExpandingView alloc] init],
[[ExpandingView alloc] init],
[[ExpandingView alloc] init],
];
for(ExpandingView *view in _expandingViews){
[view.headerView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(expandingViewTapped:)]];
[self.view addSubview:view];
}
}
- (void)viewWillLayoutSubviews{
int y = 100;
for(ExpandingView *view in _expandingViews){
view.frame = CGRectOffset(view.bounds, (CGRectGetWidth(self.view.bounds)-CGRectGetWidth(view.bounds))/2, y);
y+=view.frame.size.height;
}
}
- (void)expandingViewTapped:(UITapGestureRecognizer*)tapper{
ExpandingView *view = (ExpandingView*)tapper.view.superview;
[UIView animateWithDuration:0.5 delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0 options:0 animations:^{
[view toggleExpandedState];
[self.view layoutIfNeeded];
} completion:nil];
}

Navigation controller with 2 navigation bars - how to adjust frame of pushed view controllers?

I have a UINavigationController to which I need to add a second UINavigationBar. Neither of those bars is translucent. Problem is, view controllers that I put inside this navigation controller are partially covered by my second navigation bar. Where do I adjust the frames of those view controllers' views so that I don't get a "blinking" effect of them changing frames while being visible?
EDIT:
This is in viewDidLoad:
UINavigationBar *secondaryNavBar = [[UINavigationBar alloc] initWithFrame:CGRectMake(0, 64, self.view.frame.size.width, 50)];
secondaryNavBar.translucent = NO;
if ([secondaryNavBar respondsToSelector:#selector(setBarTintColor:)]) { //it has to work on iOS 6 as well
secondaryNavBar.barTintColor = [UIColor darkGrayColor];
secondaryNavBar.tintColor = [UIColor whiteColor];
}
else {
secondaryNavBar.tintColor = [UIColor darkGrayColor];
}
[self.view addSubview:secondaryNavBar];
self.secondaryNavBar = secondaryNavBar;
Here's a working solution. Certainly not the best, and I did not make it to support iOS 6, you'll have to work on it and test it.
CustomNavigationController.m :
#implementation CustomNavigationController {
UINavigationBar *bottomNavBar;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self showNavBar];
}
- (void)showNavBar {
UINavigationBar *secondaryNavBar = [[UINavigationBar alloc] initWithFrame:CGRectMake(0, 64, self.view.frame.size.width, 50)];
secondaryNavBar.translucent = NO;
if ([secondaryNavBar respondsToSelector:#selector(setBarTintColor:)]) { //it has to work on iOS 6 as well
secondaryNavBar.barTintColor = [UIColor darkGrayColor];
secondaryNavBar.tintColor = [UIColor whiteColor];
}
else {
secondaryNavBar.tintColor = [UIColor darkGrayColor];
}
[self.view addSubview:secondaryNavBar];
bottomNavBar = secondaryNavBar;
[self layoutNavBar];
}
- (void)layoutNavBar {
// Get the currently displayed view
UIView *contentView = self.topViewController.view;
// Get its frame and height
CGRect contentFrame = contentView.frame;
float height = contentFrame.size.height;
// Adapt height and y origin with the new nav bar
contentFrame.size.height = height - bottomNavBar.frame.size.height;
contentFrame.origin.y = bottomNavBar.frame.origin.y + bottomNavBar.frame.size.height;
// Set the view's frame
contentView.frame = contentFrame;
}
#end
ViewController.m :
#implementation ViewController
-(void)viewDidAppear:(BOOL)animated {
CustomNavigationController *navigation = (CustomNavigationController*)self.navigationController;
[navigation layoutNavBar];
}
#end
Note that you have to call layoutNavBar on viewDidAppear, or the view's frame will be reset by your app. This is not a perfectly clean solution, but a pretty good fix.

Resources