How is that possible that my editable UITextView (placed inside a straightforward UIViewController inside a UISplitView that acts as delegate for the UITextView) is not showing text from the beginning but after something like 6-7 lines?
I didn't set any particular autolayout or something similar, trying to delete text doesn't help (so no hidden chars or something).
I'm using iOS 7 on iPad, in storyboard looks good...
The problem is the same on iOS simulator and real devices. I'm getting mad :P
Here's some code. This is the ViewController viewDidLoad()
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.itemTextField.delegate = self;
self.itemTextField.text = NSLocalizedString(#"NEWITEMPLACEHOLDER", nil);
self.itemTextField.textColor = [UIColor lightGrayColor]; //optional
}
And here are the overridden functions for the UITextView I'm using some code I've found on StackOverflow to simulate a placeholder for the view (the same stuff on iPhone version of the storyboard works fine)...
// UITextView placeholder
- (void)textViewDidBeginEditing:(UITextView *)textView
{
if ([textView.text isEqualToString:NSLocalizedString(#"NEWITEMPLACEHOLDER", nil)]) {
textView.text = #"";
textView.textColor = [UIColor blackColor]; //optional
}
[textView becomeFirstResponder];
}
- (void)textViewDidEndEditing:(UITextView *)textView
{
if ([textView.text isEqualToString:#""]) {
textView.text = NSLocalizedString(#"NEWITEMPLACEHOLDER", nil);
textView.textColor = [UIColor lightGrayColor]; //optional
}
[textView resignFirstResponder];
}
-(void)textViewDidChange:(UITextView *)textView
{
int len = textView.text.length;
charCount.text = [NSString stringWithFormat:#"%#: %i", NSLocalizedString(#"CHARCOUNT", nil),len];
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
return YES;
}
Try to call -sizeToFit after passing the text. This answer could be useful to Vertically align text within a UILabel.
[UPDATE]
I update this answer o make it more readable.
The issue is that from iOS7, container view controllers such as UINavigationController or UITabbarController can change the content insets of scroll views (or views that inherit from it), to avoid content overlapping. This happens only if the scrollview is the main view or the first subviews. To avoid that you should disable this behavior by setting automaticallyAdjustsScrollViewInsets to NO, or overriding this method to return NO.
I got through the same kind of issue.
Solved it by disabling the automatic scrollView insets adjustement :
if(SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0")){
self.automaticallyAdjustsScrollViewInsets = NO; // Avoid the top UITextView space, iOS7 (~bug?)
}
This is a fairly common problem, so I would create a simple UITextView subclass, so that you can re-use it and use it in IB.
I would used the contentInset instead, making sure to gracefully handle the case where the contentSize is larger than the bounds of the textView
#interface BSVerticallyCenteredTextView : UITextView
#end
#implementation BSVerticallyCenteredTextView
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
[self addObserver:self forKeyPath:#"contentSize" options: (NSKeyValueObservingOptionNew) context:NULL];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder])
{
[self addObserver:self forKeyPath:#"contentSize" options: (NSKeyValueObservingOptionNew) context:NULL];
}
return self;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"contentSize"])
{
UITextView *tv = object;
CGFloat deadSpace = ([tv bounds].size.height - [tv contentSize].height);
CGFloat inset = MAX(0, deadSpace/2.0);
tv.contentInset = UIEdgeInsetsMake(inset, tv.contentInset.left, inset, tv.contentInset.right);
}
}
- (void)dealloc
{
[self removeObserver:self forKeyPath:#"contentSize"];
}
#end
use -observerForKeyPath with contentSize KeyPath
Look some code at My Blog (don't focus on Thai Language)
http://www.macbaszii.com/2012/10/ios-dev-uitextview-vertical-alignment.html
Inspired by Kiattisak, I've implemented vertical alignment as a category over UITextView so that you can control the vertical alignment of legacy UITextView.
You can find it as a gist here.
I had the same issue with iOS 8.1, and none of these suggestions worked.
What did work was to go into the Storyboard, and drag my UITableView or UITextView so that it was no longer the first subview of my screen's UIView.
http://www.codeproject.com/Tips/852308/Bug-in-XCode-Vertical-Gap-Above-UITableView
It seems to be linked to having a UIView embedded in a UINavigationController.
Bug ? Bug ? Did I say "bug" ...?
;-)
Swift version of Tanguy.G's answer:
if(UIDevice.currentDevice().systemVersion >= "7.0") {
self.automaticallyAdjustsScrollViewInsets = false; // Avoid the top UITextView space, iOS7 (~bug?)
}
Check top content inset of textView in -viewDidLoad:
NSLog(#"NSStringFromUIEdgeInsets(self.itemTextField.contentInset) = %#", NSStringFromUIEdgeInsets(self.itemTextField.contentInset));
Reset it in storyboard if it is not zero
Related
I tested my app with iOS 10 Beta 7 and Xcode 8 beta and everything worked fine. However just a few minutes ago I installed the now available GM releases of both and faced a weird issue.
I am using custom table view cells in my app and in my custom cell's I am using cornerRadius and clipsToBounds to create rounded views.
- (void)awakeFromNib {
[super awakeFromNib];
self.tag2label.layer.cornerRadius=self.tag2label.frame.size.height/2;
self.tag2label.clipsToBounds=YES;
}
This looked okay before however in the new GM releases all the views which had the rounded corners disappeared. This happened to UIView, UILabels and UIButtons.
I solved this below.
I am not sure if this is a new requirement, but I solved this by adding [self layoutIfNeeded]; before doing any cornerRadius stuff. So my new custom awakeFromNib looks like this:
- (void)awakeFromNib {
[super awakeFromNib];
[self layoutIfNeeded];
self.tag2label.layer.cornerRadius=self.tag2label.frame.size.height/2;
self.tag2label.clipsToBounds=YES;
}
Now they all appear fine.
To fix invisible views with cornerRadius=height/2 create category UIView+LayoutFix
In file UIView+LayoutFix.m add code:
- (void)awakeFromNib {
[super awakeFromNib];
[self layoutIfNeeded];
}
add category to YourProject.PCH file.
It will works only if you used [super awakeFromNib] in your views :
MyView.m
- (void)awakeFromNib {
[super awakeFromNib];
...
}
cornerRadius itself works just fine but the size on the frame is reported incorrectly. which is why layoutIfNeeded fixes the issue.
I have faced the same issue on moving to TVOS 10. Removing auto layout constraints and using the new Autoresizing settings in storyboards solved it for me.
My observation is that iOS 10 / TVOS 10 is not laying out auto layout based views before calling awakeFromNib, but is laying out views using autoresizing masks before calling the same method.
You could create subclass of your view like this:
#implementation RoundImageView
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
self.layer.masksToBounds = YES;
self.layer.cornerRadius = MIN(self.bounds.size.height, self.bounds.size.width)/2;
[self addObserver:self
forKeyPath:#"bounds"
options:NSKeyValueObservingOptionNew
context:(__bridge void * _Nullable)(self)];
}
return self;
}
-(void)dealloc
{
[self removeObserver:self
forKeyPath:#"bounds"
context:(__bridge void * _Nullable)(self)];
}
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
{
if(context == (__bridge void * _Nullable)(self) && object == self && [keyPath isEqualToString:#"bounds"])
{
self.layer.cornerRadius = MIN(self.bounds.size.height, self.bounds.size.width)/2;
}
}
#end
so you'll always have properly rounded corners.
I use this approach and hadn't issues upgrading to Xcode8 and iOS10.
You can also see the view in the debug view hierarchy, but cannot see it in the app.
You have to call layoutIfNeeded on the affected masked/clipped view.
(E.g. If you have a UIImageView and you do masksToBounds on its layer and you cannot see the view in the app etc.)
What I mean by floating view is custom view that is a subview (or appears to be a subview) of scrollview which scrolls along until it anchors on certain point. Similar behavior would be UITableView's section header. Attached image below
My content view (the view underneath the floating view) is not in tableview layout. Meaning if I use tableview only for the floating view, I have to put my content view inside 1 giant cell or break it to several cells with different layouts. The content view will have a lot of dynamic elements which is why I don't want to put it inside UITableViewCell unless I have to. Can I make floating view programmatically / using autolayout on scrollview?
Using the tableview section header is probably the best solution, you can always easily customise the number of cells or cells themselves to achieve a particular layout.
However if you definitely don’t want to deal with a tableview, this component seems really cool, it's actually is meant to be added to a tableview, but I tested it with the twitter example and you can actually add it to a scrollview, so you don’t need a table view and it will work, give props the guy who made it. GSKStretchyHeaderView
Hope this helps, comment if you have any questions, good luck.
Use KVO to update the floating view's frame.
Here is the sample code written in Objective-C:
// ScrollView.m
// ScrollView is a subclass of UIScrollView
#interface ScrollView ()
#property (nonatomic, strong) UIView *floatingView;
#property (nonatomic) CGRect originalBorderFrame;
#property (nonatomic) CGFloat anchorHeight;
#end
#implementation ScrollView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.floatingView = [UIView new];
self.floatingView.backgroundColor = [UIColor colorWithRed:0.8211 green:0.5 blue:0.5 alpha:1.0];
self.floatingView.frame = CGRectMake(0, 150, frame.size.width, 20);
self.originalBorderFrame = self.floatingView.frame;
[self addSubview:self.floatingView];
self.anchorHeight = 44;
[self addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}
return self;
}
- (void)dealloc {
[self removeObserver:self forKeyPath:#"contentOffset"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:#"contentOffset"]) {
if (self.contentOffset.y > self.originalBorderFrame.origin.y-self.anchorHeight) {
self.floatingView.frame = CGRectOffset(self.originalBorderFrame, 0, self.contentOffset.y - (self.originalBorderFrame.origin.y-self.anchorHeight));
}
}
}
#end
Here is the capture:
I am implementing a view that is in some way similar to what happens in Messages app, so there is a view with UITextView attached to the bottom of the screen and there is also UITableView showing the main content. When it is tapped it slides up with the keyboard and when keyboard is dismissed it slides back to the bottom of the screen.
That part I have and it is working perfectly - I just subscribed to keyboard notifications - will hide and wil show.
The problem is that I have set keyboard dismiss mode on UITableView to interactive and I cannot capture changes to keyboard when it is panning.
The second problem is that this bar with uitextview is covering some part of uitableview. How to fix this? I still want the uitableview to be "under" this bar just like in messages app.
I am using AutoLayout in all places.
Any help will be appreciated!
============
EDIT1:
Here is some code:
View Hierarchy is as follows:
View
- UITableView (this one will contain "messages")
- UIView (this one will slide)
UITableView is has constraints to top, left, right and bottom of parent view so it fills whole screen.
UIView has constraints to left, right and bottom of parent view so it is glued to the bottom - I moved it by adjusting constant on constraint.
In ViewWillAppear method:
NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.DidShowNotification, OnKeyboardDidShowNotification);
NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.WillChangeFrameNotification, OnKeyboardDidShowNotification);
NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.WillHideNotification, OnKeyboardWillHideNotification);
And here are methods:
void OnKeyboardDidShowNotification (NSNotification notification)
{
AdjustViewToKeyboard (Ui.KeyboardHeightFromNotification (notification), notification);
}
void OnKeyboardWillHideNotification (NSNotification notification)
{
AdjustViewToKeyboard (0.0f, notification);
}
void AdjustViewToKeyboard (float offset, NSNotification notification = null)
{
commentEditViewBottomConstraint.Constant = -offset;
if (notification != null) {
UIView.BeginAnimations (null, IntPtr.Zero);
UIView.SetAnimationDuration (Ui.KeyboardAnimationDurationFromNotification (notification));
UIView.SetAnimationCurve ((UIViewAnimationCurve)Ui.KeyboardAnimationCurveFromNotification (notification));
UIView.SetAnimationBeginsFromCurrentState (true);
}
View.LayoutIfNeeded ();
commentEditView.LayoutIfNeeded ();
var insets = commentsListView.ContentInset;
insets.Bottom = offset;
commentsListView.ContentInset = insets;
if (notification != null) {
UIView.CommitAnimations ();
}
}
I'd recommend you to override -inputAccessoryView property of your view controller and have your editable UITextView as its subview.
Also, don't forget to override -canBecomeFirstResponder method to return YES.
- (BOOL)canBecomeFirstResponder
{
if (!RUNNING_ON_IOS7 && !RUNNING_ON_IPAD)
{
//Workaround for iOS6-specific bug
return !(self.viewDisappearing) && (!self.viewAppearing);
}
return !(self.viewDisappearing);
}
With this approach system manages everything.
There are also some workarounds you must know about: for UISplitViewController (UISplitViewController detail-only inputAccessoryView), for deallocation bugs (UIViewController with inputAccessoryView is not deallocated) and so on.
This solution is based on a lot of different answers on SO. It have a lot of benefits:
Compose bar stays on bottom when keyboard is hidden
Compose bas follows keyboard while interactive gesture on UITableView
UITableViewCells are going from bottom to top, like in Messages app
Keyboard do not prevent to see all UITableViewCells
Should work for iOS6, iOS7 and iOS8
This code just works:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = // . . .
// . . .
cell.contentView.transform = CGAffineTransformMakeScale(1,-1);
cell.accessoryView.transform = CGAffineTransformMakeScale(1,-1);
return cell;
}
- (UIView *)inputAccessoryView {
return self.composeBar;
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.transform = CGAffineTransformMakeScale(1,-1);
// This code prevent bottom inset animation while appearing view
UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
newEdgeInsets.top = CGRectGetMaxY(self.navigationController.navigationBar.frame);
newEdgeInsets.bottom = self.view.bounds.size.height - self.composeBar.frame.origin.y;
self.tableView.contentInset = newEdgeInsets;
self.tableView.scrollIndicatorInsets = newEdgeInsets;
self.tableView.contentOffset = CGPointMake(0, -newEdgeInsets.bottom);
// This code need to be done if you added compose bar via IB
self.composeBar.delegate = self;
[self.composeBar removeFromSuperview];
[[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillChangeFrameNotification object:nil queue:nil usingBlock:^(NSNotification *note)
{
NSNumber *duration = note.userInfo[UIKeyboardAnimationDurationUserInfoKey];
NSNumber *options = note.userInfo[UIKeyboardAnimationCurveUserInfoKey];
CGRect beginFrame = [note.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect endFrame = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
newEdgeInsets.bottom = self.view.bounds.size.height - endFrame.origin.y;
CGPoint newContentOffset = self.tableView.contentOffset;
newContentOffset.y += endFrame.origin.y - beginFrame.origin.y;
[UIView animateWithDuration:duration.doubleValue
delay:0.0
options:options.integerValue << 16
animations:^{
self.tableView.contentInset = newEdgeInsets;
self.tableView.scrollIndicatorInsets = newEdgeInsets;
self.tableView.contentOffset = newContentOffset;
} completion:^(BOOL finished) {
;
}];
}];
}
Use for example pod 'PHFComposeBarView' compose bar:
#property (nonatomic, strong) IBOutlet PHFComposeBarView *composeBar;
And use this class for your table view:
#interface InverseTableView : UITableView
#end
#implementation InverseTableView
void swapCGFLoat(CGFloat *a, CGFloat *b) {
CGFloat tmp = *a;
*a = *b;
*b = tmp;
}
- (UIEdgeInsets)contentInset {
UIEdgeInsets insets = [super contentInset];
swapCGFLoat(&insets.top, &insets.bottom);
return insets;
}
- (void)setContentInset:(UIEdgeInsets)contentInset {
swapCGFLoat(&contentInset.top, &contentInset.bottom);
[super setContentInset:contentInset];
}
#end
If you would like keyboard to disappear by tapping on message:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.composeBar.textView resignFirstResponder];
}
Do not call this, this will hide composeBar at all:
[self resignFirstResponder];
UPDATE 2:
NEW SOLUTION for keyboard tracking works much better:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Compose view height growing tracking
[self.composeBar addObserver:self forKeyPath:#"frame" options:0 context:nil];
// iOS 7 keyboard tracking
[self.composeBar.superview addObserver:self forKeyPath:#"center" options:0 context:nil];
// iOS 8 keyboard tracking
[self.composeBar.superview addObserver:self forKeyPath:#"frame" options:0 context:nil];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self.composeBar removeObserver:self forKeyPath:#"frame"];
[self.composeBar.superview removeObserver:self forKeyPath:#"center"];
[self.composeBar.superview removeObserver:self forKeyPath:#"frame"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == self.composeBar.superview || object == self.composeBar)
{
// Get all values
CGPoint newContentOffset = self.tableView.contentOffset;
UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
UIEdgeInsets newScrollIndicartorInsets = self.tableView.scrollIndicatorInsets;
// Update values
CGFloat bottomInset = self.view.bounds.size.height - [self.composeBar convertPoint:CGPointZero toView:self.view].y;
CGFloat diff = newEdgeInsets.bottom - (bottomInset + 7);
newContentOffset.y += diff;
newEdgeInsets.bottom = bottomInset + 7;
newScrollIndicartorInsets.bottom = bottomInset;
// Set all values
if (diff < 0 || diff > 40)
self.tableView.contentOffset = CGPointMake(0, newContentOffset.y);
self.tableView.contentInset = newEdgeInsets;
self.tableView.scrollIndicatorInsets = newEdgeInsets;
}
}
OK, the interactive keyboard dismissal will send a notification with name UIKeyboardDidChangeFrameNotification.
This can be used to move the text view while the keyboard is being dismissed interactively.
You are already using this but you are sending it to the OnKeyboardDidShow method.
You need a third method called something like keyboardFramedDidChange. This works for the hide and the show.
For the second problem, you should have your vertical constraints like this...
|[theTableView][theTextView (==44)]|
This will tie the bottom of the tableview to the top of the text view.
This doesn't change how any of the animation works it will just make sure that the table view will show all of its contents whether the keyboard is visible or not.
Don't update the content insets of the table view. Use the constraints to make sure the frames do not overlap.
P.S. sort out your naming conventions. Method names start with a lowercase letter.
P.P.S. use block based animations.
I'd try to use an empty, zero-height inputAccessoryView. The trick is to glue your text field's bottom to it when the keyboard appears, so that they'd move together. When the keyboard is gone, you can destroy that constraint and stick to the bottom of the screen once again.
I made an open source lib for exactly this purpose. It works on iOS 7 and 8 and is set up to work as a cocoapod as well.
https://github.com/oseparovic/MessageComposerView
Here's a sample of what it looks like:
You can use a very basic init function as shown below to create it with screen width and default height e.g.:
self.messageComposerView = [[MessageComposerView alloc] init];
self.messageComposerView.delegate = self;
[self.view addSubview:self.messageComposerView];
There are several other initializers that are also available to allow you to customize the frame, keyboard offset and textview max height as well as some delegates to hook into frame changes and button clicks. See readme for more!
I have a view controller that can be popped with the new interactivePopGestureRecognizer. If there is a keyboard present and the swipe animation begins the keyboard does not move with the view. I have had a look at this question and implemented it like this in my view controller that gets dismissed
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.transitionCoordinator animateAlongsideTransitionInView:self.aTextInputView.keyboardSuperView animation:^(id<UIViewControllerTransitionCoordinatorContext> context) {
CGRect frame = self.aTextInputView.keyboardSuperView.frame;
frame.origin.x = self.view.frame.size.width;
self.aTextInputView.keyboardSuperView.frame = frame;
} completion:nil];
}
Now what I get when the view animates to disappear is the keyboard animates off the screen to the x point of 320 which makes sense as thats what I set it to, my question is how do I get the keyboard to animate with the swipe back?
Update
For any one that sees a weird animation when the view disappears you can get remove the keyboard by doing this.
[self.transitionCoordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context){
if (![context isCancelled]) {
[keyboardSuperview removeFromSuperview];
}
}];
You have a lot of custom code in your snippet, so correct me if I am wrong, but it seems you have incorrect self.aTextInputView.keyboardSuperView.
Double check that it is not nil. If it is, you forgot to add an inputAccessoryView.
Here is the full code snippet without any extensions:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
UIView *keyboardSuperview = self.textField.inputAccessoryView.superview;
[self.transitionCoordinator animateAlongsideTransitionInView:keyboardSuperview
animation:
^(id<UIViewControllerTransitionCoordinatorContext> context) {
CGRect keyboardFrame = keyboardSuperview.frame;
keyboardFrame.origin.x = self.view.bounds.size.width;
keyboardSuperview.frame = keyboardFrame;
}
completion:nil];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.textField.inputAccessoryView = [[UIView alloc] init];
}
Just found really simple solution for iOS8
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.aTextInputView resignFirstResponder];
}
I'm trying to change the navigation bar title with this statement:
[[self navigationItem] setTitle:myTitle];
It works fine in viewDidLoad and viewWillAppear, but it doesn't work anywhere else. Is what I am trying to do possible? It's a view controller I'm working with.
There is a very simple way to change the title for the current view controller.
self.title = #"New Title";
Calling this on a view controller that is in a navigation controller will result in the title shown in the navigation bar being updated with the new title.
This can be called at any time while the view controller is displayed.
There is no need to dig into the view controller's navigationItem for setting the title.
I know this is old, but I wanted to answer this for future users. I think the main issue that most answers don't cover is that it is important to set the title from the main thread, otherwise there are a handful of cases where it won't get set immediately. For Swift 3.0 you can use code like this:
DispatchQueue.main.async {
self.title = "Example string"
}
It is important to do all UI updates from the main thread. I'm not sure if the original user was setting the title via an asynchronous closure, but this should help anyone who is having difficulties.
It's actually the view controller's title property that the navigationItem will use. The answer above is the simplest and correct way to change the title in the navigation bar.
If you need to customize your navigation bar, the way you do this is to override it in your UIViewController subclass, NOT change it in viewDidAppear: or any other viewWill/Did methods
- (UINavigationItem*)navigationItem
{
UINavigationItem *navigationItem = [super navigationItem];
// customize it further than the default
return navigationItem;
}
In addition, you can have changes in your title update a custom titleView using KVO. You listen for changes in the UIViewController's title property and update the titleView property of your navigationItem. Check this out:
static void *kObjectStateObservingContext = &kObjectStateObservingContext; // outside your #implementation bit
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// other init stuff here...
[self addObserver: self forKeyPath:#"title" options:NSKeyValueObservingOptionNew context: kObjectStateObservingContext];
}
return self;
}
Don't forget to remove the observer:
- (void)dealloc
{
[self removeObserver: self forKeyPath:#"title"];
}
And now we revisit the navigationItem again, where you can create a custom UILabel that will update whenever we change the title, and after that we have to provide a handler implementation for when the title does change:
- (UINavigationItem*)navigationItem
{
UINavigationItem *navigationItem = [super navigationItem];
UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(roundf((320 - 230)/2), 0, 230, 44)];
titleLabel.font = [UIFont systemFontOfSize: 18];
titleLabel.textColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"topnav-background-text-gradient.png"]]; // you can even draw the font with a gradient!
titleLabel.shadowColor = [UIColor colorWithWhite: 1.0 alpha:0.59];
titleLabel.shadowOffset = CGSizeMake(0, 1.0);
titleLabel.text = self.title;
titleLabel.backgroundColor = [UIColor clearColor];
titleLabel.textAlignment = UITextAlignmentCenter;
navigationItem.titleView = titleLabel;
return navigationItem;
}
And finally, what to do when self.title actually changes:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ( context == kObjectStateObservingContext ) {
// here we update the text of the titleView
if ([self.navigationItem.titleView isKindOfClass:[UILabel class]]) {
[(UILabel*)self.navigationItem.titleView setText: self.title];
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
And there you go! Obviously a bit more code than you might have thought, so if you're not currently, I recommend to anyone building iOS Apps that you always create a base class view controller for your application, (MyBaseViewController : UIViewController) then subclass that for any other view controllers in your app.
You can set up the the title in viewDidLoad, because the view is not yet visible but instaciated. Same story for viewWillAppear. After the UIElement is visible, you cant change the title without a redraw.
#reid55 you can set navigation title in below method also:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
[[self navigationItem] setTitle:myTitle];
}
return self;
}