Objective-C 프로젝트를 Swift로 Converting하며 배운 교훈들
Sendbird는 iOS, Android, Web, Xamarin, Unity 등 다양한 플랫폼의 Sample UI를 제공하고 있습니다. iOS의 경우에는 Objective-C로 구현된 Sample UI만을 제공하고 있었으나 고객의 요구에 따라 Swift로 구현된 Sample UI를 제공할 필요가 있어 이미 구현된 Objective-C Sample UI를 Swift로 converting하는 작업을 진행하였습니다.
이 과정에서 Objective-C와 Swift 사이의 차이로 인해 겪은 시행착오를 공유하여 다른 분들이 시간을 조금 더 아낄 수 있지 않을까 하여 이 글을 작성합니다.
Sendbird의 Sample UI는 Interface Builder를 사용하지 않고 모든 UI를 Programmatically하게 구현하였습니다. 따라서 이 글은 Interface Builder를 사용할 경우에는 일부 맞지 않는 부분이 포함되어 있습니다.
프로젝트 다운로드
Sendbird Sample UI 프로젝트는 Github Repository에서 다운로드할 수 있습니다. Objective-C 프로젝트와 Swift 프로젝트가 하나의 Repository에 있으며 두 프로젝트의 코드를 비교하며 보시길 권장합니다.
UIView의 Subclass 초기화
iOS 개발을 하는 과정에서 UI 구현을 위해 UIView를 Subclassing해야합니다. 이 때 UIView의 init 메소드를 override해야 하는데, Objective-C와 Swift 사이에 차이가 있습니다.
Objective-C로 구현한 UIView의 Subclass에서는 일반적으로는 필요한 init 메소드만을 override해도 문제가 없습니다. UIView의 frame을 지정하여 초기화하려면 아래와 같이 initWithFrame:frame을 override합니다:
@implementation SubUIView
– (id) initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self != nil) {
// …
}
return self;
}
@end
Swift에서 위와 동일한 init 메소드를 override하려면 추가 작업이 필요합니다.
먼저 CGRect 타입의 frame을 인자로 받는 init 메소드를 override합니다. UIView 문서에서 알 수 있듯이 Swift로 구현할 때는 init(coder:)를 반드시 override해야 합니다.
단, 이 메소드를 사용할 필요가 없으므로 아래와 같이 처리합니다. Class의 property 초기화에 필요한 구문은 init(frame:) 내에서 구현하면 됩니다.
class SubUIView: UIView { override init(frame: CGRect) { super.init(frame: frame) // ... }
required init?(coder aDecoder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}
}
UIViewController의 Subclass 초기화
UIViewController를 Subclassing하는 것은 iOS 개발을 할 때 필수적인 과정입니다. Interface Builder를 사용한다면 initWithNibName:bundle:를 override해야하지만, 이번 작업에서는 Interface Builder를 사용하지 않고 Programmatically하게 구현하기 때문에 이 메소드를 구현할 필요가 없습니다.
따라서 init 메소드만 override하고, 이 메소드 내부에 Class Property를 초기화하는 구문을 구현합니다.
@implementation SubUIViewController
– (id) init
{
self = [super init];
if (self != nil) {
// …
}
return self;
}
@end
마찬가지로 Swift에서도 init() 메소드를 override하여 구현해야 합니다.
Swift에서는 UIViewController를 Subclassing하기 위해 designated initializer인 init(nibName:bundle:)을 필수적으로 구현해야 합니다. 하지만 Interface Builder를 사용하지 않을 것이기 때문에 nibName과 bundle 값을 정할 필요가 없습니다.
그래서 designated initializer보다 간단한 convenience initializer를 별도로 구현하면서 designated initializer인 init(nibName:bundle:)에는 모두 nil을 설정하였습니다. 이제 이 클래스를 초기화할 때에는 init()를 호출하고, Class의 Property를 초기화하는 구문은 override된 init(nibName:bundle:)에 구현하면 됩니다.
class SubUIViewController: UIViewController { convenience init() { self.init(nibName: nil, bundle: nil) }
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
// Initialize properties of class
}
required init?(coder aDecoder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}
}
이렇게 구현한 UIViewController Subclass는 아래와 같이 생성하고 호출합니다:
let viewController: SubUIViewController = SubUIViewController()
self.navigationController?.pushViewController(viewController, animated: false)
The only UIKit you need.
Auto Layout으로 View 구현
Interface Builder를 사용하지 않을 경우 View의 크기, 위치를 지정하기 위해 Programmatically하게 Auto Layout을 구현해야 합니다. 이를 위해 NSLayoutConstraint Class를 사용하며, Objective-C와 Swift사이에 약간의 차이가 있습니다.
Objective-C에서는 NSLayoutConstraint Class의 constraintWithItem 메소드를 사용합니다.
+ (instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c
Swift에서는 동일한 Class의 init 메소드를 사용합니다.
convenience init(item view1: AnyObject, attribute attr1: NSLayoutAttribute, relatedBy relation: NSLayoutRelation, toItem view2: AnyObject?, attribute attr2: NSLayoutAttribute, multiplier multiplier: CGFloat, constant c: CGFloat)
Objective-C에서는 다음과 같이 구현됩니다. 이 코드는 self.profileImageView와 self 사이의 위치를 규정하는 NSLayoutConstraint를 만든 뒤 self에 추가합니다.
[self addConstraint:[NSLayoutConstraint constraintWithItem:self.profileImageView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1 constant:kMessageCellLeftMargin]];
이와 동일한 Swift 코드는 아래와 같습니다:
self.addConstraint(NSLayoutConstraint.init(item: self.profileImageView!, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.Leading, multiplier: 1, constant: kMessageCellLeftMargin))
두 코드를 비교해보면 Objective-C와 다르게 Swift에서는 NSLayoutConstraint의 init 메소드를 호출한다는 것을 할 수 있습니다. 또한 attribute와 relatedBy에서 사용된 enum 값의 형태가 약간 다릅니다.
NSLayoutConstraint에서 사용되는 enum 값은 다음과 같습니다:
NSLayoutAttribute
Objective-C
typedef enum: NSInteger { NSLayoutAttributeLeft = 1, NSLayoutAttributeRight, NSLayoutAttributeTop, NSLayoutAttributeBottom, NSLayoutAttributeLeading, NSLayoutAttributeTrailing, NSLayoutAttributeWidth, NSLayoutAttributeHeight, NSLayoutAttributeCenterX, NSLayoutAttributeCenterY, NSLayoutAttributeBaseline, NSLayoutAttributeLastBaseline = NSLayoutAttributeBaseline, NSLayoutAttributeFirstBaseline,
NSLayoutAttributeLeftMargin,
NSLayoutAttributeRightMargin,
NSLayoutAttributeTopMargin,
NSLayoutAttributeBottomMargin,
NSLayoutAttributeLeadingMargin,
NSLayoutAttributeTrailingMargin,
NSLayoutAttributeCenterXWithinMargins,
NSLayoutAttributeCenterYWithinMargins,
NSLayoutAttributeNotAnAttribute = 0
} NSLayoutAttribute;
Swift Language
enum NSLayoutAttribute : Int { case Left case Right case Top case Bottom case Leading case Trailing case Width case Height case CenterX case CenterY case Baseline static var LastBaseline: NSLayoutAttribute { get } case FirstBaseline case LeftMargin case RightMargin case TopMargin case BottomMargin case LeadingMargin case TrailingMargin case CenterXWithinMargins case CenterYWithinMargins case NotAnAttribute }
NSLayoutRelation
Objective-C
enum { NSLayoutRelationLessThanOrEqual = -1, NSLayoutRelationEqual = 0, NSLayoutRelationGreaterThanOrEqual = 1, }; typedef NSInteger NSLayoutRelation;
Swift Language
enum NSLayoutRelation : Int { case LessThanOrEqual case Equal case GreaterThanOrEqual }
Selector 지정
UIButton, NSNotificationCenter 또는 NSTimer 등을 사용할 때, 실행될 메소드를 지정하기 위해 selector를 사용해야 합니다.
Objective-C에서 Selector를 사용할 경우는 @selector directive를 사용합니다.
- (void)test { // ... mTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerCallback:) userInfo:nil repeats:YES]; }
– (void)timerCallback:(NSTimer *)timer
{
// …
}
Swift에서는 아래와 같이 별도의 directive를 사용하지 않고 문자열로 메소드 명을 지정합니다.
func test() { // ... self.mTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "timerCallback:", userInfo: nil, repeats: true) // ... }
func timerCallback(timer: NSTimer) {
// …
}
문자열
Objective-C의 문자열 타입인 NSString을 Swift에서 사용해도 상관없으나 UITextField의 text와 같이 property가 String인 객체를 사용하려면 NSString과 String의 사용법 차이를 알아야 할 필요가 있습니다.
Objective-C에서 UITextField의 text는 NSString이기 때문에 length property를 참조하여 문자열의 길이를 구할 수 있습니다.
- (BOOL)textFieldShouldReturn:(UITextField *)textField { NSString *message = [textField text]; if ([message length] > 0) { // ... }
return YES;
}
Swift에서는 length라는 property가 없으며 characters property의 count property를 사용해야 합니다.
func textFieldShouldReturn(textField: UITextField) -> Bool { let message: String = textField.text! if message.characters.count > 0 { // ... }
return true
}
Formatted string을 만들어야 할 경우 Objective-C는 stringWithFormat:을 사용합니다.
[self.typingLabel setText:[NSString stringWithFormat:@"%d Typing something cool....", count]];
Swift의 String에서는 stringWithFormat 메소드가 없으며, init(format:_ arguments:) 메소드를 사용합니다. format에는 NSString과 같은 형식으로 formatted string을 설정하고, arguments에 필요한 값을 설정하면 문자열이 완성됩니다.
self.typingLabel?.text = String.init(format: "%d Typing something cool...", count)
데이터 타입 최대, 최소값
정수 또는 실수 데이터 타입의 최대, 최소값을 얻는 방법 역시 Objective-C와 Swift가 다릅니다. Objective-C에서는 최대, 최소값을 얻기 위해서는 별도로 정의된 매크로를 참조하지만 Swift에서는 데이터 타입에서 바로 가져올 수 있습니다.
Objective-C에서는 다음과 같이 매크로를 사용합니다.
CGFLOAT_MAX CGFLOAT_MIN INT32_MAX INT32_MIN LLONG_MAX LLONG_MIN
Swift에서는 다음과 같이 데이터 타입에서 최대/최소 값을 가져옵니다.
CGFloat.max CGFloat.min Int32.max Int32.min Int64.max Int64.min
Enumeration 값이 포함된 Dictionary
Objective-C에서는 NSAttributedString을 사용할 경우 Attribute를 정의하기 위해 NSDictionary를 사용합니다. Swift에서는 NSDictionary 대신 Dictionary를 사용해야 하는데 Enumeration 값을 Dictionary의 Value로 넣을 때 차이가 있습니다.
Objective-C에서는 다음과 같이 NSDictionary의 Key와 Value를 입력할 수 있습니다. NSUnderlineStyleSingle라는 enum 값은 Value로 직접 사용하지 못하고 @()를 통해 object로 변환한 뒤 사용해야 합니다.
NSDictionary *underlineAttribute = @{NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle)};
Swift에서는 다음과 같이 Dictionary의 Key와 Value를 입력할 수 있습니다. Value를 AnyObject로 정의했을 경우 Objective-C와 마찬가지로 Enumeration 값을 그대로 사용할 수 없고 rawValue property를 사용해야 합니다.
let underlineAttribute: [String: AnyObject] = [NSUnderlineStyleAttributeName: NSUnderlineStyle.StyleSingle.rawValue]
그 외 유용한 팁
위에서 설명한 것 이외에 Sendbird Sample UI 포팅 작업에서 경험한 Objective-C와 Swift 사이의 차이점을 간단히 정리한 표입니다.
* 모바일 뷰에서는 테이블의 스크롤을 좌우로 움직여 자세히 살펴볼 수 있습니다.
[table colwidth=”20|50|50″ colalign=”left|left|left”]
,Objective-C,Swift
Unicode with “\u”,@”\u00A0″,”\u{00A0}”
UIImage,+ (UIImage *)imageNamed:(NSString *)name,init?(named name: String)
UIFont,+ (UIFont *)systemFontOfSize:(CGFloat)fontSize,class func systemFontOfSize(_ fontSize: CGFloat) -> UIFont
UUID Generation,NSString *uuid = [[NSUUID UUID] UUIDString];,let uuid: String = NSUUID.init().UUIDString
[/table]
결론
- Objective-C에 비해서 Swift language의 Type Casting 규칙이 더 엄격한듯 합니다. 이건 Xcode의 교정 기능도 제대로 고쳐주지 못하니 유의할 필요가 있습니다.
- Class의 designated initializer와 convenience initializer 두 가지의 특성에 대하여 잘 이해하고 들어가여 변환 과정에서의 많은 고통의 시간을 줄일 수 있습니다.
- Xcode의 자동 완성/교정 기능이 항상 옳은 것은 아니지만 문법 오류를 비교적 잘 교정하기 때문에 Swift 문법을 익히는데 유용합니다. 따라서 맹신하면 안되고 Swift Language Guide를 종종 살펴봐야 합니다.
- Objective-C에서 사용했던 클래스와 동일한 이름의 클래스를 Swift에서 사용할 때 같은 기능을 하는 메소드가 이름이 다를 경우도 있어서 각 클래스의 레퍼런스 문서 또한 참조할 필요가 있습니다.
아직 Objective-C만 사용하는 iOS 개발자들이 Swift에 빠르게 적응하고 싶다면 기초적인 Swift 문법을 익힌 뒤 기존의 Objective-C 프로젝트를 Swift로 converting하는 작업을 해 볼 것을 추천합니다.
무운을 빕니다! 🙂
* 본 글을 기고한 Jed 는 센드버드의 소프트웨어 엔지니어입니다. 트위터에서 Jed를 Follow 해보세요.