[iOS_OSX] 배열을 정렬하기

배열을 정렬하기

배열을 정렬하는 거 언젠가는 써 먹겠지 싶어서 정리.
생각보다 자주 써먹게 되더라

배열을 정리하는 방식은 크게 세 가지로 1)디스크립터를 사용하거나 2)블럭, 3)셀렉터를 사용하는 방법이 있다. 각각이 표현의(?) 차이는 있는데 실제로는 각 요소들을 비교하여 어떤 것이 앞에 오는지를 비교하여 정렬하게 된다.

1. sort descriptor를 사용하는 법

디스크립터는 NSSortDescriptor의 인스턴스로, 정렬 순서를 표현하는 클래스이다. 코어데이터의 fetch 동작에도 사용되는데, 상당히 간단하고 코코아 바인딩에도 적용할 수 있는 등, 활용 범위가 넓다. 디스크립터를 사용한 정렬은 배열 객체에 sortedArrayUsingDescriptors: 나 sortUsingDescriptors: 메시지를 보내서 적용할 수 있다.

이 과정은 다음과 같이 구현한다.

  1. NSSortDescriptor 객체를 만든다. 이 객체를 만들 때는 비교의 대상이 되는 키이름과 정렬순서(오름차순인지 내림차순인지) 및 비교에 사용할 메소드를 지정한다.
  2. 만약 두 가지 이상의 기준을 사용한다면 디스크립터를 필요한 만큼 만든다.
  3. 디스크립터들을 하나의 배열 객체로 만든다.
  4. 정렬할 배열 객체에 sortedArrayUsingDescriptors: 메시지를 보내어 정렬된 배열 사본을 얻을 수 있다.

예를 들어 firstName, lastName의 두 개 키로 구성된 사전에 사람의 이름을 넣고, 이 사전들로 구성된 배열을 정렬하는 케이스를 살펴보겠다.

NSString *LAST = @"lastName";
NSString *FIRST = @"firstName";
NSMutableArray *array = [NSMutableArray array];
NSArray *sortedArray;
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
                        @"Jo",FIRST,@"Smith",LAST,nil];
[array addObject:dict];
/**....**/
//위 과정을 반복하여 충분한 수의 사람 이름을 배열에 추가함

NSSortDescriptor *lastNameDescriptor = [[NSSortDescriptor alloc]
                                          initWithKey:LAST
                                           ascending:YES
                                            selector:@selector(localizedCaseInsensitiveCompare:)];
NSSortDescriptor *firstNameDescriptor = [[NSSortDescriptor alloc]
                          initWithKey:FIRST
                            ascending:YES
                             selector:@selector(localizedCaseInsensitiveCompare:)];
NSArray *descriptors = [NSArray arrayWithObjects:lastDescriptor, firstDescriptor, nil];
sortedArray = [array sortedArrayUsingDescriptors:descriptors];

여기서 디스크립터 배열의 원소의 순서는 비교의 우선순위가 된다. 즉 lastname으로 정렬하고, 같은 lastname인 경우에는 firstname으로 다시 정렬하게 된다. 문자열을 비교하고 있으므로 디스크립터를 만들때 localizedCaseInsensitiveCompare: 메소드를 사용했다.

2. 블럭으로 정렬하기

요건은 간단하다. 디스크립터의 compare: 메소드를 블럭으로 구현하는 것이다. 두 개의 값을 비교해서 뒤에 오는 값이 크거나 뒤의 것이라면 NSOrderedDescending을, 반대라면 NSOrderAscending을, 같다면 NSOrderSame을 내놓으면 된다. 이 원리를 사용하면 블럭으로 소팅하는 것으로 두 개 숫자값 (배열에 들어있으므로 NSNumber가 되어있을)을 비교할 수 있다.

NSArray *sortedArray = [array sortedArrayUsingComparator:^(id obj1, id obj2) {
      if ([obj1 integerValue] > [obj2 integerValue])
          return (NSComparisonResult)NSOrderDescending;
      else if ([obj1 integerValue] < [obj2 integerValue] )
          return (NSComparisonResult)NSOrderAscending;
      else
           return (NSComparisonResult)NSOrderSame;
}];

정렬 디스크립터 조금 더 살펴 보기

정렬 디스크립터는 정렬기준을 표현하는 클래스라고 했다. 처음 예제 코드에서는 셀렉터를 사용하도록 했는데, 만일 해당 키가 문자열이 아니라면 셀렉터를 사용하지 않아도 된다. 만약 NSNumber로 된 키를 사용한다고 하면 다음과 같이 사용하면 된다.

NSSortDescriptor *ageDescriptor = [[NSSortDescriptor alloc]
                    initWithKey:@"age" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObject:ageDescriptor];
sortedArray = [studentsArray sortedArrayUsingDescriptors:sortDescriptors];

위 예제는 정렬 디스크립터를 만들면서 별도로 비교방법을 주지 않았다. 이 경우에는 디폴트 비교 메소드인 compare:가 사용된다. 이 메소드는 NSNumber와 NSDate 등에 쓰인다. 만약 문자열을 비교한다면 NSString이 가지고 있는 비교 메소드를 사용해야 한다. 문자열이 사용자에게 보여진다면 비교시에는 localizedCompare: / localizedCaseInsensitiveCompare:를 사용해야 한다.1

비교를 위한 메소드는 위의 블럭을 사용한 예에서 살펴보았듯이, 특정 객체에게 메시지를 보내서, 인자로 넘긴 객체와의 순서가 앞이냐, 뒤냐를 판별하게 하는 것이다. 만약 임의의 객체를 정렬해야 한다면 해당 객체에 비교를 위한 메소드를 구현해두거나, 카테고리를 사용하여 추가해서 사용하는 방법을 쓸 수도 있을 것이다.

추가 – 2014.12.26

Swift로 넘어오면서 배열의 정렬은 비교에 사용할 메소드를 클로저로 만들어서 넘기는 것만으로 간단히 해결될 수 있다.


  1. 문자열에서 compare:는 의도한 것과는 약간 다른 결과를 보일 수 있다.