1. 간단한 개념
먼저 ObejctC에서는 파일.h 에다가 클래스를 선언하고 파일.m 에다가 클래스를 구현해야 한다
아래는 클래스의 선언 예시이다. 헤더파일에다가 작성해야 한다.
@interface MyClass : NSObject
@end
다음은 구현부이다. .m 파일에 작성해야 한다.
@implementation MyClass
구현부
@end
그리고 h파일은 외부에 공개되는 public이고, m파일은 private인 영역인점을 기억하자.
즉 구현부(m파일)에 헤더파일에서 선언하지 않은 함수를 작성하면, 그것은 private 메소드가 된다.
다만 이런방식으로 private 메소드를 만드는건 권장되지 않고 이후에 배울 클래스 익스텐션을 쓰는게 좋다.
2. 멤버변수 선언
ObjectC에다가 멤버변수를 선언하는 방법은 두가지가 있다.
1. 인스턴스 변수
2. 프로퍼티
인스턴스 변수는 옛날방식이다. 프로퍼티는 인스턴스 변수를 대체하기위해 나온 새로운 방식이다. 고로 프로퍼티를 쓰자
인스턴스 변수로 멤버변수를 선언하는 방법은 중괄호 안에다가 작성하면 된다.
인터페이스에다 작성하면 protected이고 구현부에다 작성하면 private형태로 된다.
@interface Person : NSObject
{
NSString* firstName;
NSString* lastName;
}
@end
@implementation Person {
NSString *_anotherCustomInstanceVariable;
}
@end
프로퍼티로 변수를 선언하는 방법은 @property 키워드를 이용한다.
@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
둘의 차이가 뭔데?
인스턴스 변수는 기본적으로 private이다. 외부에 노출되지 않는다. 그리고 getter setter를 직접 만들어 주어야 한다.
프로퍼티의 경우 자동으로 getter setter가 생성된다. 그리고 여러가지 키워드를 붙여 기능을 추가할 수 있다.
프로퍼티의 getter setter 자동생성은 다음 rule을 따른다
- getter의 경우 프로퍼티의 이름과 동일하다. 즉 firstName의 getter함수 이름은 firstName이다.
아래와 같이 접근이 가능하다.
NSString *firstName = [somePerson firstName];
- setter의 경우 set으로 시작하며 프로퍼티 이름을 capitalized하게(첫글자만 대문자화) 만든다. 즉 firstName의 setter는 setFirstName이 된다.
- setter를 만들어주고 싶지 않고, read-only하게 만들고 싶다면 readonly 키워드를 붙이면 된다. 기본값은 readwrite이다.
@property (readonly) NSString *fullName;
- getter의 이름을 커스텀하게 만들고 싶으면 명시적으로 지정해주면 된다. setter의 경우 같은 기능을 제공하지 않고 함수로 직접 커스텀하게 만들어줘야한다.
@property (getter=isFinished) BOOL finished;
3. 프로퍼티와 인스턴스변수의 깊은 이해
사실 프로퍼티는 인스턴스 변수를 외부에 노출시키기 위해 탄생한 방법이고 원래는 아래와 같이 @synthesis를 이용하여 인스턴스 변수를 외부에 노출 시키려고 만들어진 기법이다.
@interface Person : NSObject
{
NSString *instanceVariableName;
}
@property NSString *propertyName;
@end
//아래는 파일.m에 작성되어야 함
@implementation Person
@synthesize propertyName = instanceVariableName;
@end
위 처럼 작성하면 propertyName을 이용하여 외부에서 instanceVariableName에 접근 가능하다.
허나 프로퍼티만 선언 해 줄 경우(그리고 readwrite로 선언된경우), Object-C에서는 컴파일러에서 자동으로 인스턴스 변수를 생성해주고 이를 프로퍼티와 synthesize 해준다.
이때 생성된 인스턴스 변수 이름은 프로퍼티 이름에 _ (underscore)가 붙는다.
즉 firstName으로 readwrite 키워드로 프로퍼티를 선언하면, _firstName이라는 인스턴스변수가 생성되는 것 이다.
자동으로 생성된 인스턴스 변수는, 인스턴스 메소드에서 사용가능하다. 그러나 보통 getter나 self키워드로 접근하는게 일반적이다.
즉 _firstName 이렇게 접근하는게 아니라 self.firstName으로 접근하는걸 권장한다.
4. 메소드 선언
기본적으로 ObjectC의 함수는 c언어와 형태가 같다. 그러나 클래스의 메소드의 경우 형태가 몹시 다르다
@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
- (void)myFunction1:(SomeType)value1;
- (void)myFunction2:(SomeType)value1 Second:(SomeType)value2;
- (void)myFunction3:(SomeType)value1 Second:(SomeType)value2 Third:(SomeType)value3;
+ (void)thisIsStaticFunction;
@end
메소드선언은 다음과같다
-/+ (리턴타입)함수이름:(변수타입)첫번째변수 NamedArguemnt:(변수타입)두번째변수 NamedArgument:(변수타입)세번째변수
처음 ObjectC를 보고 이게뭐야? 했는데 익숙해지면 생각보다 가독성이 괜찮다.
그냥 이 구조만 주목하자
AAA:(BBB)CCC
파란색은 NamedArgument로 이해해도 무방하다. 즉 함수이름도 NamedArgument로 생각하면 이해가 쉽게된다.
빨간색은 파라미터의 변수타입이다
회색은 함수내부에 사용될 파라미터의 변수 이름이다.
호출방법은 다음과 같다. ObjectC에서는 특이하게 대괄호를 이용하여 함수를 호출하는데 이를 보통 메세지 패싱이라고 한다.
[ 리시버 함수명:첫번째값 두번째NamedArgument:두번째값 세번째NamedArgument:세번째값 .... ]
여기서 리시버는 호출할 객체를 가리킨다.
연습을 해보자. -/+ 는 이 함수가 static인가 아닌가를 나타낸다.
1. 파라미터를 받지 않는 멤버함수를 만들어보자
- (void)myFunction
호출방법은 다음과 같다
[ myClass myFunction ]
2. 함수인자 하나를 받는 멤버함수를 만들어보자
선언은 다음과 같다.
- (void)myFunction:(int)val1
구현은 아래와 같다
- (void)myFunction:(int)val1 {
NSLog(@"Param1: %d", val1);
}
호출방법은 다음과 같다
[ myClass myFunction:10 ]
3. 함수인자를 세개받는 멤버함수를 만들어보자
위에 내용을 이해했으면 이제 쉽다. 선언은 다음과 같다.
- (void)myFunction:(int)val1 Second:(int)val2 Third:(int)val3
호출은 아래와 같다
[ myClass myFunction:10 Second:20 Third:30 ]
5. 클래스의 생성
NSObject 루트 클래스는 alloc이라는 메소드를 제공한다.
+ (id)alloc;
보면 리턴값이 id라는놈이다. 이놈은 대충 클래스를 생성하면 나오는 포인터라고 생각하면 된다.
이 alloc을 init이라는 초기값을 설정해주는 메소드와 같이 쓰는게 일반적이다.
- (id)init;
보면 alloc과 똑같이 id를 리턴한다. 그리고 이 둘을 아래와같이 조합해서 쓰는게 일반적이다.
NSObject *newObject = [[NSObject alloc] init];
아래와 같이 쓰면 절대 안된다고 공식문서에 나와있다. init은 alloc이된 id가 아닌 다른 id를 리턴할 수 있기 때문이라나 뭐라나...
꼭 위 예시와 같이 nested 된 형태로만 사용하라고 권장한다.
NSObject *someObject = [NSObject alloc];
[someObject init];
여기서 init을 보면 의문점이 하나 들 것이다. 파라미터를 받는 함수가 아닌데 외부에서 파라미터를 받아서 초기화 해주려면 어떻게 해야하지? 정답은 커스텀한 init함수를 따로 만들어주어야 한다. 다만 양식에 맞춰서
init은 NSObject에 의해 상속받는다. 아래는 init함수를 오버라이딩 할 때 양식이다.
- (id)init {
self = [super init];
if (self) {
// initialize instance variables here
}
return self;
}
만약 파라미터를 받아서 init을 하고 싶다면 위 양식을 지키면서 커스텀한 함수를 만들어주면 된다.
- (instancetype)initWithParameter:(parameterType)parameterName {
self = [super init];
if (self) {
// 파라미터에 대한 초기화 코드 작성
}
return self;
}
여기서 주목할만한게 instancetype이라는건데 id를 대체하는 내용이다. 애플에서는 id대신에 instancetype을 쓰라고 한다. 그래야 타입체킹이 되고 ide에서도 알아먹기 좋아서 자동완성이 잘되니 어쩌니... 아무튼 시키는대로 하자.
자세한 내용은 아래 Modern Object-C 링크에 나와있다.
6. 카테고리와 익스텐션
ObjectC에서는 기존에 있는 클래스를 확장하는 방법으로 카테고리와 익스텐션이라는 기능을 제공한다.
먼저 카테고리에 대해서 배워보자. 헤더의 양식은 다음과 같다.
@interface ClassName (CategoryName)
@end
구현부의 양식은 다음과 같다.
@implementation ClassName (CategoryName)
구현부
@end
그리고 위 양식을 작성할 파일명 규칙이 있는데 아래와 같은 파일에 저장해야 한다.
ClassName+CategoryName.h (헤더)
ClassName+CategoryName.m (구현부)
예시를 들어보자. 앞서 예제에서 firstName과 lastName의 클래스를 정의했다.
이를 아래와 같은 형태의 NSString으로 만들어주고 싶다면?
Appleseed, John
Doe, Jane
Smith, Bob
Warwick, Kate
아래와 같이 헤더를 작성해보자.
Human+MyCategory.h
#import "Human.h"
@interface Human (MyCategory)
- (NSString *)additionalMethod;
@end
그리고 구현부를 다음과같이 구현해준다.
Human+MyCategory.m
#import "Human+MyCategory.h"
@implementation Human (MyCategory)
- (NSString *)lastNameFirstNameString {
return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName];
}
@end
그리고 카테고리로 추가된 메소드를 사용하려면 카테고리 헤더를 임포트 해주어야 한다
#import "Human+MyCategory.h"
@implementation SomeObject
- (void)someMethod {
Human *person = [[Human alloc] initWithFirstName:@"John"
lastName:@"Doe"];
ChildHuman *shoutingPerson =
[[ChildHuman alloc] initWithFirstName:@"Monica"
lastName:@"Robinson"];
NSLog(@"The two people are %@ and %@",
[person Human], [ChildHuman lastNameFirstNameString]);
}
@end
위 예제에서 보면 카테고리는 자식 클래스에서도 사용이 가능하다는걸 알 수 있다.
여기서 한가지 주의사항이 있다. 아래는 애플 공식문서에 나온 내용이다.
Categories can be used to declare either instance methods or class methods but are not usually suitable for declaring additional properties. It’s valid syntax to include a property declaration in a category interface, but it’s not possible to declare an additional instance variable in a category. This means the compiler won’t synthesize any instance variable, nor will it synthesize any property accessor methods. You can write your own accessor methods in the category implementation, but you won’t be able to keep track of a value for that property unless it’s already stored by the original class.
카테고리에서는 프로퍼티는 선언이 가능하지만, 인스턴스 변수는 선언이 불가능하다. 이는 컴파일러가 프로퍼티와 인스턴스 변수를 synthesize 하지 않음과 accessor method(getter나 setter)를 synthesize 하지 않음을 의미한다. 직접 accessor method는 구현 가능하나, 오리지널 클래스에서 값을 이미 store하지 않은 이상, 프로퍼티 value값을 추적하지 못할것이다.
라고 한다.
뭔말인지 모르겠으나 프로퍼티는 아무튼 쓰지 마라 라는 얘기
그럼 프로퍼티와 인스턴스 변수를 추가하고 싶다면? 익스텐션을 쓰면 된다.
익스텐션을 배우기전에 카테고리에서 몇가지 주의사항을 숙지하자
- 카테고리 함수 이름을 오리지널 클래스에 있는 메소드와 같은 이름을 쓰지 말아야 한다.
- 같은 오리지널 클래스에 속한 카테고리들은 같은 메소드이름을 쓰지 말아야 한다
- 위와 같은 상황이 일어날 경우, 무슨 함수가 불릴지 모른다
익스텐션
익스텐션은 익명 카테고리라고 불린다. 이유는 다음과 같이 이름이 없는 카테고리를 만들면 되기 때문이다.
@interface ClassName ()
@end
카테고리와 가장 큰 차이점은. 아래 두 예제처럼 프로퍼티와 인스턴스 변수를 추가할 수 있다는 점이다.
그리고 코드 구현은 오리지널 클래스와 같은곳에서 하면 된다. 즉 .m파일에다 해야하는것!
@interface MyClass ()
@property NSObject *extraProperty;
@end
@interface MyClass () {
id _someCustomInstanceVariable;
}
...
@end
7. 익스텐션의 정확한 이해
익스텐션은 아까 .m 파일에다가 정의해야 한다고 했다. 즉 헤더파일에는 정의가 안되니 외부에 노출되지 않는다.
(ObjectC는 컴파일 하면 m파일은 컴파일 되어 외부에서 볼 수 없고 헤더는 그대로 튀어나온다고 한다..
이를 달리 말하면 private한 프로퍼티와 private한 메소드를 만드는데 사용된다는 것이다!
그리고 프로퍼티마저 override 가능하다. 헤더파일에서 readonly로 프로퍼티를 선언했는데, 구현부에서는 익스텐션을 이용하여 readwrite로 바꿀 수 있다.
허나 헤더파일에서는 readonly이므로 외부에서는 readonly로 밖에 접근하지 못한다. 다만 클래스 내부적으로는 프로퍼티를 readwrite로 접근하여 값을 변경하거나 수정 가능하다.
아래는 애플 공식문서에 나온 예제이다.
사람의 주민등록번호를 외부에는 readonly로 지정한다. 그리고 assignUniqueIdentifier는 주민등록번호 프로퍼티가 값이 없을경우 값을 부여하는 메소드이다. 허나 프로퍼티가 readonly이기에 자동으로 인스턴스 변수가 생성되지 않는다.
@interface XYZPerson : NSObject
...
@property (readonly) NSString *uniqueIdentifier;
- (void)assignUniqueIdentifier;
@end
위 예제에서 아래처럼 익스텐션으로 readwrite로 바꿔줄 수 있다.
@interface XYZPerson ()
@property (readwrite) NSString *uniqueIdentifier;
@end
@implementation XYZPerson
...
@end
이렇게 되면 클래스 구현부 내부에서는 uniqueIdentifier를 readwrite로 생각하니 인스턴스변수도 생성되고 값도 수정가능하다. 하지만 외부에는 readonly로 인식된다.
헌데 여기까지 배웠다면 몇가지 의문점이 생길것이다
1. 인스턴스 변수 쓰는거랑 익스텐션에 프로퍼티 선언하는거랑 뭔차이인가? 둘다 private 아닌가?
인스턴스 변수의 경우 헤더파일에 노출되기 때문에 다른사람이 코드를 가져가면 인스턴스 변수의 경우 노출이 된다.
2. 헤더파일에 정의하지 않고 구현부에 바로 구현해서 private메소드로 쓰는거와 클래스 익스텐션에 메소드를 정의하는건 무슨차이지?
이게 너무 궁금해서 다 찾아봤는데... 명확하게 답을 내놓은 사람이 없다. 기능적으로 별 차이 없는것이 대부분의 의견이다.
그리고 오브젝트C의 경우 명시적으로 private을 지원하지 않아서, 강제로 호출하면 또 호출이 된다고 한다. 다만 헤더파일에서 가려놓아서 다른사림이 못보는것일 뿐
개인적인 의견으론 클래스 익스텐션을 쓰는 이유는 코드상의 가독성 때문이라고 생각한다. 내가 헤더에다가 함수를 정의하지 않고 구현부에 바로 구현해서 private 메소드를 만들어 놓으면, 이게 구현부 코드만 볼 때 private인지 아닌지 구분하기가 힘들다.
대신 클래스 익스텐션을 쓰면 이 영역들은 전부다 private이라는걸 명확하게 알 수 있다는 장점이 있다.
'iOS' 카테고리의 다른 글
iOS 디벨로퍼 계정없이 무료 빌드하는법 (0) | 2024.03.19 |
---|---|
iOS에서 로그 볼때 OR, NOT 필터 거는법 (0) | 2024.02.08 |
[iOS] Static Library만들기와 사용 (1) | 2024.02.08 |
iOS를 처음 시작하며 겪는 삽질들 (0) | 2023.05.03 |
스위프트 배우기 1부 - 스위프트 투어 (1) | 2023.03.15 |
댓글