SendBird

Objective-C 프로젝트를 Swift로 Converting하며 배운 교훈들

Avatar
Jed Gyeong Software Engineer - Applications (iOS)
Share

Get Started!

1,000 MAU and 25 connections free forever

Start Now

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)

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]

결론

  1. Objective-C에 비해서 Swift language의 Type Casting 규칙이 더 엄격한듯 합니다. 이건 Xcode의 교정 기능도 제대로 고쳐주지 못하니 유의할 필요가 있습니다.
  2. Class의 designated initializer와 convenience initializer 두 가지의 특성에 대하여 잘 이해하고 들어가여 변환 과정에서의 많은 고통의 시간을 줄일 수 있습니다.
  3. Xcode의 자동 완성/교정 기능이 항상 옳은 것은 아니지만 문법 오류를 비교적 잘 교정하기 때문에 Swift 문법을 익히는데 유용합니다. 따라서 맹신하면 안되고 Swift Language Guide를 종종 살펴봐야 합니다.
  4. Objective-C에서 사용했던 클래스와 동일한 이름의 클래스를 Swift에서 사용할 때 같은 기능을 하는 메소드가 이름이 다를 경우도 있어서 각 클래스의 레퍼런스 문서 또한 참조할 필요가 있습니다.

아직 Objective-C만 사용하는 iOS 개발자들이 Swift에 빠르게 적응하고 싶다면 기초적인 Swift 문법을 익힌 뒤 기존의 Objective-C 프로젝트를 Swift로 converting하는 작업을 해 볼 것을 추천합니다.

무운을 빕니다! 🙂

* 본 글을 기고한 Jed 는 센드버드의 소프트웨어 엔지니어입니다. 트위터에서 Jed를 Follow 해보세요. 

Tags: SendBird