Size a UIToolbar to fit its items? - objective-c

I'm trying to figure out a clean way to make a UIToolbar only as wide as it needs to be to fit the items that it contains. Is there a way to either:
Configure the UIToolbar to adjust its width automatically as its items are changed?
Programatically determine the minimum width required by the items, which can then be used to set the UIToolbar's frame?
I haven't been able to figure this out due to the spacing between the items and the fact that UIBarButtonItems are not UIView subclasses.

After trying some suggestions from other answers that did not work unless I used custom views, or unless everything was loaded, I finally arrived at a way to set the toolbar width based on its items:
//Add bar items to toolbar first.
UIView* v = toolbar.subviews.lastObject;
float newWidth = v.frame.origin.x + v.frame.size.width;
//Set toolbar width
You'll need to override UIToolbar -setItems: or otherwise detect changed buttons to autoresize.
I have included this feature in my refactoring library, es_ios_utils, to set a navigation item's right item with multiple buttons. In the preceding link, see UIToolbar +toolbarWithItems: and UINavigationItem -setRightBarButtonItems.

I just checked the documentation and seems like UIBarButtonItems, even though they are not UIView subclasses, they have an attribute width. I didn't try it myself, but I think you could sum each item's width, including the flexible item, and get the width for your toolbar.
Hope it helps! ;)

Swift 3, 4 update
I have a toolbar with barButtonItem initiated with image. All I need to do is to calculate the total width of the images plus the intervals (margin.)
Margin is by default 11 in width. The code will be:
let width: CGFloat = YourBarButtonItemList.reduce(0) {sofar, new in
if let image = new.image {
return sofar + image.size.width + 11
} else {
return sofar + ADefaultValueIfThisDoesntWork + 11
}
}

Related

UICollectionView orientation (Top to bottom v. Left To Right)

I'd like to get this collection view layout:
The collection view class is also its delegate/data source and the layout delegate. The scrolling direction is horizontal. There are two horizontal sections. The first one with header view (orange). The cells with simple border lines contain labels.
The issue here is the collection orientation is wrong 2 (top to bottom instead of left to right). Is there any explicit property that can control this orientation/cell composition?
Another question is related to the cell borders. Is there any elegant way how the border color/width can be set up using auto layout (subclassing)?
Thank you.
UPDATE:
The key value to play with might be ((UICollectionViewFlowLayout *)self.collectionViewLayout).minimumLineSpacing and assigning a big number to it. But it results in one long row of cells so that the sections are together in one row. It is weird component design when a navigation mode (the scrolling direction) dictates the collection layout.
If you subclass UICollectionViewFlowLayout you will be able to layout the cells any way you like. I use a custom flow layout to display a horizontal scrolling set of cells based on an event start time and duration:
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
dataSource = self.collectionView.dataSource;
event = [dataSource eventAtIndexPath:indexPath];
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attributes.frame = [Calculate a frame for the event from its data];
return attributes;
}
Similarly use layoutAttributesForSupplementaryViewOfKind to define your header position.
I found this reference useful Custom Collection View Layouts.

iOS7 UILabel to adopt same tintColor as window

I know that for elements of classes UIButton and UIBarButtonItem they automatically assume window.tintColor as the main colour, which results of an immediate change in case I set a new tintColor to window at any time in the app.
I was wondering if there is any way to make UILabel elements to follow the same pattern, where once created they automatically assumer its default colour as window.tintColor and if changing window.tintColor at any time within my app runtime would also result in changing the UILabel tintColour automatically?
I hope that makes sense.
UILabels are a subclass of UIView, so when you are running in iOS 7 they will have a tintColor property and will inherit that color from their parent view if their tint color is set to nil (which is default).
From Apple's Documentation:
By default, a view’s tint color is nil, which means that the view uses its parent’s tint. It also means that when you ask a view for its tint color, it always returns a color value, even if you haven’t set one.
However, you also ask "if changing window.tintColor at any time within my app runtime would also result in changing the UILabel tintColour automatically?" Apple advises you to not change the tint color when items are on screen:
In general, it’s best to change a view’s tint color while the view is offscreen.
I would guess this is because there is no guarentee that all the various UI elements will detect the tintColor change and update their visible views. However, the UIView documentation suggests a workaround if you want to update tintColor while your UILables are on screen:
To refresh subview rendering when this property changes, override the tintColorDidChange method.
So just make sure to call tintColorDidChange on any views currently on screen whose tint color should update when the tintColor of their parent view changes.
But why don't your UILabel's update their color?
So the above helps you set and update your various tintColor's, but you're not seeing any effect - why?
Well that has to do with what Apple designed Tint to indicate. From the Human Interface Guidelines:
color gives users a strong visual indicator of interactivity
Apple got rid of borders and gradients around interactive elements and replaced them with color - specifically tintColor. The whole idea behind tintColor is that things users can tap on get it, and things they can't tap on don't.
UILabel is not an interactive element - it is a text description - and so Apple will let you set a tintColor on it (as any UIView has a tintColor) but setting that tintColor will not change how it is drawn.
So what should you do? First, be aware that making more than just buttons take on the tint color could be a poor UI choice for your app - iOS 7 users and Apple app reviewers both will be expecting those rules to be followed.
So are you forced to keep your UILabel free from color then?
No - especially if you do things "right". Apple explains:
In a content area, add a button border or background only if necessary. Buttons in bars, action sheets, and alerts don’t need borders because users know that most of the items in these areas are interactive. In a content area, on the other hand, a button might need a border or a background to distinguish it from the rest of the content.
I would suggest you consider the UI of your app. If you really want your non-intereactive elements to have the same tintColor as your interactive elements, then make sure you use something more, like a border or background color, so your users (and Apple app reviewers) know what is clickable and what is not.
As to how you should update the colors, you can either manually set the textColor property to be whatever color you want, or you'll need to make your own subclass of UILabel that overrides - (void)tintColorDidChange to update the textColor property when notifications are sent out - allowing you to have a UILabel whose text updates to match the tintColor of its parent.
I hope this helps!
Found above explanation to be helpful - particularly the pointer to tintColorDidChange(). However, getting it to work out right didn't work at first, but finally came up with a code example that does. Basically, I had a tableView with cells containing images and labels. The images would update with a change in tintColor, but not the labels - which didn't look right. Better for either both to change or neither change. The code below is for the cell in the tableView. This was written with Swift 4.2.
//
// ImageLabelCell.swift
// -- provides an example of changing "tintColor" of UILabel to behave like other elements.
// -- made problem a bit more complex by having a "selected" cell be in inverse colors - so also deal with background.
//
import UIKit
enum CellTypes: Int, CaseIterable { case cell1, cell2, cell3, cell4
// This type provides a demonstration of a way to associate different titles and images to populate the UILabel and UIImageView in the cell.
var title: String {
return "\(self)".uppercased()
}
var imageName: String {
return "\(self)"
}
}
class ImageLabelCell: UITableViewCell {
#IBOutlet weak var lblString: UILabel?
#IBOutlet weak var imgView: UIImageView!
fileprivate var type = CellTypes.cell1
fileprivate var cellSelected = false
}
extension ImageLabelCell {
func getFgBgColors() -> (UIColor, UIColor) { // get foreground, background colors
let white = UIColor.white
var useTint = UIColor.blue // Use your app color here. Just ensures useTint is not nil.
if let tint = tintColor {
useTint = tint
}
if cellSelected {
return (white, useTint) // Selected cell is white on colored background
} else {
return (useTint, white) // Unselected cell is colored on white background
}
}
func configureCell(type: CellTypes, andSelected selected: Bool) {
// Save properties we may use again later
self.type = type
self.cellSelected = selected
// Set label text and image
lblString?.text = type.title
imgView.image = UIImage(named: type.imageName)
// Set colors
let (fgColor, bgColor) = getFgBgColors()
imgView.tintColor = fgColor
self.contentView.backgroundColor = bgColor
lblString?.textColor = fgColor
}
override func tintColorDidChange() {
// This gets called when the program tint color changes to gray (or back again) such as when popups appear/disappear.
let (fgColor, bgColor) = getFgBgColors()
// NOTE: need to set text color and background color. Imageview can take care of itself.
lblString?.textColor = fgColor
self.contentView.backgroundColor = bgColor
}
}
Simple solution may work for some usecases - use let fakeButton = UIButton(type: .system) which automatically adjust to window.tintColor.
Important thing is to set .system type which automatically match the window.tintColor.
You may also set fakeButton.isEnabled = false to prevent user interacting with the button. However, we didn't set target-action so the button is already the fake one.

Interface Builder Autolayout and Resizing

So I have two questions about Interface Builder for Xcode:
I'm trying to build an interface with a label on the left and a text field on the right, which have constant spacing between them, but I want the text field to expand horizontally when I resize the window horizontally to keep the spacing. I've added a restraint that keeps the spacing between them equal, but when I move the window, it resizes the label box rather than the text field. I tried pinning the width of the label, but then it stops me from resizing the window.
Is there any way to resize multiple items at the same time? Like if I have 8 labels vertically and I want to size them all to each be an 8th of the window space, how can I do that without just eyeballing it? It would be easy if you could highlight all of them and drag one corner to resize them all, but it wont let me do that.
For your first problem you should uncheck AutoLayout from Utilities Panel -> File Inspector and see what happens. Concerning the second you can create how many labels you want with your desired size directly from code as follows :
Example for 10 labels:
for (int i=0; i<30; i++)
{
UILabel *label = [[UILabel alloc] initWithFrame:anyFrame];
[yourView addSubView:label];
[label release]; //if not using ARC
}

Change size of viewing area after changing size of UINavigationBar and UITabBar

I'm currently working on my very first app and I've changed the size of the UINavigationBar and UITabBar and now I have extra space black space in the general viewing area (etc. ViewController, DetailViewController). How can I change the viewing area to accommodate for this new size?
I've pasted how I'm currently setting the new size for both UINavigationBar and the UITabBar.
/* Get the screenarea of the device */
CGRect screenArea = [[UIScreen mainScreen] applicationFrame];
/* Define the size of the navigation bar */
CGRect viewHeaderbBarFrame = navigationBar.frame;
viewHeaderbBarFrame.origin.y = screenArea.origin.y;
viewHeaderbBarFrame.origin.x = screenArea.origin.x;
viewHeaderbBarFrame.size.height = 44;
viewHeaderbBarFrame.size.width = screenArea.size.width;
navigationBar.frame = viewHeaderbBarFrame;
/* Define the size of the footer bar */
CGRect viewTabBarFrame = tabBar.frame;
viewTabBarFrame.origin.y = screenArea.size.height - 26;
viewTabBarFrame.origin.x = screenArea.origin.x;
viewTabBarFrame.size.height = 46;
viewTabBarFrame.size.width = screenArea.size.width;
tabBar.frame = viewTabBarFrame;
Thanks.
basically by shrinking your tabbar and navbar, you need your view to expand. I'm assuming that you are using a tabbar for primary navigation.
Figure out the proper size and adjust your view like this:
[[self.tabBarController.view.subviews objectAtIndex:0] setFrame:CGRectMake(newX,newY,newWidth,newHeight)];
in the buyer beware category, if your goal is the app store, you would be wise to review the guidelines on modifying those elements. I'm not sure of all the specific points, but I think apple frowns making changes like that to plainly displayed core navigation elements.
Also, this approach works well for the current screen dimensions, but may not work if the rumors are true and the next phone has a bigger screen.
be well.
You should never have to mess with the view height in this way. Views are pushed onto the nav bar. Their size should be set to fit a 320 x 480 screen minus tab, nav, and title bars. And you should set the AutoResize width/height as flexible and the left/rigth/top/bottom to not-flexible (just leave them out).
view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight
iOS likes everything to to be spring loaded and pinned to one another. Then when you rotate things, it all sticks and moves properly. They do all the work for you. So, it's best to embrace their funky way of handing things. The interface builder gui can help. It even lets you test out rotation if you want. And you can set the option of tab/nav/title bars too. Or if you understand it, then you can also do this manually.

UIScrollView - Custom Map - Prevent marker subview on map from scaling with map

I have a custom map of a limited area, and have it set up to correctly show the users' location. The map is a 1600px square image within a UIScrollView.
I have a crosshair image to show the current location of the user, which at zoomScale 1.0 is the desired size. When I pinch and zoom the scrollView, the crosshair scales with it. I would like to have the subview remain the same size on screen.
I haven't been able to find any information on this, what would be the best way to go about this?
If there is anything I can provide you with to help the answer, please let me know.
Many thanks!
EDIT -
Having looked in to this further, there is a UIScrollViewDelegate method - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale which I tried using to take the marker's current center and size, then adjust, but this only scales at the end of the zoom. I would prefer to have the marker remain the same size while the user is zooming.
EDIT 2-
Cake has provided a great answer below, but I haven't been able to implement this in the way I imagined it would be.
I have the UIImageView as a placeholder, with alpha set to 0. This placeholder moves around relative to the map to show the user location. This operates as I expect it to. Unfortunately, this resizes with the map, as it is a subview of the map (so it stays in place).
Taking Cake's below answer, I have created the non-scaling crosshair image, and added it as a sibling subview to the scrollview. The maths, once Cake had pointed them out, were quite simple to get the new frame for the crosshair:
CGPoint ULPC = userLocationPlaceholder.center;
float zs = scrollView.zoomScale;
CGRect newFrame = CGRectMake(((ULPC.x * zs) - scrollView.contentOffset.x) - 20, ((ULPC.y * zs) - scrollView.contentOffset.y) - 20, 40, 40);
Where the image is 40points wide. This matches the centers perfectly.
The problem I now have is that I cannot get the crosshair image to stay locked to the placeholder.
I have tried using a self calling animation as such:
-(void)animeUserLocationAttachment
{
[UIView animateWithDuration:0.05
delay:0
options:(UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveLinear )
animations:^{
userLocationDotContainer.frame = newFrame;
} completion:^(BOOL finished){
// Call self
[self animateUserLocationAttachment];
}];
}
As soon as I start scrolling/zooming, this locks the animation so that the crosshair just sits in place until I release the scrolling/zooming, then it correctly updates it's location.
Is there any way I can get around this, or an alternative method I can apply?
Many thanks
EDIT 3 -
I've re-accepted Cake's answer as it covers 90% of the issue. Further to his answer I have implemented the ScrollViewDelegate methods scrollViewWillBeginDragging: andscrollViewWillBeginDecelerating: to scale the placeholder to match the current size of the crosshair relative to the map, show the placeholder (that is a subview of the map image) and hide the crosshair image. The delegate method scrollviewWillBeginZooming:withView: does not show the placeholder because it scales with the map. As Cake recommends, I'll make a new question for this issue.
The counterpart methods (scrollViewDidEndZooming:withView:atScale:, scrollViewDidEndDragging:willDecelerate: and -scrollViewDidEndDecelerating:`) all hide the placeholder, and re-show the crosshair.
The question is old but for the future similar questions I've recently resolved a similar problem applying the hint of Andrew Madsen of another post.
I'had a UIScrollView, with an UIImageView in it. Attached to the UIImageView I had many MKAnnotationView (those are my subviews that I didn't want scaling with the superview).
I did subclass UIImageView and implement setTransform: method like here:
#import "SLImageView.h"
#implementation SLImageView
- (void)setTransform:(CGAffineTransform)transform
{
[super setTransform:transform];
CGAffineTransform invertedTransform = CGAffineTransformInvert(transform);
for (id obj in self.subviews)
{
if ([obj isKindOfClass:[MKAnnotationView class]])
{
[((UIView *)obj) setTransform:invertedTransform];
}
}
}
#end
This works perfectly!
Mick.
Create another crosshair image that's associated with the view or view controller that contains the scrollview. Then have this one always snap to the center of the crosshair image you already have. Then, hide your original crosshair image. Then you can avoid having the scrollview scale the disassociated crosshair, and it should stay the same size.
Relative coordinate systems
Each view in cocoa touch has a frame property that has an origin. In order to position an object owned by one view properly relative to another view, all you have to do is figure out the differences in their origins. If one view is a subview of another, then this isn't too difficult.
Get the origin of the container view
Get the location of the subview inside of the container view
Get the origin of the subview
Calculate the difference in the positions of the origins
Get the location of the object you want to overlap (relative to the subview)
Calculate the location of the object you want to overlap relative to the container view
Move your crosshair to this position
Swift equivalent for Mick's answer:
class MapContainerView:UIView {
#IBOutlet var nonScalingViews: [UIView]!
override var transform: CGAffineTransform {
didSet {
guard let nonScalingViews = nonScalingViews else {
return
}
let invertedTransform = CGAffineTransformInvert(transform)
for view in nonScalingViews {
view.transform = invertedTransform
}
}
}
}