46

I am using IBOutletCollections to group several Instances of similar UI Elements. In particular I group a number of UIButtons (which are similar to buzzers in a quiz game) and a group of UILabels (which display the score). I want to make sure that the label directly over the button updates the score. I figured that it is easiest to access them by index. Unfortunately even if I add them in the same order, they do not always have the same indexes. Is there a way in Interface Builder to set the correct ordering.

gebirgsbärbel
  • 2,327
  • 1
  • 22
  • 38

10 Answers10

75

EDIT: Several commenters have claimed that more recent versions of Xcode return IBOutletCollections in the order the connections are made. Others have claimed that this approach didn't work for them in storyboards. I haven't tested this myself, but if you're willing to rely on undocumented behavior, then you may find that the explicit sorting I've proposed below is no longer necessary.


Unfortunately there doesn't seem to be any way to control the order of an IBOutletCollection in IB, so you'll need to sort the array after it's been loaded based on some property of the views. You could sort the views based on their tag property, but manually setting tags in IB can be rather tedious.

Fortunately we tend to lay out our views in the order we want to access them, so it's often sufficient to sort the array based on x or y position like this:

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Order the labels based on their y position
    self.labelsArray = [self.labelsArray sortedArrayUsingComparator:^NSComparisonResult(UILabel *label1, UILabel *label2) {
        CGFloat label1Top = CGRectGetMinY(label1.frame);
        CGFloat label2Top = CGRectGetMinY(label2.frame);

        return [@(label1Top) compare:@(label2Top)];
    }];
}
Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
cduhn
  • 17,818
  • 4
  • 49
  • 65
  • this is a nice idea, only you should maybe change the code to compare to x in your first if-statement – gebirgsbärbel Jul 27 '11 at 17:34
  • Well, this example only demonstrates how to sort by y position. You're right that you'll need to tweak the comparator if you want to sort by x. – cduhn Jul 27 '11 at 20:15
  • 3
    return [[NSNumber numberWithFloat:[obj1 frame].origin.x] compare:[NSNumber numberWithFloat:[obj2 frame].origin.x]]; - this will sort by x – Grav Feb 07 '12 at 19:16
  • 7
    For the benefit of new visitors to this question, Xcode now appears to reliably preserve the order of IBOutletCollection views. – Nick Lockwood Mar 01 '12 at 00:51
  • Sweet! Can you confirm that it works in the simulator and on the device? Also, does it work if you run the app on older OS versions? – cduhn Mar 01 '12 at 18:49
  • Works on simulator and device. I can't confirm for sure that it works on OS versions prior to 4 but I've tested on 4.1 and above and it's fine. I use them for the high scores table in my game, so I'd have noticed pretty quickly if they were in the wrong order ;-) – Nick Lockwood Apr 15 '12 at 20:37
  • 6
    @NickLockwood I don't think this is the case when using storyboards. – Declan McKenna Feb 27 '13 at 21:50
  • This is not working for views inside of tableviewcells that are not visible. Any way to get position of non-visible views? – abc123 Apr 23 '13 at 04:03
  • @SKG, if you're talking about static cells that were laid out in a storyboard with no reuse identifiers set, you probably want to sort by `[self.tableView indexPathForCell:cell1].row` (and possibly `.section` if your table has more than one). I haven't tried this with static tables, but I assume that static tables don't reuse their cell objects, so it seems like this should work. – cduhn Apr 28 '13 at 18:17
  • Sort by y first and then x: if ([label1 frame].origin.y < [label2 frame].origin.y) return NSOrderedAscending; else if ([label1 frame].origin.y > [label2 frame].origin.y) return NSOrderedDescending; else { if ([label1 frame].origin.x < [label2 frame].origin.x) return NSOrderedAscending; else if ([label1 frame].origin.x > [label2 frame].origin.x) return NSOrderedDescending; else return NSOrderedSame; }; – bbrame Jun 18 '13 at 20:03
  • @NickLockwood it does reliably preserve the order, but is there a way to actually edit the order in IB or do you have to remove all items and re-add them? – jhabbott Aug 17 '13 at 13:48
  • There's no way to insert another item in the list AFAIK. Just remove the links to the outlets and re-bind them in order. You don't actually have to delete the views. – Nick Lockwood Aug 19 '13 at 09:09
  • If you do want to use tags: `[array sortedArrayUsingComparator:^NSComparisonResult(UIView * view1, UIView * view2) { return [@(view1.tag) compare:@(view2.tag)]; }];` – Rivera Nov 27 '13 at 07:54
  • Xcode 6 seems to remember the order you add items to the outlet collection. I've only tried this from a storyboard. – Murray Sagal Jan 03 '15 at 01:00
  • The order is not preserved even in XCode 7.3.1. Reopening XCode did break my ordering today. Swift version is `array.sortInPlace({ $0.tag < $1.tag })` – Dmitry Jun 09 '16 at 05:52
23

I ran with cduhn's answer and made this NSArray category. If now xcode really preserves the design-time order this code is not really needed, but if you find yourself having to create/recreate large collections in IB and don't want to worry about messing up this could help (at run time). Also a note: most likely the order in which the objects were added to the collection had something to do with the "Object ID" you find in the Identity Inspector tab, which can get sporadic as you edit the interface and introduce new objects to the collection at a later time.

.h

@interface NSArray (sortBy)
- (NSArray*) sortByObjectTag;
- (NSArray*) sortByUIViewOriginX;
- (NSArray*) sortByUIViewOriginY;
@end

.m

@implementation NSArray (sortBy)

- (NSArray*) sortByObjectTag
{
    return [self sortedArrayUsingComparator:^NSComparisonResult(id objA, id objB){
        return(
            ([objA tag] < [objB tag]) ? NSOrderedAscending  :
            ([objA tag] > [objB tag]) ? NSOrderedDescending :
            NSOrderedSame);
    }];
}

- (NSArray*) sortByUIViewOriginX
{
    return [self sortedArrayUsingComparator:^NSComparisonResult(id objA, id objB){
        return(
            ([objA frame].origin.x < [objB frame].origin.x) ? NSOrderedAscending  :
            ([objA frame].origin.x > [objB frame].origin.x) ? NSOrderedDescending :
            NSOrderedSame);
    }];
}

- (NSArray*) sortByUIViewOriginY
{
    return [self sortedArrayUsingComparator:^NSComparisonResult(id objA, id objB){
        return(
            ([objA frame].origin.y < [objB frame].origin.y) ? NSOrderedAscending  :
            ([objA frame].origin.y > [objB frame].origin.y) ? NSOrderedDescending :
            NSOrderedSame);
    }];
}

@end

Then include the header file as you chose to name it and the code can be:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Order the labels based on their y position
    self.labelsArray = [self.labelsArray sortByUIViewOriginY];
}
Ivan Dossev
  • 565
  • 5
  • 11
  • This seems like a good solution if you don't trust outlet collection order. Another option is to use numbered outlets for each view (e.g. label0, label1, etc) and then access them using [self valueForKey:[NSString stringWithFormat:@"label%i", index]]; which would let you iterate through them without writing repetitive code. – Nick Lockwood Jul 05 '12 at 21:55
  • Nice, I didn't think of that. Upside is you don't have to create an extra array, just need to make sure all the elements are named correctly. Only thing to consider is how frequently these elements are accessed. Performance with [NSString stringWithFormat:] is slower than looking things up in a predefined array. – Ivan Dossev Jul 10 '12 at 18:27
  • I'm doubtful that accessing UI elements would ever be a performance bottleneck in a real-world application, but if we were to suppose that it was, you could always loop through them once in viewDidLoad and dump the references into an array for subsequent access. – Nick Lockwood Jul 10 '12 at 21:37
  • This code appears to work fine. However, Xcode 4.3.2 appears to order the outlets as they are listed top-to-bottom in the view and so after some tests I will not need to use it but it's good code and worth a +1. – Christopher Oct 12 '12 at 20:52
6

Not sure when this changed exactly, but as of Xcode 4.2 at least, this no longer seems to be a problem. IBOutletCollections now preserve the order in which the views were added in Interface Builder.

UPDATE:

I made a test project to verify that this is the case: IBOutletCollectionTest

NANNAV
  • 4,875
  • 4
  • 32
  • 50
Nick Lockwood
  • 40,865
  • 11
  • 112
  • 103
  • Are you sure that this is always the case and was not just accidentally true. Because in some cases the order was just fine and as long as I would not change anything it would stay this way. – gebirgsbärbel Mar 08 '12 at 09:16
  • Well, in every case I've tried it in the last few months (which is several), the IBOutletCollection retains the view order as being whatever order I connected them in IB, which is what I'd expect it to do. I also vaguely recall that this wasn't the case the first time I tried them a year or so ago. So either it didn't used to work and now does, or it always worked this way and I just connected them up wrong when I first tried it and then wrongly gave up on IBOutletCollections. Either way though, it seems to work correctly now. Do you have evidence that it doesn't? – Nick Lockwood Mar 08 '12 at 23:45
  • I did not specifically try it out since last june. But at that time it definitely did not work and mixed up the ordering pretty badly from time to time. Maybe it is really fixed. I will try that out. – gebirgsbärbel Mar 17 '12 at 01:05
  • 1
    This does not seem to be the case. I am getting a consistent ordering that is only changing if I Remove or add items to the outlet collection, but it is NOT the same order that I added them – Dan F Jun 15 '12 at 20:55
  • I suppose another possibility is that they are ordered the same as the order of the views in the nib file. Generally when I've used it, the view order and outlet order have been the same. – Nick Lockwood Jun 16 '12 at 21:26
  • 1
    @NickLockwood I have tried making a simple 3x3 button grid for the purposes of making an iPad-type passcode lock for my app, but the order they're in is seemingly totally random. I obviously added them in order (1-9,0) but the order they appear in the array at runtime is 2,7,5,9,8,4,6,1,3,0. That order seems to have absolutely no correlation between the order they were added to the view, the order they were added to the outlet collection, or even any kind of spatial sorting – Dan F Jun 19 '12 at 13:48
  • 1
    I just built a test project to verify that I'm not going mad: http://charcoaldesign.com/resources/IBOutletCollectionTest.zip - The button order in the outlet collection follows the order in which they were bound. Changing the order of the views makes no difference. If I unbind and rebind an individual view then it changes the order in a predictable way. Can you provide a counter-project to disprove it? – Nick Lockwood Jun 19 '12 at 17:15
  • 1
    Have you also tried in Storyboard editor? At least, I can't get it to work in Storyboard editor of Xcode 4.3.3. Order seems to be mixed up if something in the Storyboard file gets changed. – ernesto Jul 05 '12 at 10:18
5

I found that Xcode sorts the collection alphabetically using the ID of the connection. If you open the version editor on your nib file you can easily edit the id's (making sure they are unique otherwise Xcode will crash).

<outletCollection property="characterKeys" destination="QFa-Hp-9dk" id="aaa-0g-pwu"/>
<outletCollection property="characterKeys" destination="ahU-9i-wYh" id="aab-EL-hVT"/>
<outletCollection property="characterKeys" destination="Kkl-0x-mFt" id="aac-0c-Ot1"/>
<outletCollection property="characterKeys" destination="Neo-PS-Fel" id="aad-bK-O6z"/>
<outletCollection property="characterKeys" destination="AYG-dm-klF" id="aae-Qq-bam"/>
<outletCollection property="characterKeys" destination="Blz-fZ-cMU" id="aaf-lU-g7V"/>
<outletCollection property="characterKeys" destination="JCi-Hs-8Cx" id="aag-zq-6hK"/>
<outletCollection property="characterKeys" destination="DzW-qz-gFo" id="aah-yJ-wbx"/>

It helps if you first order your object manually in the Document Outline of IB so they show up in sequence in the the xml code.

massimobio
  • 808
  • 9
  • 11
  • Thanks. This helps with a certain [redacted] version of Xcode that introduces an outlet collection ordering bug. – Ari Braginsky Sep 03 '13 at 22:16
  • Hmm. Is this a reliable behavior of the runtime? If it's not documented, then it's not part of the frameworks' contract, and could change in the future. I wouldn't write an app depending on this behavior. – Duncan C Dec 12 '13 at 00:56
  • Please NEVER do this. Wether it works or not is irrelevant, this is obfuscation, looks like black magic to others developer and could be broken any time ! – Antzi Nov 02 '16 at 04:41
5

Not as far as I am aware.

As a workaround, you could assign each of them a tag, sequentially. Have the buttons range 100, 101, 102, etc. and the labels 200, 201, 202, etc. Then add 100 to the button's tag to get its corresponding label's tag. You can then get the label by using viewForTag:.

Alternatively, you could group the corresponding objects into their own UIView, so you only have one button and one label per view.

Jim
  • 72,985
  • 14
  • 101
  • 108
  • I already knew about the first solution. The problem is, that I am really hoping there is a way to do it in interface builder that works repeatedly and not only with some good luck. The reason for this is, that we try to do a project, where we teach school girls to program a little quiz in three days. As they are not very experienced, the amount of coding required should be limited. Solution number 2 will not work, as the items are not directly next to each other, or is it possible to have one large view that overlays the other view without getting problems with receiving input events? – gebirgsbärbel Jun 30 '11 at 08:07
2

The extension proposed by @scott-gardner is great & solves problems such as a collection of [UIButtons] not showing in the expected order. The below code is simply updated for Swift 5. Thanks really goes to Scott for this! extension Array where Element: UIView {

  /**
   Sorts an array of `UIView`s or subclasses by `tag`. For example, this is useful when working with `IBOutletCollection`s, whose order of elements can be changed when manipulating the view objects in Interface Builder. Just tag your views in Interface Builder and then call this method on your `IBOutletCollection`s in `viewDidLoad()`.
   - author: Scott Gardner
   - seealso:
   * [Source on GitHub](bit dot ly/SortUIViewsInPlaceByTag)
   */
  mutating func sortUIViewsInPlaceByTag() {
    sort { (left: Element, right: Element) in
      left.tag < right.tag
    }
  }

}
Gallaugher
  • 1,593
  • 16
  • 27
1

It seems very random how IBOutletCollection is ordered. Maybe I am not understanding Nick Lockwood's methodology correctly - but I as well made a new project, added a bunch of UILabels, and connected them to a collection in the order they were added to the view.

After logging, I got a random order. It was very frustrating.

My workaround was setting tags in IB and then sorting the collections like so:

[self setResultRow1:[self sortCollection: [self resultRow1]]];

Here, resultRow1 is an IBOutletCollection of about 7 labels, with tags set through IB. Here is the sort method:

-(NSArray *)sortCollection:(NSArray *)toSort {
    NSArray *sortedArray;
    sortedArray = [toSort sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
        NSNumber *tag1 = [NSNumber numberWithInt:[(UILabel*)a tag]];
        NSNumber *tag2 = [NSNumber numberWithInt:[(UILabel*)b tag]];
        return [tag1 compare:tag2];
    }];

    return sortedArray;
}

Doing this, I can now access objects by using [resultRow1 objectAtIndex: i] or such. This saves overhead of having to iterate through and compare tags every time I need to access an element.

anon_dev1234
  • 2,143
  • 1
  • 17
  • 33
  • As I already said in my post, I know the solution, but did not want to use it in this specific case: "You could sort the views based on their tag property, but manually setting tags in IB can be rather tedious." – gebirgsbärbel Jun 22 '12 at 08:56
1

I needed this ordering for a collection of UITextField objects for setting where the "Next" button on the keyboard would lead to (field tabbing). This is going to be an international app so I wanted the language direction to be ambiguous.

.h

#import <Foundation/Foundation.h>

@interface NSArray (UIViewSort)

- (NSArray *)sortByUIViewOrigin;

@end

.m

#import "NSArray+UIViewSort.h"

@implementation NSArray (UIViewSort)

- (NSArray *)sortByUIViewOrigin {
    NSLocaleLanguageDirection horizontalDirection = [NSLocale characterDirectionForLanguage:[[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode]];
    NSLocaleLanguageDirection verticalDirection = [NSLocale lineDirectionForLanguage:[[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode]];
    UIView *window = [[UIApplication sharedApplication] delegate].window;
    return [self sortedArrayUsingComparator:^NSComparisonResult(id object1, id object2) {
        CGPoint viewOrigin1 = [(UIView *)object1 convertPoint:((UIView *)object1).frame.origin toView:window];
        CGPoint viewOrigin2 = [(UIView *)object2 convertPoint:((UIView *)object2).frame.origin toView:window];
        if (viewOrigin1.y < viewOrigin2.y) {
            return (verticalDirection == kCFLocaleLanguageDirectionLeftToRight) ? NSOrderedDescending : NSOrderedAscending;
        }
        else if (viewOrigin1.y > viewOrigin2.y) {
            return (verticalDirection == kCFLocaleLanguageDirectionLeftToRight) ? NSOrderedAscending : NSOrderedDescending;
        }
        else if (viewOrigin1.x < viewOrigin2.x) {
            return (horizontalDirection == kCFLocaleLanguageDirectionTopToBottom) ? NSOrderedDescending : NSOrderedAscending;
        }
        else if (viewOrigin1.x > viewOrigin2.x) {
            return (horizontalDirection == kCFLocaleLanguageDirectionTopToBottom) ? NSOrderedAscending : NSOrderedDescending;
        }
        else return NSOrderedSame;
    }];
}

@end

Usage (after layout)

- (void)viewDidAppear:(BOOL)animated {
    _availableTextFields = [_availableTextFields sortByUIViewOrigin];

    UITextField *previousField;
    for (UITextField *field in _availableTextFields) {
        if (previousField) {
            previousField.nextTextField = field;
        }
        previousField = field;
    }
}
David Robles
  • 373
  • 1
  • 8
  • 13
1

Here's an extension I created on Array<UIView> to sort by tag, e.g., useful when working w/ IBOutletCollections.

extension Array where Element: UIView {

  /**
   Sorts an array of `UIView`s or subclasses by `tag`. For example, this is useful when working with `IBOutletCollection`s, whose order of elements can be changed when manipulating the view objects in Interface Builder. Just tag your views in Interface Builder and then call this method on your `IBOutletCollection`s in `viewDidLoad()`.
   - author: Scott Gardner
   - seealso:
   * [Source on GitHub](http://bit.ly/SortUIViewsInPlaceByTag)
   */
  mutating func sortUIViewsInPlaceByTag() {
    sortInPlace { (left: Element, right: Element) in
      left.tag < right.tag
    }
  }

}
Scott Gardner
  • 8,603
  • 1
  • 44
  • 36
1

I used the extension proposed by @scott-gardner to order Image Views in order to display counters using individual png images of dot-matrix digits. It worked like a charm in Swift 5.

self.dayDigits.sortUIViewsInPlaceByTag()

func updateDayDigits(countString: String){
        for i in 0...4 {
            dayDigits[i].image = offDigitImage
        }
        let length = countString.count - 1
        for i in 0...length {
            let char = Array(countString)[length-i]
            dayDigits[i].image = digitImages[char.wholeNumberValue!]
        }
    }
e1000
  • 39
  • 3