2014년 6월 30일 월요일

Objective-C에서 Swift로 넘어가기 #2

앞서 썼던 Objective-C에서 Swift로 넘어가기 위한 글에 이어서 이번에도 스위프트로 넘어갈 때 알아두면 크게 손해보지는 않는(?) 사항 몇 가지를 정리해 본다. 이번 글은 NSFileManager를 다루면서 겪었던 다수의 짜증(?) 해결 과정을 담고 있다.

NSErrorPointer

NSFileManager 의 contentsOfDirectoryAtPath 메소드는 아래와 같이 정의되어 있다.
func contentsOfDirectoryAtPath(_ path: String!,
                               error error: NSErrorPointer) -> [AnyObject!]
여기서 못생긴 NSErrorPointer 라는 이름을 보게 되었다. 물론 이름 만으로는 Objective-C의 'NSError *' 형식의 변수를 넘겨줘야 한다고 알 수 있다. 그런데 NSErrorPointer 방식으로 변수를 선언해서 넘겨주려 해도 에러가 만발하였다.

하지만 이 녀석은 의외로 순순하게 해결되었다. 그냥 NSError 로 inout 형식으로 레퍼런스를 넘겨주니 동작하였다.
let fm = NSFileManager.defaultManager()
var error: NSError?
let contents = fm.contentsOfDirectoryAtPath("/foo/bar", error: &error)
왜 저렇게 써 놔서 사람 헷갈리게 만드는지 원...

CMutablePointer

NSFileManager 의 fileExistsAtPath 메소드를 다루려면 보게 되는 욕(?) 같은 타입. 참고로 이 메소드는 몇 가지 오버로드가 있는데 여기서 칭하는 건 두 번째 파라미터를 통해 디렉토리 여부 확인 기능이 추가된 녀석이다.

우선 Objective-C 스타일의 원형을 보자.
- (BOOL)fileExistsAtPath:(NSString *)path
             isDirectory:(BOOL *)isDirectory
여기서 두 번째 인자인 isDirectory는 BOOL 타입 포인터라서 단순하게 생각된다. 하지만 스위프트 버전의 정의를 보면
func fileExistsAtPath(_ path: String!,
          isDirectory isDirectory: CMutablePointer<ObjCBool>) -> Bool
이렇다. 길고 욕(?) 같은 이름이 섞여있는 이 CMutablePointer 라는 제너릭을 지원하는 타입은 뭐냔 말이다!

아니 물론 용도는 명확하다. 그냥 C 스타일 포인터에 매핑되어 있는 타입이다. 그런데 이게 굉장히 무시무시한 방법으로 사용해야 되는데 unsafe 라는 키워드가 붙어있는 이름을 써야한다. 그래서 집어치웠다. -_- 안써 퉷!

그런데 의외의 방법으로 해결이 되긴 하였다. 그냥 ObjCBool 이라는 타입의 변수를 레퍼런스로 넘겨주니 되었다. (참고. Xcode 6 Beta 3 에서 약간의 표현 변경사항이 있음)
let fm = NSFileManager.defaultManager()
// var isDirectory = ObjCBool(0)
var isDirectory: ObjCBool = false
let exists = fm.fileExistsAtPath(“/foo/bar/path”, isDirectory: &isDirectory)
if isDirectory.getLogicValue() {
    ...
} else {
    ...
}
자자 이 미친 네이밍을 어떻게 해결하면 좋을까. 아니 왜 되냐구!

ObjCBool 타입을 생성할 때 0을 인자로 넘겨줬는데 이걸 비울 수 없기 때문에 그냥 임의로 0을 넣었다. 참고로 이 녀석은 Objective-C의 BOOL 타입에 매핑되어 있는 녀석이라 true/false가 아닌 숫자 0과 0 이외의 것으로 참과 거짓을 구분하는 녀석이다.

물론 이런 숫자 타입의 Boolean 타입은 논리(Logical) 타입이 아니기 때문에 스위프트의 if 문 등에서 쓸 수가 없다. 다행히도 getLocalValue() 라는 Bool 타입을 리턴해주는 메소드가 있었으니 망정이었지...

AnyObject 이터레이션

사실 이건 좀 불만에 가까운 항목이다. 이번 예제는 제일 위에서 언급한 ​contentsOfDirectoryAtPath 메소드의 결과물을 가공하는 과정에서 알게 되었다.

contentOfDirectoryAtPath의 정의를 다시 한번 보자.
func contentsOfDirectoryAtPath(_ path: String!,
                               error error: NSErrorPointer) -> [AnyObject!]
AnyObject의 배열(Array)인데 Unwrapped Implicit Optional로 보내준다. 여기서 AnyObject 라는 것에 주목하자.

AnyObject는 아무 클래스의 오브젝트를 담을 수 있는 녀석이다. 스위프트에선 뭐라고 칭해야 할지 모르겠는데, Objective-C 의 NSObject 클래스 타입과 비슷한 용도이다. 그런데 이런 형식은 스위프트의 type safe 라는 철학과는 맞지 않는다.

그래서 스위프트에서 AnyObject[]의 이터레이션을 할 때는 아래와 같은 식으로 형식을 바꿔서(Type Conversion) 써야 한다. 아래 예제는 items 라는 AnyObject[] 데이터를 이터레이션 하는 코드 예제이다.
for item: AnyObject in items {
    let myItem = item as TypeName
    ...
}
사실 이걸 for문 한 줄에서 어떻게 더 좋게 표현할 수 없나를 한참 고민했었다. 왜냐하면 애플 공식 문서의 내용대로 따라하면 Xcode가 죽어버리기 때문에... -_-;;;;다행히도 위 코드는 다운캐스팅(Down Casting)을 이용해 한 줄로 바꾸는 것이 가능하다.
for item in items as Array<TypeName> {
    ...
}
[추가] Xcode 6 Beta 3 에서 AnyObject의 다운캐스팅이 더 추가되었다.
for item in items as? [TypeName] {
    ...
}
as 뒤의 물음표는 역시나 옵셔널 지칭인데 상황에 따라 안붙을수도 있고 느낌표(!)가 올 수도 있다. 이건 기존의 컬렉션에 쓰이는 타입에 따라 달라진다.

혹시나 오해하시는 분이 계실까봐 적는데, TypeName 이란건 실제로 AnyObject에 담겨 있는 오브젝트의 타입과 맞는 타입 이름을 적으라는 의미이다.

추가로 약간의 팁. AnyObject에서는 is 라는 키워드를 이용해 오브젝트 타입을 알 수 있다.
for item: AnyObject in items {
    if item is Int:
        let value: Int = item as Int
        println("item is Integer \(value)")
    else if item is String:
        let value: String = item as String
        println("item is String \(value)")
    ...
이런 식이다.

그런데 Any나 AnyObject를 제외한 다른 타입에서는 is 라는걸 쓰면 요상한 에러가 난다. 참고하자.

잡담

글투(?)로 봐서 글쓴이의 심리 상태가 그다지 호의적이지 못 하다는 것을 알 수 있으리라 생각된다. 사실 아직 스위프트로 코딩하기에는 Objective-C용 프레임워크의 잔재가 심하게 많이 남아있어서 오히려 불편을 유발하는 경우가 많다.

즉 언어 자체는 완성도가 높은 편이지만 프레임워크가 아직 미완성이다.

당연하지. 이제 겨우 Xcode 6 Beta2 다! 시간이 흐르면 흐를수록 이 글이 쓸모없어 질 거라 생각되고 그렇다면 오히려 기쁘겠다 젠장...

[관련글] Objective-C에서 Swift로 넘어가기
[관련글] 스위프트(Swift) 가이드

댓글 없음 :