I am upgrading my application to iOS 11 and I am seeing some problems with the navigation bar, part of my problems have already asked questions here, so I won't mention them in this question.
The particular problem in question is that the navigation bar's bar button items are spaced differently. My main left and right bar button items are closer to the screen's horizontal center now and I can't move them near to the screen edges. In the past I have used a custom UIButton subclass and created bar button items with custom view. The alignment solution were alignmentRectInsets and contentEdgeInsets, now I couldn't manage to produce the expected results using this approach.
Edit:
I have retested with iOS 11 beta 2 and the issue remains.
Edit 2:
I Have retested with iOS beta 3 and the issue remains.
Now in iOS 11 you can manage UINavigationBarContentView to adjust left and right constraints, and UIStackView to adjust buttons (or other elements).
This is my solution for a navigation bar with items on left and right. Also it fixes if you have several buttons together in one side.
- (void) updateNavigationBar {
for(UIView *view in self.navigationBar.subviews) {
if ([NSStringFromClass([view class]) containsString:#"ContentView"]) {
// Adjust left and right constraints of the content view
for(NSLayoutConstraint *ctr in view.constraints) {
if(ctr.firstAttribute == NSLayoutAttributeLeading || ctr.secondAttribute == NSLayoutAttributeLeading) {
ctr.constant = 0.f;
} else if(ctr.firstAttribute == NSLayoutAttributeTrailing || ctr.secondAttribute == NSLayoutAttributeTrailing) {
ctr.constant = 0.f;
}
}
// Adjust constraints between items in stack view
for(UIView *subview in view.subviews) {
if([subview isKindOfClass:[UIStackView class]]) {
for(NSLayoutConstraint *ctr in subview.constraints) {
if(ctr.firstAttribute == NSLayoutAttributeWidth || ctr.secondAttribute == NSLayoutAttributeWidth) {
ctr.constant = 0.f;
}
}
}
}
}
}
}
As you see it is not necessary to add more constraints, as other people have done. There are already defined constraints so they can be changed.
After about two days, here is the simplest and safest solution I could come up with. This solution only works with custom view bar button items, the code for which is included. It is important to note that the left and right margins on the navigation bar have not changed from iOS10 to iOS11 - they are still 16px. Such a large margin makes it difficult to have a sufficiently large click region.
Bar buttons are now layed out with UIStackView's. The prior method of shifting those buttons closer to the margin involved using negative fixed spaces which these stack views cannot handle.
Subclass UINavigationBar
FWNavigationBar.h
#interface FWNavigationBar : UINavigationBar
#end
FWNavigationBar.m
#import "FWNavigationBar.h"
#implementation FWNavigationBar
- (void)layoutSubviews {
[super layoutSubviews];
if (#available(iOS 11, *)) {
self.layoutMargins = UIEdgeInsetsZero;
for (UIView *subview in self.subviews) {
if ([NSStringFromClass([subview class]) containsString:#"ContentView"]) {
subview.layoutMargins = UIEdgeInsetsZero;
}
}
}
}
#end
Using the UINavigationController
#import "FWNavigationBar.h"
UINavigationController *controller = [UINavigationController initWithNavigationBarClass:[FWNavigationBar class] toolbarClass:nil];
[controller setViewControllers:#[rootViewController] animated:NO];
Creating a UIBarButton
Place this code either in a UIBarButton category or in the file you plan on using the bar button which will return an identical looking bar button item using a UIButton.
+ (UIBarButtonItem *)barButtonWithImage:(UIImage *)image target:(id)target action:(SEL)action {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
//Note: Only iOS 11 and up supports constraints on custom bar button views
button.frame = CGRectMake(0, 0, image.size.width, image.size.height);
button.tintColor = [UIColor lightGrayColor];//Adjust the highlight color
[button setImage:image forState:UIControlStateNormal];
//Tint color only applies to this image
[button setImage:[image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateHighlighted];
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
return [[UIBarButtonItem alloc] initWithCustomView:button];
}
Setting the bar button in your controller
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.leftBarButtonItem = [UIBarButtonItem barButtonWithImage:[UIImage imageNamed:#"your_button_image"] target:self action:#selector(leftButtonPressed)];
}
Lastly, I would recommend leaving the left and right margins at zero and just adjusting the position of the button within your image. This will allow for you to take advantage of the full clickable region up to the edge of the screen. The same goes for the height of your image - make sure the height is 44 points.
There is good article on this : http://www.matrixprojects.net/p/uibarbuttonitem-ios11/
Using this we can at least push rightbarbuttonitems to right till it leaves 8 pixel margin from trailing of the UINavigationBar.
Explained really well.
I noticed a similar problem.
I reported an Apple Radar for a similar problem we noticed, #32674764 if you want to refer to it, if you create a Radar.
I also created a thread in Apple's forum, but no feedback yet:
https://forums.developer.apple.com/message/234654
I stumbled upon this: UINavigationItem-Margin.
It works like a charm.
Taken from this answer: BarButtons now use Autolayout and thus require constraints.
if #available(iOS 9.0, *) {
cButton.widthAnchor.constraint(equalToConstant: customViewButton.width).isActive = true
cButton.heightAnchor.constraint(equalToConstant: customViewButton.height).isActive = true
}
Objective C
if (#available(iOS 9, *)) {
[cButton.widthAnchor constraintEqualToConstant: standardButtonSize.width].active = YES;
[cButton.heightAnchor constraintEqualToConstant: standardButtonSize.height].active = YES;
}
Solution 1:
In light of Apple's response that this is expected behavior, we worked around the problem by removing the toolbar and adding a custom view.
I realize that this may not be possible in all cases, but the regular UIView is much easier to customize to the app's appearance than the toolbar and navigation bar where Apple has control of the button positioning.
Instead of setting our custom button as the custom view of the ui bar button object, it was we set it as a subview of the blank ui buttons in the custom view.
Doing this we were able to return to the same look of our ios 10 app
Solution 2:
A bit messy, we wrapped our custom view button in an outer UIButton so the frame location can be set. This does make the outer left edge of the button un-tappable unfortunately, but corrects the look of the button position. See example:
UIButton* outerButton = [UIButton new]; //the wrapper button
BorderedButton* button = [self initBorderedButton]; //the custom button
[button setTitle:label forState:UIControlStateNormal];
[button setFrame:CGRectMake(-10, 0, [label length] * 4 + 35, 30)];
[button addTarget:controller action:#selector(popCurrentViewController) forControlEvents:UIControlEventTouchUpInside];
[outerButton addSubview:button]; //add custom button to outer wrapper button
[outerButton setFrameWidth:button.frameWidth]; //make sure title gives button appropriate space
controller.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:outerButton]; //add wrapper button to the navigation bar
controller.navigationItem.hidesBackButton = YES;
Using that method we keep our navigation bar and can position the button to where it is needed.
Edit: We found that solution 2 does not work on ios 10, this probably will affect only the tiny percent of developers forced to be backward compatible.
Solution 3
What we were really more concerned about with the inward crowding of the buttons was the fact that the title of the navigation bar ran into the custom left button, the size of the margins was less important and used as a tool to make space. The solution is to simply add a spacer item to the left items.
UIBarButtonItem* backButton = [[UIBarButtonItem alloc] initWithCustomView:button];
UIBarButtonItem* spacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
controller.navigationItem.leftBarButtonItems = [NSArray arrayWithObjects:backButton, spacer, nil];
Swift version of thedeveloper3214's custom UINavigationBar subclass' layoutSubviews method(https://stackoverflow.com/a/46660888/4189037):
override func layoutSubviews() {
super.layoutSubviews();
if #available(iOS 11, *){
self.layoutMargins = UIEdgeInsets()
for subview in self.subviews {
if String(describing: subview.classForCoder).contains("ContentView") {
let oldEdges = subview.layoutMargins
subview.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: oldEdges.right)
}
}
}
}
I had the same issue. I had three buttons as right barbutton items on the navigation bar stack view. So, I have updated the insets of the subviews of navigation bar.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
for view in (self.navigationController?.navigationBar.subviews)! {
view.layoutMargins = UIEdgeInsets.init(top: 0, left: 11, bottom: 0, right: 11)
}
}
Here 11 is the space which I needed. It can be anything as per your requirement.
Also, if you want the buttons with 0 insets, replace view.layoutMargins = UIEdgeInsets.init(top: 0, left: 11, bottom: 0, right: 11) with view.layoutMargins = UIEdgeInsets.zero
Another answer may be help.
My solution : It works in ios 9 - 12. You should call fixNavigationItemsMargin(margin:) in function viewDidAppear(_ animated: Bool) and viewDidLayoutSubviews(). fixNavigationItemsMargin(margin:) would modify the UINavigationController stack.
you could call fixNavigationItemsMargin(margin:) in BaseNavigationController ,do the common work. And call fixNavigationItemsMargin(margin:) in UIViewController do precise layout.
// do common initilizer
class BaseNavigationController: UINavigationController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
fixNavigationItemsMargin()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fixNavigationItemsMargin()
}
}
// do precise layout
class ViewController: UIViewController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
navigationController?.fixNavigationItemsMargin(40)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.fixNavigationItemsMargin(40)
}
}
extension UINavigationController {
func fixNavigationItemsMargin(_ margin: CGFloat = 8) {
let systemMajorVersion = ProcessInfo.processInfo.operatingSystemVersion.majorVersion
if systemMajorVersion >= 11 {
// iOS >= 11
guard let contentView = navigationBar.subviews
.first(
where: { sub in
String(describing: sub).contains("ContentView")
}) else { return }
// refer to: https://www.matrixprojects.net/p/uibarbuttonitem-ios11/
// if rightBarButtonItems has not any custom views, then margin would be 8(320|375)/12(414)
// should use customView
let needAdjustRightItems: Bool
if let currentVC = viewControllers.last,
let rightItems = currentVC.navigationItem.rightBarButtonItems,
rightItems.count > 0,
rightItems.filter({ $0.customView != nil }).count > 0 {
needAdjustRightItems = true
} else {
print("Use 8(320|375)/12(414), if need precious margin ,use UIBarButtonItem(customView:)!!!")
needAdjustRightItems = false
}
let needAdjustLeftItems: Bool
if let currentVC = viewControllers.last,
let leftItems = currentVC.navigationItem.leftBarButtonItems,
leftItems.count > 0,
leftItems.filter({ $0.customView != nil }).count > 0 {
needAdjustLeftItems = true
} else {
print("Use 8(320|375)/12(414), if need precious margin ,use UIBarButtonItem(customView:)!!!")
needAdjustLeftItems = false
}
let layoutMargins: UIEdgeInsets
if #available(iOS 11.0, *) {
let directionInsets = contentView.directionalLayoutMargins
layoutMargins = UIEdgeInsets(
top: directionInsets.top,
left: directionInsets.leading,
bottom: directionInsets.bottom,
right: directionInsets.trailing)
} else {
layoutMargins = contentView.layoutMargins
}
contentView.constraints.forEach(
{ cst in
// iOS 11 the distance between rightest item and NavigationBar should be margin
// rightStackView trailing space is -margin / 2
// rightestItem trailing to rightStackView trailing is -margin / 2
let rightConstant = -margin / 2
switch (cst.firstAttribute, cst.secondAttribute) {
case (.leading, .leading), (.trailing, .trailing):
if let stackView = cst.firstItem as? UIStackView,
stackView.frame.minX < navigationBar.frame.midX {
// is leftItems
if needAdjustLeftItems {
cst.constant = margin - layoutMargins.left
}
} else if let layoutGuide = cst.firstItem as? UILayoutGuide,
layoutGuide.layoutFrame.minX < navigationBar.frame.midX {
// is leftItems
if needAdjustLeftItems {
cst.constant = margin - layoutMargins.left
}
}
if let stackView = cst.firstItem as? UIStackView,
stackView.frame.maxX > navigationBar.frame.midX {
// is rightItems
if needAdjustRightItems {
cst.constant = rightConstant
}
} else if let layoutGuide = cst.firstItem as? UILayoutGuide,
layoutGuide.layoutFrame.maxX > navigationBar.frame.midX {
// is rightItems
if needAdjustRightItems {
cst.constant = rightConstant
}
}
default: break
}
})
// ensure items space == 8, minispcae
contentView.subviews.forEach(
{ subsub in
guard subsub is UIStackView else { return }
subsub.constraints.forEach(
{ cst in
guard cst.firstAttribute == .width
|| cst.secondAttribute == .width
else { return }
cst.constant = 0
})
})
} else {
// iOS < 11
let versionItemsCount: Int
if systemMajorVersion == 10 {
// iOS 10 navigationItem.rightBarButtonItems == 0
// space = 16(320|375) / 20(414)
// should adjust margin
versionItemsCount = 0
} else {
// iOS 9 navigationItem.rightBarButtonItems == 0
// space = 8(320|375) / 12(414)
// should not adjust margin
versionItemsCount = 1
}
let spaceProducer = { () -> UIBarButtonItem in
let spaceItem = UIBarButtonItem(
barButtonSystemItem: .fixedSpace,
target: nil,
action: nil)
spaceItem.width = margin - 16
return spaceItem
}
if let currentVC = viewControllers.last,
var rightItems = currentVC.navigationItem.rightBarButtonItems,
rightItems.count > versionItemsCount,
let first = rightItems.first {
// ensure the first BarButtonItem is NOT fixedSpace
if first.title == nil && first.image == nil && first.customView == nil {
print("rightBarButtonItems SPACE SETTED!!! SPACE: ", abs(first.width))
} else {
rightItems.insert(spaceProducer(), at: 0)
// arranged right -> left
currentVC.navigationItem.rightBarButtonItems = rightItems
}
}
if let currentVC = viewControllers.last,
var leftItems = currentVC.navigationItem.leftBarButtonItems,
leftItems.count > versionItemsCount,
let first = leftItems.first {
if first.title == nil && first.image == nil && first.customView == nil {
print("leftBarButtonItems SPACE SETTED!!! SPACE: ", abs(first.width))
} else {
leftItems.insert(spaceProducer(), at: 0)
// arranged left -> right
currentVC.navigationItem.leftBarButtonItems = leftItems
}
}
}
}
}
I momently developing a application with a tab bar and some navigation view controllers in Storyboard using iOS 6 and Xcode 4.5
Usually the app should support all interface orientations but I have two views that only should support portrait mode.
So I added the following code to the view controllers:
-(BOOL)shouldAutorotate
{
return NO;
}
-(NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationPortrait;
}
On a other app I developed without storyboard and navigation view controller on iOS 6 it works but her NOT! :/
I hope someone can help, because I found some other post that where not helpful...
With best regards from Germany
Laurenz
EDIT:
I also tried - Doesn't work! :
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
As far as I can tell, this problem arises because the UITabBarController and UINavigationController are returning their own default values for -(BOOL)shouldAutorotate and -(NSUInteger)supportedInterfaceOrientations.
One solution is to extend both of those classes via categories (or just subclass), in order to return the appropriate values from your own implementations of these methods in your view controller. This is what worked for me (you can just drop this into your App Delegate):
#implementation UITabBarController(AutorotationFromSelectedView)
- (BOOL)shouldAutorotate {
if (self.selectedViewController) {
return [self.selectedViewController shouldAutorotate];
} else {
return YES;
}
}
- (NSUInteger)supportedInterfaceOrientations {
if (self.selectedViewController) {
return [self.selectedViewController supportedInterfaceOrientations];
} else {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
}
#end
#implementation UINavigationController(AutorotationFromVisibleView)
- (BOOL)shouldAutorotate {
if (self.visibleViewController) {
return [self.visibleViewController shouldAutorotate];
} else {
return YES;
}
}
- (NSUInteger)supportedInterfaceOrientations {
if (self.visibleViewController) {
return [self.visibleViewController supportedInterfaceOrientations];
} else {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
}
#end
By default, all your view controllers will continue to autorotate. In the two View Controllers that should only support portrait mode, implement the following:
-(BOOL)shouldAutorotate {
return NO;
}
-(NSUInteger)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskPortrait;
}
Excellent answer by Jonathan.
I modified his code little bit to handle navigation controller in a single snippet.
- (BOOL)shouldAutorotate {
if (self.selectedViewController) {
if ([self.selectedViewController isKindOfClass:[UINavigationController class]]) {
return [[[(UINavigationController*)self.selectedViewController viewControllers] lastObject] shouldAutorotate];
}
return [self.selectedViewController shouldAutorotate];
} else {
return YES;
}
}
- (NSUInteger)supportedInterfaceOrientations {
if (self.selectedViewController) {
if ([self.selectedViewController isKindOfClass:[UINavigationController class]]) {
return [[[(UINavigationController*)self.selectedViewController viewControllers] lastObject] supportedInterfaceOrientations];
}
return [self.selectedViewController supportedInterfaceOrientations];
} else {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
}
Id like to be able to track if the user shared by facebook, twitter, etc, but it seems there's no way to know which method was selected. Is there?
You can use Activity Types in setCompletionHandler
UIActivityViewController *activityVC = [[UIActivityViewController alloc]initWithActivityItems:activityItems applicationActivities:nil];
[activityVC setCompletionHandler:^(NSString *activityType, BOOL completed) {
if([activityType isEqualToString: UIActivityTypeMail]){
NSLog(#"Mail");
}
if([activityType isEqualToString: UIActivityTypePostToFacebook]){
NSLog(#"Facebook");
}
}];
[self presentViewController:activityVC animated:TRUE completion:nil];
Built-in activity types for which the system has built-in support for.
NSString *const UIActivityTypePostToFacebook;
NSString *const UIActivityTypePostToTwitter;
NSString *const UIActivityTypePostToWeibo;
NSString *const UIActivityTypeMessage;
NSString *const UIActivityTypeMail;
NSString *const UIActivityTypePrint;
NSString *const UIActivityTypeCopyToPasteboard;
NSString *const UIActivityTypeAssignToContact;
NSString *const UIActivityTypeSaveToCameraRoll;
Edited for iOS 8
Please note that this will generate a compiler warning in iOS 8, you need to use the setCompletionWithItemsHandler method instead of the setCompletionHandler method.
Replace:
[activityVC setCompletionHandler:^(NSString *activityType, BOOL completed) {
with:
[activityVC setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
In Swift 4:
activityViewController.completionWithItemsHandler = { activity, success, items, error in
if !success{
print("cancelled")
return
}
if activity == .postToTwitter {
print("twitter")
}
if activity == .mail {
print("mail")
}
}
swift 3
let activityViewController:UIActivityViewController = UIActivityViewController(activityItems: [screenCapture], applicationActivities: nil)
activityViewController.excludedActivityTypes = [UIActivityType.print, UIActivityType.postToWeibo, UIActivityType.copyToPasteboard, UIActivityType.addToReadingList, UIActivityType.postToVimeo]
activityViewController.completionWithItemsHandler = { activity, success, items, error in
if !success{
print("cancelled")
return
}
if activity == UIActivityType.postToFacebook {
print("facebook")
}
if activity == UIActivityType.mail {
print("mail")
}
}
self.present(activityViewController, animated: true, completion: nil)
You can use this lib https://github.com/rdougan/RDActivityViewController to identify which activity has been touched and return custom data by activity just by implementing its protocol.
Like This:
- (NSArray *)activityViewController:(NSArray *)activityViewController itemsForActivityType:(NSString *)activityType
{
if ([activityType isEqualToString:UIActivityTypePostToTwitter]) {
return #[#"Twitter text"];
} else if ([activityType isEqualToString:UIActivityTypeMessage]) {
return #[#"Message text"];
} else if ([activityType isEqualToString:UIActivityTypeMail]) {
return #[#"Mail text"];
} else if ([activityType isEqualToString:UIActivityTypePostToFacebook]) {
return #[#"Facebook text"];
} else {
return #[#"Default text"];
}
}
I hope I have helped you!
Hi, I have already uploaded a sqlite3 file to icloud. Now I want to download this file to my APP's Document folder. After I pressed the download button, it will show the success information. But in fact, there is no sqlite file in the documents folder or maybe the file is broken. Could someone help me?
-(IBAction)downloadPressed:(id)sender
{
if(![self downloadFileIfNotAvailable:self.icloudURL])
{
[self displayAlert:#"Warning!!" withmessage:#"failed to restore"];
}else{
[self displayAlert:#"Congratulations" withmessage:#"restore sccuessfully"];
}
}
-(BOOL)downloadFileIfNotAvailable:(NSURL *)file
{
NSNumber* isIniCloud = nil;
NSError* err;
if ([file getResourceValue:&isIniCloud forKey:NSURLIsUbiquitousItemKey error:nil]) {
// If the item is in iCloud, see if it is downloaded.
if ([isIniCloud boolValue]) {
NSFileManager* fm = [NSFileManager defaultManager];
if([fm removeItemAtPath:self.filePath error:&err])
{
if (![fm startDownloadingUbiquitousItemAtURL:file error:nil]) {
return NO;
}else{
return YES;
}
}
}
return NO;
}
return NO;
}
I've to integrated the FB SDK 3.1 in my ios application.
I successfully share an image on my wall, but i'm not able to set the "via [appname]" (it takes by default "via ios"). I've already put in my info.plist file the appid value.
I'm also not able to make the sharing work for ios5 (it replies with Error 400).
Following there's the code i use:
- (void)viewDidLoad{
appDelegate = [[UIApplication sharedApplication]delegate];
if (!appDelegate.session.isOpen) {
appDelegate.session = [[FBSession alloc] init];
if (appDelegate.session.state == FBSessionStateCreatedTokenLoaded) {
[appDelegate.session openWithCompletionHandler:^(FBSession *session,
FBSessionState status,
NSError *error) {
}];
}
}
and when i have to share, i call this method:
- (IBAction)fbShare:(id)sender {
if (appDelegate.session.isOpen) {
BOOL displayedNativeDialog = [FBNativeDialogs presentShareDialogModallyFrom:self
initialText:textView.text
image:img
url:nil
handler:nil];
if (!displayedNativeDialog) {
[self performPublishAction:^{
[FBRequestConnection startForUploadPhoto:img
completionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
}];
}];}
} else {
if (appDelegate.session.state != FBSessionStateCreated) {
appDelegate.session = [[FBSession alloc] init];
}
[appDelegate.session openWithCompletionHandler:^(FBSession *session,
FBSessionState status,
NSError *error) {
}];}
}
- (void) performPublishAction:(void (^)(void)) action {
if ([FBSession.activeSession.permissions indexOfObject:#"publish_actions"] == NSNotFound) {
[FBSession.activeSession reauthorizeWithPublishPermissions:[NSArray arrayWithObject:#"publish_actions"]
defaultAudience:FBSessionDefaultAudienceFriends
completionHandler:^(FBSession *session, NSError *error) {
if (!error) {
action();
}
else{
}
}];
} else {
action();
}}
Any hint?
Thanks a lot,
erica
I successfully share an image on my wall, but i'm not able to set the "via [appname]" (it takes by default "via ios"). I've already put in my info.plist file the appid value.
Most likely you've not set the Facebook app name. I'd recommend looking it at https://developers.facebook.com/apps admin panel