이 글은 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
을 허용한다는 것을 명시하기 위한 형수식자입니다.
예를 들면, NSData
의 initWithContentsOfURL:
이니셜라이져는 아래와 같이 정의되어 있어서 리턴값이 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_BEGIN
과 NS_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_BEGIN
과 NS_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부터 사용하기 쉽게 할 수 있다는 것에 틀린 부분은 없다고 생각합니다.
단지, 이것들을 바꾸는 것은 이외로 간단하게 되는 것은 아닌 것 같은 인상을 줍니다. 이유라고 한다면 한 부분에만 nullable
과 nonnull
을 지정하는 것이 불가능하다거나 나름대로 큰 클래스가 된다면 nullable
이라거나 nonnull
인 것을 간단하게 판단할 수 없는 경우가 많아진다는 인상을 주기 때문입니다.
Objective-C의 경우 기본적으로는 nullable
이 된다고 생각하지만, Swift에서 이용할 때에는 Optional로써 취급되지 않으면 안되기 떄문에 단순하게 nullable
로 바꾸는 것에는 큰 강점을 느낄 수 없습니다.
그래도 Optional로써 취급되어지는 것이 강점이라고 생각하며 AnyObject
의 캐스팅이 줄어든다는 것은 Swift코드를 작성해나가는데 큰 강점이 될 것이라 생각합니다.
참 고
- https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html
- https://developer.apple.com/swift/blog/?id=31
- https://developer.apple.com/videos/play/wwdc2015-401/
- https://developer.apple.com/swift/blog/?id=25
- http://www.slideshare.net/GoichiHirakawa/new-objectivec-features-for-swift-20
- http://qiita.com/hironytic/items/16920fb0a5c8d8127e10
- http://qiita.com/yimajo/items/e31496e575fe576649d3