제목: You Probably Don't Want enumerated

스위프트 표준 라이브러리에서 한가지 종종 오용되는 부분은 시퀀스(Sequence)의 enumerated()함수이다. 이 함수는 새로운 시퀀스를 만들어주는 데, 이 시퀀스는 원래 시퀀스의 각 요소와 그 요소에 해당되는 번호를 가진다.

enumerated()는 잘못 사용되고 있다. 이 함수는 각 요소에 번호를 제공하기 때문에 그 번호 문제에대한 쉬운 해결법이 될 수 있다. 그러나 이런 번호 문제는 더 나은 방법으로 해결될 수 있다. 그런 경우가 무엇인지 보자. 우리가 어떤 실수를 하고 있는지 보고, 이것을 추상적인 수준에서 한번 해결해보자.

enumerated()를 사용할때 주된 이슈는 이 함수가 요소와 그에 해당하는 각 인덱스(index)를 반환한다고 생각해버리는 것이다. 이 함수는 모든 시퀀스에서 사용가능하지만, 시퀀스는 인덱스를 가진다는 것을 보장하는 녀석이 아니기 때문에, 우리는 이것이 인덱스가 아니라는 것을 기억해야한다. 코드에서 index라 부르지않고 offset이라 부르고 있는데, 이 네이밍 컨벤션은 글 마지막에 소개해 놓았다. 오프셋(offset)은 항상 0에서 시작하여 각 요소마다 증가하는 정수형태를 말한다. Array의 경우 인덱스와 일치하겠지만, 기본적으로 다른 타입에서는 그렇지 않다. 아래 예제를 한번 보자.
let array = ["a", "b", "c", "d", "e"]
let arraySlice = array[2..<5]
arraySlice[2] // => "c"
arraySlice.enumerated().first // => (0, "c")
arraySlice[0] // fatalError
arraySlice라는 변수는 당연하게도 ArraySlice이다. 그러나 startIndex는 특별하게도 0이아닌 2이다. 이때 enumerated()first를 호출하면 오프셋이 0인 튜플을 반환해주고, 그 첫번째 요소인 "c"를 반환한다.

다른 방법으로 예시를 보자.
zip(array.indices, array)
실제로는 이렇게 한다.
zip((0..<array.count), array)
그리고 Array가 아닌 다른 것과 작업을 하면 언제든지 틀린 동작 결과를 만들 것이다.

enumerated()를 사용하면서 (인덱스가 아닌) 오프셋을 사용것으로 생긴 이슈 말고도 다른 이슈들이 있다. enumerated() 사용에대해 여러번 생각해볼 수 있는데, enumerated()를 사용할때 얻을 수 있는 더 나은 이점이 있다. 조금 더 살펴보자.

내가 본 enumerated()의 가장 일반적인 사용은, 다른 배열로부터 일치하는 요소를 잡기위해 열거된(enumerated) 배열로부터 오프셋으로 사용하는 것이다.
for (offset, model) in models.enumerated() {\
     let viewController = viewControllers[offset]
     viewController.model = model
}
이 코드는, 배열이된 modelsviewControllers에 의존하는데, 이 배열은 0에서 시작하고 정수에의해 색인(index)된다. 그리고 이 배열의 길이가 같다는 것에도 의존하고 있다. 만약 models 배열이 viewControllers 배열보다 짧다면, 별다른 나쁜일이 일어나지 않겠지만, viewControllersmodels보다 짧다면 크레쉬가 일어날 것이다. 또한 큰 역할을 하고 있지도 않은 추가적인 offset 변수까지 가지고 있어야한다. 더 스위프트한 방법으로 다시 짜보면 아래처럼 될 수 있다.
for (model, viewController) in zip(models, viewControllers) {
     viewController.model = model
}
이 코드는 읽는이를 집중시키며, 모든 Sequence 타입에서 동작한다. 또한 배열의 길이가 일치하지 않는 것도 알아서 처리해준다. 더 나은 방법일 것이다.

다른 예제를 보자. 이 코드는 첫번째 imageView와 그 컨테이너 사이에 오토레이아웃 제약(constraint)를 추가하고 쌍의 이미지뷰 사이의 오토레이아웃 제약을 만든다.
for (offset, imageView) in imageViews.enumerated() {
     if offset == 0 {
          imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
     } else {
          let imageToAnchor = imageView[offset - 1]
          imageView.leadingAnchor.constraint(equalTo: imageToAnchor.trailingAnchor).isActive = true
     }
}
이 코드 예제도 비슷한 문제가 있다. 우리는 쌍의 요소가 필요하지만, 고수준에서 작업할때 enumerated()를 사용하는 것은 지긋지긋한 인덱스를 다뤄가며 필요한 번호를 뽑아내야 한다는 의미이다. 이 부분도 마찬가지로 zip이 도와줄 것이다.

먼저 첫번째 요소에서 컨테이너 제약을 다루는 코드를 작성하자.
imageViews.first?.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
다음으로 이 쌍의 요소를 다루자.
for (left, right) in zip(imageViews, imageViews.dropFirst()) {
     left.trailingAnchor.constraint(equalTo: right.leadingAnchor).isActive = true
}
이제 됐다. 인덱스는 보이지 않고, 어떠한 (다중-전달) 시퀀스로 동작하므로 더 집중할 수 있게 되었다.

(한 익스텐션에 쌍으로 만드는(pairing) 코드를 넣을 수도 있고, 필요에 따라서는 .eachPair()을 호출할 수도 있다.)

enumerated()의 몇몇 유효한 사용이 있을 수 있다. 여러분이 얻어내고 있는 것이 인덱스가 아니라 그냥 정수이기 때문에 각 요소에 해당하는 (인덱스가 아닌) 번호로 작업해야 할 때가 바로 옳바른 사용 시점이다. 예를들어 여러 뷰들의 각 수직 좌표 y를 높이와 시퀀스의 오프셋의 곱으로 만들어야 한다면 enumerated()가 적절할 것이다. 아래에 구체적인 예시가 있다.
for (offset, view) in views.enumerated() {
     view.frame.origin.y = offset * view.frame.height
}
여기의 offset은 번호의 속성으로 사용되고 있기때문에 enumerated()가 잘 동작한다.

이제 간단하게 요약해보자면, enumerated()를 인덱스로 사용하고 있다면 그 문제를 해열하는데 더 좋은 방법이 있을 것이며, enumerated()를 번호로 사용한다면 좋아요를 표시한다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,

작년에 나는 Swift String Cheat Sheet라는 글을 썼는데, 이 글은 Swift 표준 라이브러리에서 더 복잡한 API중 하나를 어떻게 사용하는지 기억나게 해주었다. 이번 Swift3에서는 중요한 변화를 겪으면서 코드 마이그레이션을 힘들게 만들었따. 이것은 부분적으로 API 네이밍 가이드라인이 새로 바뀌면서, 컬렉션, 인덱스, 범위(Range)의 새로운 모델이 적용되었기 때문이기도 했다.

이 글은 Swift3을 위해 업데이트한 Swift Playground에 필요한 것들을 메모한 것이다.

좋은 리네이밍
표준 라이브러이에서 새로운 API 가이드라인을 적용시키면 사용하고 있던 String에서 많은 프로퍼티와 메소드를 바꿔야한다. Xcode에서 이 작업을 어느정도 대신 해주기 때문에 그 모든 바뀐점에대해 언급하진 않겠다. 아래에는 일반적으로 바뀐것에 대한 방법을 알려준다.

문자열 초기화
표준 라이브러리는 String 생성자를 init(count: repeatedValue)에서 init(count: repeatedValue)로 바뀌었다. repeatedValue는 Charater 대신에 String으로 바뀌었는데, 더 유연하게 되었다.

upper/lower case로 변환하기
uppercaseString과 lowercaseString 프로퍼티가 이제 uppercased()와 lowercased() 함수로 바뀌었다.

조금 더 있다가 다른 바뀐 이름에대해 다룰것이다.

인덱스를 사용하여 컬렉션을 탐색하기
Swift3에 들어오면서 String에 가장 큰 영향을 준 변화 중 하나는 컬렉션과 인덱스의 새로운 모델이다. 고쳐 쓰기위해 String의 요소에 직접 접근할 수 없고 대신에 컬렉션에 있는 인덱스를 사용해야한다.

Swift3에서 각 컬렉션의 startIndex와 endIndex 프로퍼티는 바뀌지 않았다.

character에 있는 것을 원할때 character 프로퍼티를 생략할 수도 있따.

인덱스로 문자열을 탐색할 수 있도록 바뀌었다. 이제 successor(), predecessor(), advancedBy(n) 함수들은 없어졌다.

Swift3에서는 이제 같은 결과를 얻기 위해 index(after:), index(before:), index(_: offsetBy:)를 사용한다.

또한 end 인덱스를 넘어가버릴때 에러를 피하기위해 offset 한계를 정할 수도 있다. index(_: offsetBy: limitedBy:) 함수는 너무 멀리까지 가버리면 nil을 반환하는 옵셔널 반환 함수이다.

첫번째로 일치하는 요소(아래는 character이다)의 인덱스를 찾는다.

마지막으로는, 두 인덱스 사이의 거리를 계산해주는 메소드 이름이 바뀌었다.

범위(Range) 사용하기
범위는 Swift3에서 바뀌었는데, character에 시작 인덱스(lower bound)와 끝 인덱스(upper bound)를 가지고 있다고 가정하자.

upper와 lower 바운드로부터 범위를 생성하기 위한 생성자 전체

..<와 ... 연산자를 사용하면 더 쉽게 생성할 수 있다.

하위 문자열에 일치하는 문자가 있는지 확인하고 범위를 반환한다.

Playground
여러분은 전체적으로 업데이트된 playground 내 예제 코드를 저장소에서 확인할 수 있다. 또한 이전에 작성한 포스팅(영문)도 업데이트 되었다.

더 읽을거리



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,