在Interface Builder中设置IBOutletCollection的顺序

46

我正在使用IBOutletCollections将多个相似的UI元素分组。特别是将一些UIButtons(类似于问答游戏中的蜂鸣器)和一组UILabels(用于显示得分)分组。我想确保直接在按钮上方的标签更新得分。我认为通过索引访问它们最容易。不幸的是,即使我按照相同的顺序添加它们,它们也不总是具有相同的索引。是否有一种方法可以在界面构建器中设置正确的排序。

10个回答

75

编辑:多位评论者声称较新版本的Xcode会按照连接的顺序返回IBOutletCollections。其他人则声称,在storyboards中这种方法不起作用。我自己没有测试过,但如果你愿意依赖未记录的行为,那么你可能会发现下面提出的显式排序已经不再必要。


很遗憾,在IB中似乎没有办法控制IBOutletCollection的顺序,所以你需要在加载完数组后根据视图的某些属性对其进行排序。你可以根据它们的tag属性来排序视图,但在IB中手动设置标签可能会相当繁琐。

幸运的是,我们通常按照要访问它们的顺序布置视图,因此通常可以根据x或y位置对数组进行排序,就像这样:

- (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)];
    }];
}

3
返回 [[NSNumber numberWithFloat:[obj1 frame].origin.x] compare:[NSNumber numberWithFloat:[obj2 frame].origin.x]]; - 这将按照x值进行排序 - Grav
7
为了方便新来访问此问题的人,现在 Xcode 似乎能够可靠地保留 IBOutletCollection 视图的顺序。 - Nick Lockwood
6
我认为在使用storyboards时情况并非如此。 - Declan McKenna
@NickLockwood 它确实可靠地保留了顺序,但是在IB中是否有一种实际编辑顺序的方法,还是必须删除所有项目并重新添加它们? - jhabbott
据我所知,无法在列表中插入另一个项目。只需删除与插座的链接并按顺序重新绑定它们即可。实际上,您不必删除视图。 - Nick Lockwood
显示剩余10条评论

23
我使用cduhn提供的答案,创建了这个NSArray类别。 如果现在的Xcode真的保留设计时的顺序,那么这段代码就不是必需的,但如果你发现自己需要在IB中创建/重新创建大量集合,并且不想担心混乱,那么这可能会有所帮助(在运行时)。另外注意一点:对象添加到集合中的顺序很可能与您在Identity Inspector选项卡中找到的“Object ID”有关,当您编辑界面并在以后时间引入新对象集合时,它可以变得间歇性。
@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

然后按你选择的名称引入头文件,代码可能是这样的:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Order the labels based on their y position
    self.labelsArray = [self.labelsArray sortByUIViewOriginY];
}

如果您不信任插座集合的顺序,那么这似乎是一个不错的解决方案。另一个选择是为每个视图使用编号插座(例如label0、label1等),然后使用[self valueForKey:[NSString stringWithFormat:@“label%i”,index]]来访问它们;这将使您能够遍历它们而不编写重复的代码。 - Nick Lockwood
不错,我没想到那个。 好处是你不需要创建额外的数组,只需要确保所有元素的命名正确即可。 唯一需要考虑的是这些元素被访问的频率。使用 [NSString stringWithFormat:] 的性能比在预定义数组中查找要慢。 - Ivan Dossev
我怀疑在现实世界的应用程序中,访问UI元素永远不会成为性能瓶颈,但是如果我们假设它是,您可以在viewDidLoad中循环一次并将引用转储到数组中以供后续访问。 - Nick Lockwood
这段代码似乎可以正常工作。然而,Xcode 4.3.2 按照它们在视图中自上而下列出的顺序对输出进行排序,因此经过一些测试后我将不需要使用它,但这是良好的代码,值得一个 +1。 - Christopher

6

我不确定这个问题具体是什么时候改变的,但至少在Xcode 4.2中,IBOutletCollection似乎不再是一个问题。现在IBOutletCollections会保留在Interface Builder中添加视图的顺序。

更新:

我创建了一个测试项目来验证这一情况:IBOutletCollectionTest


自从去年六月以来,我并没有专门尝试过它。但那时候它明显不起作用,并且经常会混乱地打乱顺序。也许现在已经修复了。我会试一下的。 - gebirgsbärbel
1
这似乎不是这种情况。我得到了一个一致的排序,只有在我从插座集合中删除或添加项目时才会更改,但它不是我添加它们的相同顺序。 - Dan F
1
@NickLockwood 我试着创建一个简单的 3x3 按钮网格,用于为我的应用程序创建类似 iPad 的密码锁,但它们的顺序似乎完全随机。我显然按顺序添加了它们(1-9,0),但是它们在运行时出现在数组中的顺序是2、7、5、9、8、4、6、1、3、0。这个顺序似乎与它们被添加到视图的顺序、添加到输出集合的顺序甚至任何空间排序之间都没有任何相关性。 - Dan F
1
我刚刚建立了一个测试项目来验证我没有疯掉:http://charcoaldesign.com/resources/IBOutletCollectionTest.zip - Outlet集合中的按钮顺序遵循它们绑定的顺序。更改视图的顺序没有任何影响。如果我取消绑定并重新绑定单个视图,则可以以可预测的方式更改顺序。你能提供一个反驳它的对比项目吗? - Nick Lockwood
1
你是否也尝试在Storyboard编辑器中处理?至少,在Xcode 4.3.3的Storyboard编辑器中,我无法让它正常工作。如果Storyboard文件中的内容被更改,顺序似乎会混乱。 - ernesto
显示剩余3条评论

5
我发现Xcode使用连接的ID按字母顺序对集合进行排序。 如果您在nib文件上打开版本编辑器,您可以轻松地编辑id(确保它们是唯一的,否则Xcode将崩溃)。
<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"/>

如果您首先在IB的文档大纲中手动按顺序排列对象,则有助于它们在xml代码中按顺序显示。


谢谢。这有助于解决某个版本的Xcode中出现的插座集合排序错误问题。 - Ari Braginsky
嗯,这是运行时的可靠行为吗?如果没有记录,那么它不是框架合同的一部分,并且将来可能会更改。我不会编写依赖于此行为的应用程序。 - Duncan C
请永远不要这样做。无论它是否有效都是无关紧要的,这是混淆代码,对其他开发人员来说看起来像黑魔法,并且随时可能被破解! - Antzi

5
据我所知,这并不可行。作为一种解决方法,您可以给它们分别分配标签,按顺序排序。将按钮的范围设置为100、101、102等,标签设置为200、201、202等。然后将按钮的标签加上100以获取其对应标签的标签。您可以使用viewForTag:来获取标签。或者,您可以将相应的对象分组到它们自己的UIView中,这样每个视图只有一个按钮和一个标签。

我已经知道第一个解决方案了。问题是,我真的希望有一种在界面构建器中可重复使用的方法,而不仅仅是凭运气。原因是我们正在尝试做一个项目,在这个项目中,我们要教学校女孩们在三天内编写一个小测验。由于她们没有太多经验,所需编码量应该是有限的。第二个解决方案行不通,因为项目不是直接相邻的,或者说有没有可能有一个大视图覆盖另一个视图而不会出现接收输入事件的问题? - gebirgsbärbel

2

@scott-gardner 提出的扩展很好,解决了诸如未按预期顺序显示 [UIButtons] 集合等问题。下面的代码已经简单更新为 Swift 5。真正的感谢应该送给 Scott!

扩展如下:

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
    }
  }

}

1

IBOutletCollection的排序方式看起来非常随机。也许我没有正确理解Nick Lockwood的方法 - 但是我也创建了一个新项目,在视图中添加了大量UILabel,并将它们按照添加到视图中的顺序连接到集合中。

在记录日志后,我得到了一个随机的顺序。这非常令人沮丧。

我的解决方法是在IB中设置标签,然后像这样对集合进行排序:

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

这里,resultRow1是一个IBOutletCollection,包含大约7个标签,通过IB设置标签。这是排序方法:

-(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;
}

通过这样做,我现在可以使用[resultRow1 objectAtIndex: i]或类似的方式访问对象。这样可以节省每次需要访问元素时迭代并比较标记的开销。

正如我在帖子中所说的,我知道解决方案,但不想在这种特定情况下使用它:“您可以根据视图的标记属性对其进行排序,但在IB中手动设置标记可能会相当繁琐。” - gebirgsbärbel

1
这是我在 Array<UIView> 上创建的一个扩展,可按 tag 进行排序,例如,在使用 IBOutletCollection 时非常有用。
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
    }
  }

}

1
我需要对一组UITextField对象进行排序,以设置键盘上的“下一个”按钮将导航到哪个文本框(字段切换)。由于这是一款国际应用程序,因此我希望语言方向不明确。

.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

使用方法(布局后)

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

    UITextField *previousField;
    for (UITextField *field in _availableTextFields) {
        if (previousField) {
            previousField.nextTextField = field;
        }
        previousField = field;
    }
}

1

我使用@scott-gardner提出的扩展程序来排序图像视图,以便使用点阵数字的单独png图像显示计数器。在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!]
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接