1
votes

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.

1
You can remove the self.frame = frame; line in your initialization code. The call to super should do that already.Michael Enriquez

1 Answers

4
votes

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.