SBM blog CTA mobile 1

옴니채널 비즈니스 메시징으로 당신의 비즈니스를 성장 시키고 비용을 줄이세요

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

Blog Cover lessons learned from converting object C into swift
Oct 20, 2017 • 6 min read
Jed 1
Jed Gyeong
Software Engineer - Applications (iOS)
SBM blog CTA mobile 1

옴니채널 비즈니스 메시징으로 당신의 비즈니스를 성장 시키고 비용을 줄이세요

SBM blog CTA mobile 1

옴니채널 비즈니스 메시징으로 당신의 비즈니스를 성장 시키고 비용을 줄이세요

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)

U Ikit Mobile content offer background

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]

결론

  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 해보세요.

Ebook Grow Mobile content offer background

비즈니스 성과로 이어지는 디지털 커뮤니케이션

센드버드와 함께, 지금 바로 시작해보세요