1월 132017
 

이 글은 qiita에 작성된 http://qiita.com/motokiee/items/b30514204a819a09425b(작성자 motokiee님, 2016-02-29 투고)을 번역한 글입니다.

Objective-C에서 Swift로 이전하는 과도기

수년간 개발되어온 앱에 슬슬 Swift를 도입하기 시작하지 않았나요? 당연히 Objective-C에서 만들어진 재산을 그대로 둔 상태로 개발하게 될 것이라고 생각합니다.

이 때 Optional을 다루는 것에 대해 곤란해하고있지 않을까 생각이 듭니다. Objective-C에서는 리시버가 nil인 상태로 메시지를 보내더라도 크래시가 발하지 않았지만, Optional이 있는 Swift로 부터 Objective-C의 코드를 호출하는 경우에는 좀 곤란해 집니다.

Swift와 Objective-C의 호환성을 강화하기 위해서 nullable, nonnull이 Objective-C에 추가되었습니다.

Swift의 코드를 작성할 때, 이러한 형수식자(Type qualifier)를 사용해서 Objective-C 쪽도 개선하여 Swift 도입을 좀 더 쉽게 할 수 있을 것이라고 생각합니다.

nullable

nullable은 Optional에 있는 nil을 허용한다는 것을 명시하기 위한 형수식자입니다.

예를 들면, NSDatainitWithContentsOfURL: 이니셜라이져는 아래와 같이 정의되어 있어서 리턴값이 nil이 될 수있다는 것을 명시하고 있습니다.

- (nullable instancetype)initWithContentsOfURL:(NSURL *)url;

이것을 스위프트에서 보면 아래와 같이 failable initializer로 변환되어 실패할 가능성이 있는 이니셜라이져가 되는 것을 알 수 있습니다.

public init?(contentsOfURL url: NSURL)

이와 같이 인스턴스 생성이랑 파라미터, 리턴값이 nil이 될 수 있는 경우에는 Objective-C쪽에 nullable을 지정해 두는 것으로 Swift에서 Optional으로 다루는 것이 가능해집니다.

Objective-C에서는 nullable을 지정하지 않는 경우에는 “Implicitly unwrapped optional”이 됩니다. 아래와 같은 Objective-C의 메소드에서 고려해보겠습니다.

- (UIImage*)createImage;

nullable을 붙이지 않고 Swift에서 사용하려고 하면 변환시에 “Implicitly unwrapped optional”이 되어 버립니다.

func createImage() -> UIImage!

이 메소드의 리턴 값을 사용하려고 할 때 nil일 경우 크래시가 발생해버려서 Swift부터 이 Objective-C의 코드를 사용할 때에 불안한 부분이 따라다니게 됩니다. 혹시 이러한 처리를 보게 된다면 nullable을 지정하여 Swift쪽에서 Optional으로 이용가능하도록 하면 Swift에서도 안심하고 이용할 수 있게 됩니다.

- (nullable UIImage*)createImage;

아래의 Objective-C의 코드를 수정하면 Swift에서는

func createImage() -> UIImage?

과 같이 변환됩니다.

nonnull

nonnull은 Optional이 아니라는 것을 명시하기 위한 형수식자로 nonnull을 함수랑 메소드의 파라미터, 리턴 값에 지정하는 경우에는 Optional한 변수를 지정하는 것이 불가능해집니다.

NS_ASSUME_NONNULL_BEGIN, NS_ASSUME_NONNULL_END

하지만 UIKit의 소스코드를 확인해보면 nonnull을 찾을 수 없습니다.

예를 들면 UIVisualEffectView는 이하와 같이 정의 되어 있습니다만 nonnull은 어디에도 쓰이고 있지 않습니다. nullable은 제대로 쓰이고 있습니다.

NS_CLASS_AVAILABLE_IOS(8_0) @interface UIVisualEffectView : UIView <NSSecureCoding>
@property (nonatomic, strong, readonly) UIView *contentView; // Do not add subviews directly to UIVisualEffectView, use this view instead.
@property (nonatomic, copy, nullable) UIVisualEffect *effect;
- (instancetype)initWithEffect:(nullable UIVisualEffect *)effect NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
@end

하지만 Swift의 변환은 제대로 Optional이 아니도록 되어 있습니다.

@available(iOS 8.0, *)
public class UIVisualEffectView : UIView, NSSecureCoding {
    public var contentView: UIView { get } // Do not add subviews directly to UIVisualEffectView, use this view instead.
    @NSCopying public var effect: UIVisualEffect?
    public init(effect: UIVisualEffect?)
    public init?(coder aDecoder: NSCoder)
}

nonnull의 지정은 제대로 되어 있다는 것입니다. 왜 nonnull이 지정되지 않은것과 상관없이 변환이 가능한 걸까요?

알아보니 NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END 매크로가 사용되어 있었습니다.

앞에서 이야기 했던 UIVisualEffectView.h에도 제대로 이 매크로가 사용되어 있었습니다.

//
//  UIVisualEffectView.h
//  UIKit
//
//  Copyright (c) 2014-2015 Apple Inc. All rights reserved.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

// ...중략

NS_CLASS_AVAILABLE_IOS(8_0) @interface UIVisualEffectView : UIView <NSSecureCoding>
@property (nonatomic, strong, readonly) UIView *contentView; // Do not add subviews directly to UIVisualEffectView, use this view instead.
@property (nonatomic, copy, nullable) UIVisualEffect *effect;
- (instancetype)initWithEffect:(nullable UIVisualEffect *)effect NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
@end

NS_ASSUME_NONNULL_END

Foundation과 UIKit의 소스를 찾아보면 이 매크로가 사용되어 있었습니다.

확실히 nonnull, nullable을 하나하나 다 쓰고 있는 것은 큰일이겠지요. Objective-C에서 nonnull, nullable을 붙일 필요가 있는 경우는 적극적으로 NSASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END을 사용하고 nullable만을 쓰는 것이 좋을지도 모르겠습니다.

주의

같은 파일내에 메소드와 프로퍼티에 하나라도 nonnull, nullable을 쓰는 경우, 파일 내의 보든 메소드의 파라미터, 리턴 값, 프로퍼티에 형수식자를 붙이지 않으면 안됩니다. warning이 발생합니다.

 

Lightweight Generics

Swift로부터 Objective-C의 코드를 사용하려고 하는 때, NSArray로부터 변환과 NSDictionary로부터 Dictionary의 변환에서는 곤란할 부분이 없습니다만 배열 요소의 형이 AnyObject가 되어버려 곤란해 질 수 있을 것이라 생각합니다.

이와 같은 경우에 guard랑 Optional Binding같은 것을 사용해서 안전하게 형변환하여 구현할 것이라 생각하지만 Objective-C의 코드를 Generics를 사용해서 수정하는 편이 더 좋아보입니다.

UIView는 subviews라 불리우는 NSArray의 프로퍼티를 가지고 있습니다만, Generics를 사용해서 UIView의 배열이라는 것을 명시하고 있습니다.

@property(nonatomic,readonly,copy) NSArray<__kindof UIView *> *subviews;

__kindof는 서브클래스(자식 클래스)도 허용하기 위한 어노테이션입니다. subviews는 UIView의 서브클래스도 허용하는 것을 명시합니다.

Objective-C에 Nullability와 Generics을 지정해 가는 공정

Objective-C에서 이하와 같이 정의되어있는 오브젝트를 보겠습니다.

@interface MNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nonatomic, copy) NSArray *items;
- (NSString*)hey;
- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;
@end

이것을 Swift로부터도 사용하기 쉽도록 Nullability와 Generics를 지정해보겠습니다.

아무 손도 대지 않은 경우, Swift에서는 이렇게 보입니다. 이니셜라이져와 프로퍼티에 !가 붙어서 “Implicitly unwrapped optional”이 된 것을 알 수 있습니다.

public class MNPerson : NSObject {
    public var name: String!
    public var age: UInt
    public var items: [AnyObject]!
    public func hey() -> String!
    public init!(name: String!, age: UInt)
}

Objective-C의 헤더가 Swift에서 어떤식으로 표시되는지를 확인하는 방법

Objective-C에서 Nullability와 Generics를 지정할 때, jump bar의 좌측 끝에 있는 버튼을 클릭하면 나타나는 “Generated Interface”를 사용해서 Objective-C의 헤더파일이 Swift에 어떤 인터페이스가 되는지 확인하는 것이 가능합니다.

 

nullable의 설정

먼저 nullable을 붙여보도록 합시다. 아래와 같이 됩니다.

@interface MNPerson : NSObject
@property (nullable, nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray *items;
- (nullable NSString*)hey;
- (nullable instancetype)initWithName:(nullable NSString*)name age:(NSUInteger)age;
@end

Nullability는 포인터형만 지정하는 것이므로 primitive한 값, NSUInteger같은 것에는 nullable을 붙일 필요가 없습니다.

이것을 Generated Interface에서 보면 아래와 같이 됩니다.

public class MNPerson : NSObject {
    public var name: String?
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String?
    public init?(name: String?, age: UInt)
}

nonnull의 설정

우선 Optional으로 취급되고 있도록 되었습니다. 하지만 모든 것이 Optional이라면 하나하나 Optional Binding으로 값을 끄집어내지 않으면 안되기 때문에 좀 귀찮습니다.

nonnull으로 취급하는 장소가 없나 구현을 확인해봅시다.

- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age {
    self = [super init];

    if (self) {
        _name = name;
        _age = age;
    }
    return self;
}


- (NSString*)hey {
    return @"hey";
}

이니셜라이져에서 인스턴스 변수인 _name_age에 값이 설정되어 있습니다. hey 메소드도 실패 가능성은 없기 때문에 여기에 해당하는 프로퍼티랑 메소드의 파라미터를 nonnull으로 해봅시다.

@interface MNPerson : NSObject
@property (nonnull, nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray *items;
- (nonnull NSString*)hey;
- (nonnull instancetype)initWithName:(nonnull NSString*)name age:(NSUInteger)age;
@end

Swift에서 봐보면 이렇게 됩니다.

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String?
    public init(name: String, age: UInt)

NS_ASSUME_NONNULL_BEGIN,NS_ASSUME_NONNULL_END 매크로를 사용하면 아래와 같이 쓸 수 있습니다.

NS_ASSUME_NONNULL_BEGIN

@interface MNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray *items;
- (NSString*)hey;
- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;
@end
NS_ASSUME_NONNULL_END

nil이 될 가능성이 있는 곳에만 nullable을 지정할 필요가 있습니다만 nonnull인 프로퍼티에 대하여는 지정이 불필요해집니다.

결과는 앞과 같은 형태, 아래와 같이 됩니다.

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String
    public init(name: String, age: UInt)
}

Generics의 설정

이것으로 Optional의 설정은 완료했습니다만 Swift로부터 사용할 때에 귀찮은 점이 한 부분 남아 있습니다. Generated Interface를 봐 봅시다.

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [AnyObject]?
    public func hey() -> String
    public init(name: String, age: UInt)
}

items 프로퍼티가 AnyObject의 배열이 되어 있습니다. 하나하나 캐스트 하는 것도 귀찮습니다. 여기서는 items에 쌓여있는 것은 문자열이라 한정해서 Generics의 설정을 하겠습니다.

NS_ASSUME_NONNULL_BEGIN

@interface MNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger age;
@property (nullable, nonatomic, copy) NSArray<NSString*> *items;
- (NSString*)hey;
- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;
@end
NS_ASSUME_NONNULL_END

items 프로퍼티에 대하여 NSString을 지정했습니다. Generated Interface를 봐 봅시다.

public class MNPerson : NSObject {
    public var name: String
    public var age: UInt
    public var items: [String]?
    public func hey() -> String
    public init(name: String, age: UInt)
}

위와 같이 [String]으로 되어 있는 것을 알 수 있습니다.

이렇듯 구현을 확인하면서 Swift로부터 이용하기 쉽도록 해 가는 것이 가능합니다.

정리

코드양적으로 봤을 때 그렇게까지 많이 재작성하지 않더라도 Swift에서 사용하기 쉽게 인터페이스를 수정하는 것이 가능합니다.

어떻게 구현되었는지 파악되어 있는 경우에는 이와 같은 Nullability와 Generics를 지정하는 것이 기존의 Objective-C의 코드를 Swift부터 사용하기 쉽게 할 수 있다는 것에 틀린 부분은 없다고 생각합니다.

단지, 이것들을 바꾸는 것은 이외로 간단하게 되는 것은 아닌 것 같은 인상을 줍니다. 이유라고 한다면 한 부분에만 nullablenonnull을 지정하는 것이 불가능하다거나 나름대로 큰 클래스가 된다면 nullable이라거나 nonnull인 것을 간단하게 판단할 수 없는 경우가 많아진다는 인상을 주기 때문입니다.

Objective-C의 경우 기본적으로는 nullable이 된다고 생각하지만, Swift에서 이용할 때에는 Optional로써 취급되지 않으면 안되기 떄문에 단순하게 nullable로 바꾸는 것에는 큰 강점을 느낄 수 없습니다.

그래도 Optional로써 취급되어지는 것이 강점이라고 생각하며 AnyObject의 캐스팅이 줄어든다는 것은 Swift코드를 작성해나가는데 큰 강점이 될 것이라 생각합니다.

참 고