본문 바로가기

iOS+

Objective-C

개요

이 글은 Objective-C에 대해 공부하며 메모한 글이다

 

모든 내용을 자세하게 정리하지 않았지만, 기본적으로 C를 알고있는 상태에서 Objective-C에 대해 배울 때, 해당 언어만 독특하게 가지고 있는 요소들을 중심적으로 정리했다

 

  • Objective-C와 Swift
  • Method와 Function
  • Struct
  • Class
  • Category와 Extension
  • Protocol
  • Block
  • Error handling

 

Objective-C와 Swift

Apple은 2014년에 Swift를 도입했지만, 많은 기존 프로젝트와 라이브러리가 Objective-C로 작성되어 있었기 때문에 두 언어 간의 원활한 상호 작용이 필수적이었다

이를 위해 Apple은 다음과 같은 상호 운용성(interoperability) 메커니즘을 구현했다

브리징 헤더(Bridging Header)

브리징 헤더는 Swift 프로젝트에서 Objective-C 코드를 사용할 수 있게 해주는 특별한 헤더 파일이다

이 파일에서 Swift 코드에서 사용하고 싶은 Objective-C 헤더 파일들을 import 한다

 

// MyProject-Bridging-Header.h
#import "MyObjectiveCClass.h"

 

이렇게 하면 Swift 코드에서 이 Objective-C 클래스들을 직접 사용할 수 있다

 

// Swift Code
let myObjCInstance = MyObjectiveCClass()
myObjCInstance.someMethod()

Objective-C 런타임

Swift는 자체 런타임을 가지고 있으며, Objective-C와는 독립적으로 동작하지만, Objective-C와의 상호 운용성을 지원하기 위해 Objective-C 런타임과 상호 작용할 수 있다

 

예를 들어, Swift에서 @objc 속성을 사용하면 해당 요소를 Objective-C 코드에서 사용할 수 있게 된다

 

// Swift Code
@objc class MySwiftClass: NSObject {
    @objc func myMethod() {
        print("This method can be called from Objective-C")
    }
}

 

이제 Objective-C 코드에서 이 Swift 클래스를 사용할 수 있다

 

// Objective-C Code
MySwiftClass *swiftInstance = [[MySwiftClass alloc] init];
[swiftInstance myMethod];

 

Swift 클래스가 Objective-C 코드에서 사용되기 위해서는 Objective-C의 런타임 시스템과 호환되어야 하는데, 이 호환성을 제공하는 것이 바로 NSObject 클래스

 

Swift에서 NSObject를 상속받은 클래스는 Objective-C의 런타임 시스템에서 인식될 수 있다

@objc 속성은 Swift 코드가 Objective-C 런타임에서 사용 가능하도록 하는 역할을 하지만, 이 속성만으로는 Objective-C에서 완벽하게 호환되지 않을 수 있다

 

// Swift Code
import Foundation

@objc class MySwiftClass: NSObject {
    @objc var name: String
    
    @objc init(name: String) {
        self.name = name
    }
    
    @objc func greet() {
        print("Hello, \\(name)!")
    }
}

 

// Objective-C 코드에서 Swift 클래스를 사용
#import "MyProject-Swift.h"

MySwiftClass *swiftObject = [[MySwiftClass alloc] initWithName:@"World"];
[swiftObject greet];

 

Method와 Function

- (returnType) methodName : (argumentType) argumentName

 

Objective-C에서 메서드 관련해서 위와같은 형태를 많이 볼 텐데 일단 이 형태를 인지해둬야 한다

첫 번째 인자는 나머지 인자들과 달리 메서드 이름이다

 

- (return_type) method_name:( argumentType1 ) argumentName1 
joiningArgument2:( argumentType2 ) argumentName2 ... 
joiningArgumentn:( argumentTypen ) argumentNamen {
    // body of the function
}

 

아래와 같이 변환된 Swift 코드를 보면 더 이해하기 쉬울 것이다

 

func methodName(argumentName1: argumentType1, 
                argumentName2: argumentType2, 
                argumentNameN: argumentTypen) -> return_type {
    // body of the function
}

 

참고로, 함수는 일반적인 다른 프로그래밍 언어와 동일하다

 

returnType functionName(parameterType parameterName) {
		/// ...
    return returnValue;
}

 

Struct

Objecitve-C에서 구조체는 C와 동일하다

 

struct [structure tag] {
   member definition;
   member definition;
   ...
   member definition;
} [one or more structure variables];  

// 예시
struct Books {
   NSString *title;
   NSString *author;
   NSString *subject;
   int   book_id;
} book;

books.book_id = 1234

 

기억해야 할 만한 점은 C와 같이 포인터 구조체의 변수는 로 접근해서 처리하는 것이다

 

@interface SampleClass:NSObject
- (void) printBook:( struct Books *) book ;
@end

@implementation SampleClass 
- (void) printBook:( struct Books *) book {
   NSLog(@"Book title : %@\\n", book->title);
   NSLog(@"Book author : %@\\n", book->author);
   NSLog(@"Book subject : %@\\n", book->subject);
   NSLog(@"Book book_id : %d\\n", book->book_id);
}

@end

int main() {
   struct Books Book1
 
   Book1.title = @"Objective-C Programming";
   Book1.author = @"Nuha Ali"; 
   Book1.subject = @"Objective-C Programming Tutorial";
   Book1.book_id = 6495407;
   
}

 

Class

Objective-C에서 클래스정의부, 구현부가 존재한다

Class 부분에선 이것들과 클래스 객체를 생성하고 제거하는 방법에 대해 정리한다

 

Objective-C 클래스에서 -는 인스턴스 메서드를 나타내며, +는 클래스 메서드를 나타낸다

 

@interface Car : NSObject {
    /// 인스턴스 변수 (직접 접근 가능, 외부 접근 불가)
    NSString *engineType;
    int numberOfDoors;
}

/// 프로퍼티로 선언된 인스턴스 변수
@property (nonatomic, assign) int speed;

/// engineType getter/setter 메서드 선언
- (void)setEngineType:(NSString *)type;
- (NSString *)getEngineType;

@end

@implementation Car

- (void)setEngineType:(NSString *)type {
    engineType = type;
}

- (NSString *)getEngineType {
    return engineType;
}

@end

정의부(@interface)

정의부는 클래스의 청사진을 제공하며, 클래스의 속성과 메서드를 선언한다

이때 @interface를 사용하여 정의한다

 

@interface Car : NSObject {
    /// 인스턴스 변수 (직접 접근 가능, 외부 접근 불가)
    NSString *engineType;
    int numberOfDoors;
}

/// 프로퍼티로 선언된 인스턴스 변수
@property (nonatomic, assign) int speed;

/// engineType getter/setter 메서드 선언
- (void)setEngineType:(NSString *)type;
- (NSString *)getEngineType;
@end

 

프로퍼티로 선언되지 않은 인스턴스 변수는 외부에서 직접 접근이 불가능하다!

그래서 따로 getter/setter 메서드를 구현해야 하는데, @property를 이용하면 자동으로 구현해 준다

@property

인스턴스 변수에 대한 getter/setter를 자동으로 구현해 준다(readwrite으로 기본값이 설정되어 있다)

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray *items;
@property (nonatomic, weak) id delegate;
@property (nonatomic, assign) int count;
@property (nonatomic, readonly) NSString *identifier;
@property (nonatomic, strong, readonly) NSString *name;

이때 첫 번째 인자는 thread-safe인가 아닌가에 대한 설정(기본값 atomic)이고, 두 번째는 아래와 같다

  • copy - 값을 복사하여 저장 (문자열에 주로 사용)
  • strong - 강한 참조 (ARC에서 기본값)
  • weak - 약한 참조 (순환 참조 방지에 사용)
  • assign - 단순 할당 (기본 데이터 타입에 사용)
  • readonly - 읽기 전용 속성(기본값 readwrite)

구현부(@implementation)

구현부는 클래스의 실제 동작을 정의하는데 정의부에서 선언한 메서드의 구체적인 내용을 구현한다

이때 @implementation를 사용하여 구현한다

 

@implementation Car

- (instancetype)initWith:(NSString *)type {
    self = [super init];
    engineType = type;
    return self;
}

- (instancetype)init {
    self = [super init];
    engineType = @"defaultType";
    return self;
}

- (void)setEngineType:(NSString *)type {
    engineType = type;
}

- (NSString *)getEngineType {
    return engineType;
}

정의부 예시코드에서 @property로 정의하지 않는 engineType은 다음과 같이 직접 getter/setter 메서드를 구현한다

생성자

idinstancetype를 반환타입으로 이용하여 생성자를 정의한다(생성자의 마지막 부분은 return self)

NSObject의 상속한다면, 기본 생성자 수행을 하고 원하는 작업을 하면 수행하면 된다

객체 생성과 소멸

  • [[ClassName alloc] init], [ClassName new]
  • objectName = nil

 

객체 생성 시, NSObject의 alloc 메서드를 통해 메모리를 할당받고, init 메서드를 수행시키면서 객체를 생성한다

객체가 소멸될 때는 dealloc 메서드가 실행된다

 

int main(void) {

    Car *myCar = [[Car alloc] init];
		/// 코드...

		myCar = nil;  /// @autoreleasepoool을 이용한다면 명시적으로 해제안해도 된다
		
		return 0;
}

 

retain, release 같은 MRR에 사용되던 메서드는 ARC를 사용하는 이제는 더 이상 사용되지 않는다

 

Category와 Extension

Category

카테고리는 Swift의 extension 느낌이다

 

@interface로 클래스 정의처럼 코드를 작성하는데, 상속은 없고 클래스 명 뒤에 (CategoryName)을 붙여서 정의한다

이후 @implementation을 통해 구현 부분을 작성하면 된다

 

아래의 예시코드는 MyAdditions라는 카테고리를 만들어 NSString을 확장한다

 

@interface NSString(MyAdditions)

+(NSString *)getCopyRightString;

@end

@implementation NSString(MyAdditions)

+(NSString *)getCopyRightString {
   return @"Copyright HS";
}

@end

int main(int argc, const char * argv[]) {
   NSString *copyrightString = [NSString getCopyRightString];
   NSLog(@"Accessing Category: %@",copyrightString);
   
   return 0;
}

Extension

익스텐션클래스의 비공개(private) 인터페이스를 정의하는 방법이다

 

주로 클래스의 내부 구현을 숨기거나, 인스턴스 변수를 추가할 때 사용되는데, 익명의 카테고리가 곧 익스텐션이다

익스텐션은 보통 같은 파일 내에서 선언되어야 하며, 익스텐션에 선언된 메서드나 프로퍼티는 해당 파일에서만 사용할 수 있다

 

@interface MyClass : NSObject

@property (nonatomic, strong) NSString *publicProperty;

- (void)publicMethod;

@end

 

만약 위와같이 MyClass를 정의하고 아래와 같이 메서드를 구현했다면, MyClass에 정의되어있지 않은 privateMethod 메서드는 외부에서 사용할 수 없다

#import "MyClass.h"

// 익스텐션(익명 카테고리)를 통해 비공식적인 속성과 메서드 정의
@interface MyClass ()

@property (nonatomic, strong) NSString *privateProperty;
- (void)privateMethod;

@end

@implementation MyClass

// public 메서드 구현
- (void)publicMethod {
    NSLog(@"This is a public method.");
    self.privateProperty = @"This is a private property";
    [self privateMethod];
}

// private 메서드 구현
- (void)privateMethod {
    NSLog(@"This is a private method. Private property: %@", self.privateProperty);
}

@end

 

 

Protocol

Delegate 패턴의 기반이 되는 프로토콜은 Swift에서 프로토콜과 달리 구현이 옵셔널로 가능하다!

  • 델리게이터(Delegator) - 자신의 행동을 다른 객체에게 위임하는 객체
  • 델리게이트(Delegate) - 델리게이터 객체의 요청을 받아서 해당 행동을 구현하는 객체

정의는 @protocol로 하며, 구현 여부를 @required와 @optional로 지정할 수 있다

 

@protocol ProtocolName
@required
// list of required methods
@optional
// list of optional methods
@end

 

그리고 프로토콜을 채택하는 부분은 다음과 같이 <ProtocolName>를 이용한다

 

@interface MyClass : NSObject <MyProtocol>
...
@end

프로토콜을 사용하는 Delegate 패턴 예시

아래 예시는 PrintProtocolDelegate에 processCompleted 메서드가 존재하는데

PrintProtocolDelegate를 준수하는 SampleClass를 만들었을 때,

PrintClass는 SampleClass 안(하위)에 위치하는데 Delegate를 설정하여

SampleClass는 설정한 Delegate가 사용할 메서드를 구현한다

 

그렇다면 PrintClass는 자신의 행동을 SampleClass에 위임한 Delegator고,

SampleClass는 프로토콜을 채택하고 메서드를 구현했기에 Delegate

 

@interface PrintClass:NSObject {
   id delegate;
}

- (void) printDetails;

- (void) setDelegate:(id)newDelegate;

@end

@implementation PrintClass

- (void)printDetails {
   NSLog(@"Printing Details");
   [delegate processCompleted];   /// 등록한 델리게이트의 메서드 수행
}

- (void) setDelegate:(id)newDelegate {
   delegate = newDelegate;
}

@end

 

@protocol PrintProtocolDelegate
- (void)processCompleted;
@end

@interface SampleClass:NSObject<PrintProtocolDelegate>
- (void)startAction;

@end

@implementation SampleClass
- (void)startAction {
   PrintClass *printClass = [[PrintClass alloc]init];
   [printClass setDelegate:self];
   [printClass printDetails];
}

-(void)processCompleted {   /// 프로토콜 메서드 구현
   NSLog(@"Printing Process Completed");
}

@end

 

int main(int argc, const char * argv[]) {
   SampleClass *sampleClass = [[SampleClass alloc]init];
   [sampleClass startAction];

   return 0;
}

 

Block

Objective-C의 블록은 Swift의 클로저 느낌이고, 기본 형태는 다음과 같이 ^를 이용하며 나타낸다

 

returntype (^blockName)(argumentType)= ^{
};

 

아무래도 이런 꼴의 모양을 간략하게 작성하기 위해 typedef와 같이 자주 사용된다

typedef

블록을 간결하게 나타내기 위해 typedef는 아래처럼 사용된다

 

typedef void (^CompletionBlock)(void);

 

위와같은 typedef를 이용한 블록 정의는 스위프트코드로 바꿨을 때, 아래와 같다

 

typealias CompletionBlock = () -> Void

 

참고로 C와 마찬가지로 struct를 간결하게 표현하기 위해서도 사용한다

 

typedef struct {
   NSString *title;
   NSString *author;
   NSString *subject;
   int   book_id;
} Books;

Books book1;

 

인자 두 개를 받아 곱하는 MultiplyTwoValues 블락을 이용하는 예시 코드는 다음과 같다

 

typedef double (^MultiplyTwoValues)(double, double);

int main(int argc, const char * argv[]) {
    MultiplyTwoValues blockFunc  =
       ^(double firstValue, double secondValue) {
          return firstValue * secondValue;
       };

    double result = blockFunc(2,4);
    NSLog(@"The result is %f", result);
    
}

 

아래는 콜백함수를 받아 처리하는 메서드를 블락을 통해 전달하는 예시다

이러한 방식은 자주 사용되므로 잘 기억해야 한다

 

typedef void (^CompletionBlock)();

@interface SampleClass:NSObject

- (void)performActionWithCompletion:(CompletionBlock)completionBlock;

@end

@implementation SampleClass

- (void)performActionWithCompletion:(CompletionBlock)completionBlock {

   NSLog(@"액션 수행 완료");
   completionBlock();
}

@end

int main() {
   
   SampleClass *sampleClass = [[SampleClass alloc]init];
   [sampleClass performActionWithCompletion:^{
      NSLog(@"액션 수행 후 호출");
   }];

}

 

Error handling

NSError

  • Domain − 미리정의된 NSError 도메인이거나 임의 문자열 이어야 함(nil 불가)
  • Code − 에러코드
  • User Info − 에러설명(desc)
NSString *domain = @"com.MyCompany.MyApplication.ErrorDomain";
NSString *desc = NSLocalizedString(@"Unable to complete the process", @"");
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : desc };
NSError *error = [NSError errorWithDomain:domain code:-101 userInfo:userInfo];

 

Objective-C에서 에러처리를 하기 위해서는 NSError를 메서드 외부에서 이중포인터로 받고,

메서드를 수행하다가 오류가 발생하면 해당 포인터에서 에러 내용을 업데이트한다

동작 수행 후, 에러가 nil인지 확인하여 알맞은 처리를 하면 된다

 

@interface SampleClass:NSObject

-(NSString *) getEmployeeNameForID:(int) id withError:(NSError **)errorPtr;

@end

@implementation SampleClass

-(NSString *) getEmployeeNameForID:(int) id withError:(NSError **)errorPtr {
   if(id == 1) {
      return @"Employee Test Name";
   } else {
      NSString *domain = @"com.MyCompany.MyApplication.ErrorDomain";
      NSString *desc =@"Unable to complete the process";
      NSDictionary *userInfo = [[NSDictionary alloc]
                                initWithObjectsAndKeys:desc,
                                @"NSLocalizedDescriptionKey",NULL];
      *errorPtr = [NSError errorWithDomain:domain 
																			     code:-101 userInfo:userInfo];
      return @"";
   }
}

int main(void) {
	    SampleClass *sampleClass = [[SampleClass alloc]init];
	    NSError *error = nil;
	    NSString *name1 = [sampleClass getEmployeeNameForID:1 withError:&error];
	    
	    if(error) {
	        NSLog(@"Error finding Name1: %@",error);
	    } else {
	        NSLog(@"Name1: %@",name1);
	    }
}

 

기타

메서드의 호출

Objective-C에서 메시지 전달은 객체 간 통신의 기본이다

Objective-C는 이를통해 객체의 메서드를 호출한다

 

이때 메시지 구조는 [receiver message]의 형태로 구성된다

receiver는 메시지를 받는 객체이고, message는 수행할 작업이다

이런 요청은 컴파일 시점이 아닌 런타임에 실제 호출될 메서드가 결정되어 높은 유연성을 제공한다

 

[myObject doSomething];

 

이 코드는 "myObject에게 doSomething이라는 작업을 해달라고 요청한다"는 의미다

실제로 어떤 메서드가 호출될지는 런타임에 myObject의 실제 클래스를 바탕으로 결정된다

NSObject

해당 클래스를 상속하면 다음과 같은 이점이 있다

  • 메모리 관리 - ARC
  • 런타임 지원 - 동적타이핑, 메시지전달
  • KVC(Key-Value Coding) - 객체 속성에 동적 접근
  • KVO(Key-Value Observing) - 객체 속성 변경 관찰
  • 기본 메서드 제공 - description, hash, isEqual…

id와 instancetype

id는 포인터를 나타내는 타입으로, 메서드의 반환 타입이 명시되어 있지 않으면, Objective-C에서는 기본적으로 id 타입을 반환한다

 

instancetype는 id와 달리 컴파일러가 정확한 반환 타입을 추론할 수 있게 해 주며, 메서드가 반환하는 객체의 실제 타입을 반환 타입으로 지정한다

 

// id 생략해도 기본적으로 id다
- (id)init {
    return self;
}

- (instancetype)init {
    return self;
}

@

@는 Objective-C에서 문자열 리터럴을 나타내는 데 사용된다

 

NSString *greeting = @"Hello, World!";
NSLog(@"%@", greeting);

NSAutoreleasePool과 @autoreleasepool

NSAutoreleasePool은 수동으로 메모리 관리를 하던 시절에 사용하던 방식으로, 객체를 autorelease 큐에 넣어 나중에 메모리를 해제하는 역할이다

이때 [pool drain]은 풀에 있는 객체들을 해제시켜 주기 때문에 명시적으로 객체의 소멸자를 작성하지 않아도 된다

 

int main(int argc, const char * argv[]) {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    MyClass *mc = [[MyClass alloc] init];
    mc.name = @"HS";
    [mc printName];
    
    [mc release]
    [pool drain];
    return 0;
}

 

하지만 ARC 모드에서는 메모리 관리가 자동으로 이루어지기 때문에, NSAutoreleasePool을 사용할 필요가 없고 실제로 사용할 수도 없다

ARC 모드에서는 NSAutoreleasePool을 사용하는 대신, @autoreleasepool 블록을 사용해야 한다

int main(int argc, const char * argv[]) {
    @autoreleasepool {
		    MyClass *mc = [[MyClass alloc] init];
		    mc.name = @"HS";
		    [mc printName];
		}
}

 

Foundation - Data Storage

가장 마지막에 nil로 끝을 나타내는 특징이 존재한다

NSArray

[[NSArray alloc]initWithObjects: 값1, 값2, ..., nil];

NSDictionary

[[NSDictionary alloc] initWithObjectsAndKeys: 값1, 키1, 값2, 키2, ..., nil];

 

마무리

이 글은 Objective-C 코드를 읽는데 최소한의 노력으로 도움이 될 만하게 정리한 글이다

Firebase를 이용한 GoogleSignIn의 동작원리가 궁금해서 구현 코드를 보니 Objective-C로 작성되어 있었다

 

이 언어를 사용하여 코드를 작성할 일이 있을까 싶지만, 분석을 해야 할 일은 앞으로 있지 않을까 생각하며 시간이 남았을 때 간단하게 공부하며 정리했다

 

이 글의 내용을 어느정도 알게 되었기에, 코드 분석을 할 수 있게 되었고 그 구현에 대한 궁금증이 해결되었다

 

메모

  • 스위프트 코드로 변환해서 확인할 수 있는 사이트
  • XCode에서 Objective-C 코드 작성
    • XCode → MacOS → Command Line Tool → Objective-C

'iOS+' 카테고리의 다른 글

Xcode Cloud와 Sparkle framework  (2) 2024.09.29
WidgetKit  (0) 2024.09.15
macOS 앱 배포 정리  (1) 2024.08.31
Cannot preview in this file  (0) 2024.08.18
UserNotifications  (0) 2024.08.11