UINavigationBar는 tintColor를 이용해서 색상을 바꿀 수 있다. 하지만, 제목이나 버튼의 글자 색깔을 바꾸는 기능은 제공하지 않고 있다. 이건 참 곤란하다. tintColor를 밝은 색으로 주면 제목과 버튼 글자가 잘 보이지 않는다.
비록 정상적인 방법은 아니지만, 글자 색깔을 바꾸어 보았다.
#include <objc/runtime.h>
#include <objc/message.h>
@implementation UIKitHack (UINavigationTextColor)
/*
* -[UINavigationItemView drawText:inRect:]
*
* 이 메소드를 갈아치워서 UINavigationBar의 Title Text Color를 바꿀 수 있다.
*/
static void UINavigationItemView_drawTextInRect(id self, SEL _cmd, NSString *string, CGRect rect)
{
CGPoint point = rect.origin;
UIFont *font = [self performSelector:@selector(_defaultFont)];
/*
* Draw shadow of string
*/
point.y += 2;
[[UIColor whiteColor] set];
[string drawAtPoint:point forWidth:rect.size.width withFont:font lineBreakMode:UILineBreakModeTailTruncation];
/*
* Draw string
*/
point.y -= 1;
[[UIColor blackColor] set];
[string drawAtPoint:point forWidth:rect.size.width withFont:font lineBreakMode:UILineBreakModeTailTruncation];
}
/*
* -[UIButtonLabel textColor]
*
* 이 메소드를 추가하여 Button Text Color를 바꿀 수 있다.
*/
static UIColor *UIButtonLabel_textColor(id self, SEL _cmd)
{
if ([[self superview] isKindOfClass:NSClassFromString(@"UINavigationButton")])
{
return [UIColor blackColor];
}
else
{
struct objc_super super = { self, [UILabel class] };
return objc_msgSendSuper(&super, _cmd);
}
}
/*
* -[UIButtonLabel shadowColor]
*
* 이 메소드를 추가하여 Button Text Shadow Color를 바꿀 수 있다.
*/
static UIColor *UIButtonLabel_shadowColor(id self, SEL _cmd)
{
if ([[self superview] isKindOfClass:NSClassFromString(@"UINavigationButton")])
{
return [UIColor whiteColor];
}
else
{
struct objc_super super = { self, [UILabel class] };
return objc_msgSendSuper(&super, _cmd);
}
}
/*
* -[UIButtonLabel setShadowOffset:]
*
* 이 메소드를 갈아치워서 Button Text Shadow Offset을 바꿀 수 있다.
*/
static IMP UIButtonLabel_original_setShadowOffset;
static void UIButtonLabel_setShadowOffset(id self, SEL _cmd, CGSize offset)
{
if ([[self superview] isKindOfClass:NSClassFromString(@"UINavigationButton")])
{
struct objc_super super = { self, [UILabel class] };
objc_msgSendSuper(&super, _cmd, CGSizeMake(0, 1));
}
else
{
UIButtonLabel_original_setShadowOffset(self, _cmd, offset);
}
}
/*
* 추가하고 갈아치우는 삽질의 마무리
*/
+ (void)load
{
Class class;
Method method;
class = NSClassFromString(@"UINavigationItemView");
if (class)
{
method = class_getInstanceMethod(class, @selector(drawText:inRect:));
if (method)
{
method_setImplementation(method, (IMP)UINavigationItemView_drawTextInRect);
}
else
{
NSLog(@"-[UINavigationItemView drawText:inRect:] method not found");
}
}
else
{
NSLog(@"UINavigationItemView class not found");
}
class = NSClassFromString(@"UIButtonLabel");
if (class)
{
class_addMethod(class, @selector(textColor), (IMP)UIButtonLabel_textColor, "@@:");
class_addMethod(class, @selector(shadowColor), (IMP)UIButtonLabel_shadowColor, "@@:");
method = class_getInstanceMethod(class, @selector(setShadowOffset:));
if (method)
{
UIButtonLabel_original_setShadowOffset = method_setImplementation(method, (IMP)UIButtonLabel_setShadowOffset);
}
else
{
NSLog(@"-[UIButtonLabel setShadowOffset:] method not found");
}
}
else
{
NSLog(@"UIButtonLabel class not found");
}
}
@end
정상적인 방법이 아니므로, 사용 및 응용은 알아서 하시기를...
이 글은 스프링노트에서 작성되었습니다.
이 포스팅 제목 뽑기 참 힘들었다... 지금도 마음에 안 들지만, 그냥 할련다. 내가 원래 그렇지...
iPhone Application 개발을 하다보면 가끔 참 황당하기 짝이없는 API들을 보는 경우가 있는데, 오늘 소개할 것은 UIActionSheet와 UIAlertView다. 사실, 이 둘은 생긴 모양은 다르지만, API생김새는 거의 똑같다. 그러므로 예는 모두 UIActionSheet를 사용하겠다.
먼저, UIActionSheet를 띄워보자.
UIActionSheet *actionSheet;
actionSheet = [[UIActionSheet alloc] initWithTitle:@"뭐하실래요?"
delegate:self
cancelButtonTitle:@"됐어요"
destructiveButtonTitle:@"약주세요"
otherButtonTitles:@"밥사주세요", @"술사주세요", nil];
[actionSheet showInView:[self view]];
[actionSheet release];
이제는 UIActionSheetDelegate 메소드 중 마음에 드는 것을 하나 골라 어떤 버튼이 눌렸는지 확인하여 실행에 옮길 차례다.
- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex
{
if (buttonIndex == [actionSheet destructiveButtonIndex])
{
[self drug];
}
else if (buttonIndex == [actionSheet firstOtherButtonIndex])
{
[self dinner];
}
else if (buttonIndex == ([actionSheet firstOtherButtonIndex] + 1))
{
[self drink];
}
}
만약, 버튼이 더 추가되거나 순서가 바뀌거나 하면... 여러 종류의 ActionSheet를 띄워야하는 ViewController라면... 생각만 해도 악몽이다. UIActionSheet의 designated initializer가 아니라 -addButtonWithTitle: 메소드를 사용하여 버튼을 추가하면서 각각의 button index를 가지고 있게 하면 그나마 낫겠지만, 그것도 그리 마음에 드는 방법은 아니다.
그래서, 별 수 없이 button title을 key로, 메소드 셀렉터를 value로 하는 dictionary를 만들어 사용해봤다.
static NSDictionary *actionSheetActions = nil;
+ (void)initialize
{
if (!actionSheetActions)
{
actionSheetActions = [[NSDictionary alloc] initWithObjectsAndKeys:
@"drug", @"약주세요",
@"dinner", @"밥사주세요",
@"drink", @"술사주세요",
nil];
}
}
- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex
{
if (buttonIndex != [actionSheet cancelButtonIndex])
{
SEL selector = NSSelectorFromString([actionSheetActions objectForKey:[actionSheet buttonTitleAtIndex:buttonIndex]]);
if (selector)
{
[self performSelector:selector];
}
}
}
낡고 느린 방법이긴 하지만, 나는 이게 더 좋아보인다. 아... 이렇게까지 해야하는지...
PS) 이 예제에서 사용한 코드는 보기 쉽게 하기 위해 일부러 NSLocalizedString()을 사용하지 않고 한국어 타이틀을 코드에 집어넣었다.
이 글은 스프링노트에서 작성되었습니다.
NSFileHandle이나 NSURLConnection은 Async I/O를 위해 내부적으로는 쓰레드를 사용하지만, I/O 이벤트가 발생했을 때 delegate를 호출해주는 편리한 인터페이스를 제공하고 있다. 다만, delegate 객체는 진행중인 I/O에 대한 상태관리(?)를 해야한다. 상태관리라니 뭔가 거창하게 해야하는 건 아니고, 현재까지 수행해야할 또는 완료된 I/O 데이터를 가지고 있어야 한다든지, 오류가 발생했을 때 대처해야 한다던지... 뭐, 그런 것들...
문제는 이러한 I/O가 동시 다발적 또는 산발적으로 마구 발생하는 경우는 일일이 다루기가 귀찮아질 수 있다. 그래서, NSOperationQueue를 이용해 보기로 했다. 다음 예제는 NSOperation의 서브클래스로, NSURLConnnection을 이용하여 서버에서 데이터를 다운받아 넘기는 간단한 것이다.
@interface URLDownload : NSOperation
{
NSURLConnection *connection;
NSMutableData *data;
BOOL executing;
BOOL finished;
id delegate;
SEL selector;
}
/*
* delegate method의 signature는 다음과 같다.
* - 리턴타입: void
* - 첫번째 인자: download operation 객체 (URLDownload *)
* - 두번째 인자: 다운받은 데이터 객체 (NSData *)
* - 세번째 인자: 에러 객체 (NSError *)
*/
- (id)initWithRequest:(NSURLRequest *)aRequest delegate:(id)aDelegate selector:(SEL)aSelector;
@end
@implementation URLDownload
- (id)initWithRequest:(NSURLRequest *)aRequest delegate:(id)aDelegate selector:(SEL)aSelector
{
self = [super init];
if (self)
{
connection = [[NSURLConnection alloc] initWithRequest:aRequest delegate:self startImmediately:NO];
delegate = aDelegate;
selector = aSelector;
}
return self;
}
- (void)dealloc
{
[connection release];
[data release];
[super dealloc];
}
#pragma mark overrides for concurrent operation
/*
* concurrent operation을 위해서는 다음의 메소드를 오버라이드해야 한다.
* - isConcurrent
* - isExecuting
* - isFinished
* - start
*/
- (BOOL)isConcurrent
{
return YES;
}
- (BOOL)isExecuting
{
return executing;
}
- (BOOL)isFinished
{
return finished;
}
- (void)start
{
if ([self isCancelled])
{
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
}
else
{
[self willChangeValueForKey:@"isExecuting"];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];
}
}
#pragma mark others
/*
* cancel 메소드를 오버라이드하여 NSURLConnection의 수행을 cancel시키고 operation을 종료한다.
*/
- (void)cancel
{
[super cancel];
[connection cancel];
if ([self isExecuting])
{
/*
* NSOperationQueue의 cancelAllOperations에 의해 cancel될 때
* [self stop] 을 하면 아직 cancel되지 않은 operation들이 수행을 시작할 수 있기 때문에
* performSelector:withObject:afterDelay: 메소드를 이용하여 stop시킨다.
*/
[self performSelector:@selector(stop) withObject:nil afterDelay:0];
}
}
/*
* executing과 finished property를 종료 상태로 바꿔주기 위한 편의성 메소드
*/
- (void)stop
{
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
#pragma mark NSURLConnectionDelegate
/*
* 응답이 들어오면 데이터를 저장하기 위한 공간을 생성한다.
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
long long length = [response expectedContentLength];
if (length == NSURLResponseUnknownLength)
{
length = 0;
}
data = [[NSMutableData alloc] initWithCapacity:length];
}
/*
* 데이터가 들어오면 저장해 놓는다.
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)receivedData
{
[data appendData:receivedData];
}
/*
* 오류가 발생하면 nil data와 함께 오류를 delegate에 전달하고 operation을 종료한다.
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSData *nilData;
NSInvocation *invocation;
if (![self isCancelled])
{
nilData = nil;
invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:"v@:@@@"]];
[invocation setTarget:delegate];
[invocation setSelector:selector];
[invocation setArgument:&self atIndex:2];
[invocation setArgument:&nilData atIndex:3];
[invocation setArgument:&error atIndex:4];
[invocation invoke];
}
[self stop];
}
/*
* 데이터 로딩이 끝나면 데이터를 delegate에 전달하고 operation을 종료한다.
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSError *nilError;
NSInvocation *invocation;
if (![self isCancelled])
{
nilError = nil;
invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:"v@:@@@"]];
[invocation setTarget:delegate];
[invocation setSelector:selector];
[invocation setArgument:&self atIndex:2];
[invocation setArgument:&data atIndex:3];
[invocation setArgument:&nilError atIndex:4];
[invocation invoke];
}
[self stop];
}
@end
사용은 다음과 같이 한다.
- (NSOperationQueue *)operationQueue
{
return operationQueue; // NSOperationQueue를 적절하게 미리 만들어 두어야 한다.
}
- (void)downloadDataWithURL:(NSURL *)aURL
{
URLDownload *download;
download = [[URLDownload alloc] initWithRequest:[NSURLRequent requestWithURL:aURL] delegate:self selector:@selector(urlDownload:data:error:)];
[[self operationQueue] addOperation:download];
[download release];
}
/*
* 다운로드가 끝나면 이 메소드가 호출된다.
* 오류가 발생했을 때 data는 nil이 들어온다.
*/
- (void)urlDownload:(URLDownload *)aDownload data:(NSData *)data error:(NSError *)error
{
if (data)
{
// 다운로드 완료
}
else
{
NSLog(@"download error: %@", error);
}
}
이 글은 스프링노트에서 작성되었습니다.
NULL이라함은 보통 "값이 없음"을 말한다.
C언어에서 NULL은 사실 그냥 0으로 정의되어 있는데, 포인터변수에 NULL을 사용하여 레퍼런스하는 대상이 없다는 의미로 사용한다. Objective-C에서는 객체 포인터 타입인 id에서 사용하기 위해 따로 nil을 정의하고 있다. nil도 사실 0으로 정의되어 있기 때문에 이넘이나 그넘이나 같은 넘이다.
Objective-C가 C나 C++과 다른 점 중에 하나는 이 nil이 단순히 레퍼런스하는 객체가 없다는 의미 외에도 "nil object"라고까지 표현할만큼 특별한 기능(?)을 발휘하고 있다는 점이다. 바로 어떠한 메세지도 받아들이고(respond) 결과로 자기 자신을 리턴한다는 것이다.(자세한 내용은 여기참조) 즉, 다음과 같은 코드도 런타임에서 어떠한 에러도 발생하지 않고 잘 실행된다. 물론 아무것도 하지 않지만.
[nil killYourself];
Java나 Smalltalk도 예외를 발생시키지만, Objective-C는 그렇지 않다. 이런 특별한 기능(?)이 복잡한 예외처리를 필요로 하는 경우나 디버깅할 때 귀찮을 경우도 있지만, 코드를 단순화시키기 쉽고 잘 죽지 않는(허허.. 이거 좋은 건지...) 프로그램을 만들 수 있다... (응?)
그런데....
NSNull 이라는 것도 있다. 코코아의 컬렉션 객체들이 nil 즉, NULL 포인터를 저장할 수 없기 때문에 존재하는, nil은 아닌 Null 객체다. (헉.. 이건, 뭐...) 문제는 NSNull 객체는 nil이 가지고 있는 특별한 기능(?)이 없다는 것이다. 즉, 다음과 같은 코드는 시키는 대로 아주 잘 죽는다... 쩝.
[[NSNull null] killYourself];
그래서, nil과 NSNull 객체가 둘 다 들어올 수 있는 코드에서는 객체 포인터를 미리 잘 확인해봐야 한다. 다음과 같이...
if (anObj && ![anObj isKindOfClass:[NSNull class]])
{
// 이제 anObj를 마음껏 사용한다.
}
매번 if문을 저렇게 쓰는 것이 싫다면 nil이나 NSNull 객체가 아닌지 확인하는 메소드를 다음과 같이 구현할 수 있겠다.
@interface NSObject (NullTesting)
- (BOOL)isNotNull;
@end
@implementation NSObject (NullTesting)
- (BOOL)isNotNull
{
return [self isKindOfClass:[NSNull class]] ? NO : YES;
}
@end
또는, 이렇게.
@interface NSObject (NullTesting)
- (BOOL)isNotNull;
@end
@implementation NSObject (NullTesting)
- (BOOL)isNotNull
{
return YES;
}
@end
@implementation NSNull (NullTesting)
- (BOOL)isNotNull
{
return NO;
}
@end
그러면, 다음과 같이 if 문의 조건을 쉽게 쓸 수 있다.
if ([anObj isNotNull])
{
// 이제 anObj를 마음껏 사용한다.
}
주의할 점은 isNull 메소드는 불가능하다. 왜냐하면 nil은 항상 nil(0)만 리턴하기 때문이다.
if문조차 싫다면 Objective-C의 message forwarding을 이용해서 NSNull이 nil 처럼 모든 메세지를 무시하도록 만들 수 있겠다.
@implementation NSNull (NilObject)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
return [NSMethodSignature signatureWithObjCTypes:"@@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
id ret = nil;
[anInvocation setReturnValue:&ret];
}
@end
이제는 좀 마음이 편해질려나...
이 글은 스프링노트에서 작성되었습니다.
Mac OS X Application에 custom font를 embed하는 것은 매우 쉽다. 다음과 같이 하면 된다.
-
target에서 copy files build phase를 추가하고 Destination: Resourecs, Path: Fonts 와 같이 세팅한다.
(알아보기 쉽게 이 build phase의 이름을 Copy Fonts와 같이 주면 더 좋겠다.) - project의 Info.plist에 ATSApplicationFontsPath (Application fonts resource path) 를 위에서 만든 build phase의 path인 Fonts로 세팅한다.
- project에 폰트파일을 추가하고 폰트파일을 1단계에서 만든 build phase에 추가해준다.
너무 간단하다. 아마 손이 빠르신 분은 1분도 안 걸릴듯... (이외에 다른 Info.plist 키들에 대한 설명은 Apple의 Runtime Configuration Guidelines 문서를 참고)
하지만, 이것은 Application Bundle일 때에만 사용할 수 있다. 만약, Application이 아니라 Framework이나 Loadable Bundle일 경우는 이 방법이 통하지 않는다. 하지만, 다음의 비교적 간단한 코드를 사용하면 쉽게 자신의 Bundle에 포함되어 있는 embedded font들을 로딩할 수 있다.
- (void)loadFonts
{
NSArray *paths;
NSString *path;
FSRef fileRef;
OSStatus status;
paths = [[NSBundle bundleForClass:[self class]] pathsForResourcesOfType:nil inDirectory:@"Fonts"];
for (path in paths)
{
CFURLGetFSRef((CFURLRef)[NSURL fileURLWithPath:path], &fileRef);
status = ATSFontActivateFromFileReference(&fileRef,
kATSFontContextLocal,
kATSFontFormatUnspecified,
NULL,
kATSOptionFlagsDefault,
NULL);
if (status != noErr)
{
NSLog(@"error %d returned while loading embedded font file: %@", status, path);
}
}
}
이 글은 스프링노트에서 작성되었습니다.
아주 가끔 문자열의 Bezier Path를 얻어오고 싶을 때가 있다.
여기저기 참고해서 한번 만들어 보았다.
#import <Cocoa/Cocoa.h>
@interface LayoutManager : NSLayoutManager
{
NSBezierPath *bezierPath;
}
+ (NSBezierPath *)bezierPathWithAttributedString:(NSAttributedString *)string inRect:(NSRect)rect;
@end
@implementation LayoutManager
- (id)init
{
self = [super init];
if (self)
{
bezierPath = [[NSBezierPath bezierPath] retain];
}
return self;
}
- (void)dealloc
{
[bezierPath release];
[super dealloc];
}
- (NSBezierPath *)bezierPath
{
return [[bezierPath retain] autorelease];
}
- (void)showPackedGlyphs:(char *)glyphs length:(NSUInteger)glyphLen glyphRange:(NSRange)glyphRange atPoint:(NSPoint)point font:(NSFont *)font color:(NSColor *)color printingAdjustment:(NSSize)printingAdjustment
{
/*
* 이 메소드는 LayoutManager가 글자를 그리기 위해 호출한다.
* 여기에서 그냥 Bezier Path에다 바로 그린다.
*/
[bezierPath moveToPoint:point];
[bezierPath appendBezierPathWithPackedGlyphs:glyphs];
}
+ (NSBezierPath *)bezierPathWithAttributedString:(NSAttributedString *)string inRect:(NSRect)rect
{
NSTextContainer *textContainer;
NSTextStorage *textStorage;
LayoutManager *layoutManager;
NSImage *tmpImage;
NSBezierPath *theBezierPath;
NSAffineTransform *transform;
NSAffineTransformStruct tfStruct;
NSRect usedRect;
/*
* Bezier Path를 그리기 위해 LayoutManager를 구성한다.
* 이 때, NSTextContainer는 인자로 받은 rect의 size를 사용한다.
*/
textContainer = [[NSTextContainer alloc] initWithContainerSize:rect.size];
textStorage = [[NSTextStorage alloc] initWithAttributedString:string];
layoutManager = [[LayoutManager alloc] init];
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
/*
* 화면용 폰트의 힌트를 사용하지 않게 한다.
*/
[layoutManager setUsesScreenFonts:NO];
/*
* LayoutManager가 글자를 그릴 공간을 만든다.
*/
tmpImage = [[NSImage alloc] initWithSize:NSMakeSize(10.0, 10.0)];
/*
* LayoutManager의 좌표계는 뒤집혀 있으므로 이미지의 좌표계를 뒤집어 준다.
*/
[tmpImage setFlipped:YES];
/*
* 이미지에 글자를 그린다.
* 실제로는 Bezier Path에 그려지므로 이 이미지에는 아무것도 그려지지 않는다.
*/
[tmpImage lockFocus];
[layoutManager drawGlyphsForGlyphRange:[layoutManager glyphRangeForTextContainer:textContainer] atPoint:NSZeroPoint];
[tmpImage unlockFocus];
/*
* 그려진 Bezier Path를 얻어온다.
*/
theBezierPath = [layoutManager bezierPath];
/*
* 좌표계에서 그려진 범위를 가져온다.
*/
usedRect = [layoutManager usedRectForTextContainer:textContainer];
/*
* Bezier Path에 적용할 AffineTransform을 만든다.
* 다음의 transform을 수행하게 된다.
* 1. 뒤집힌 좌표계를 다시 뒤집어 복구 (flip)
* 2. 인자로 받은 rect안으로 translate (이 때, X축은 기준점, Y축은 중앙으로 정렬)
*/
transform = [NSAffineTransform transform];
tfStruct.m11 = 1.0;
tfStruct.m12 = 0.0;
tfStruct.tX = NSMinX(rect);
tfStruct.m21 = 0.0;
tfStruct.m22 = -1.0;
tfStruct.tY = (NSHeight(rect) + NSHeight(usedRect)) / 2.0 + NSMinY(rect);
[transform setTransformStruct:tfStruct];
/*
* Bezier Path에 생성한 AffineTransform을 적용한다.
*/
[theBezierPath transformUsingAffineTransform:transform];
/*
* 생성한 객체들을 소멸시킨다.
*/
[tmpImage release];
[textStorage release];
[textContainer release];
[layoutManager release];
/*
* Bezier Path를 리턴한다.
*/
return theBezierPath;
}
@end
만약, 인자로 넘긴 rect안에서 X축 정렬이나 Y축 정렬을 바꾸고 싶으면 AffineTransformStruct의 멤버값을 적당히 바꿔주면 되겠다. 나머지는 주석으로 충분히 설명되었으리라 믿고 추가 설명은 생략... 흐흐..
이 글은 스프링노트에서 작성되었습니다.
Mac OS X 10.5부터 NSOperationQueue를 이용하여 Thread Pool 기반의 Worker Thread Model을 아주 쉽게 만들 수 있게 되었다. 이것은 priority, dependency까지 설정할 수 있기 때문에 범용으로 사용할 수 있을 뿐 아니라 인터페이스도 매우 쉽게 되어 있다.
내가 필요한 것은 백그라운드 쓰레드 하나가 그냥 기다리고 있다가 특정 메소드 호출만 처리해 주는 단순한 것인데, NSOperationQueue는 좀 뒤뚱한 짓을 하고 있었다. MaxConcurrentOperationCount를 1로 주었는데, 내부적으로 쓰레드풀을 관리한답시고 고작 쓰레드 하나를 만들었다가 없앴다가 반복하는 등...
하지만, 제대로 만들기 위해 메세지큐 만들고, condition lock하고 signal 보내고... 싫었다. 그래서, NSRunLoop을 이용해서 정말 간단하게 하나 만들어 보았다.
#import <Foundation/Foundation.h>
@interface WorkerThread : NSObject
{
NSThread *thread;
NSLock *lock;
}
- (void)start;
- (void)stop;
- (void)performSelector:(SEL)aSelector target:(id)target withObject:(id)anObject;
@end
@implementation WorkerThread
- (id)init
{
self = [super init];
if (self)
{
thread = [[NSThread alloc] initWithTarget:self selector:@selector(main) object:nil];
lock = [[NSLock alloc] init];
}
return self;
}
- (void)dealloc
{
[thread release];
[lock release];
[super dealloc];
}
- (void)main
{
NSAutoreleasePool *mainPool;
NSAutoreleasePool *loopPool;
mainPool = [[NSAutoreleasePool alloc] init];
[lock lock];
[self performSelector:@selector(self) onThread:thread withObject:nil waitUntilDone:NO];
while (![thread isCancelled])
{
loopPool = [[NSAutoreleasePool alloc] init];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
[loopPool release];
}
[lock unlock];
[mainPool release];
}
- (void)start
{
[thread start];
}
- (void)stop
{
[thread cancel];
[lock lock];
[lock unlock];
}
- (void)performSelector:(SEL)aSelector target:(id)target withObject:(id)anObject
{
[target performSelector:aSelector onThread:thread withObject:anObject waitUntilDone:NO];
}
@end
NSRunLoop은 input source로부터 input을 기다리게 되는데, 만약, input source가 없다면 run loop이 종료된다. 48번째 라인은 자기자신에게 self를 호출하는 의미없는 라인처럼 보이지만, 이 때 message send에 대한 input source가 등록된다.
한가지 깔끔하지 못한 것은 48번째 라인에 의해 등록된 input source를 해제할 방법이 없다는 것이다. 그렇기 때문에, 플래그로 1초마다 쓰레드를 종료해야할 지 말지 결정한다. 만약, 이 쓰레드가 프로그램이 종료할 때까지 멈출 필요가 없다면 while문을 없애고 다음과 같이 바꿔도 무방하다.
[[NSRunLoop currentRunLoop] run];
이 글은 스프링노트에서 작성되었습니다.
가끔 메소드가 어떻게 호출되고 있는지 로그로 남겨서 확인해 보고 싶을 때가 있다. 매번 확인하고 싶은 메소드 시작부분마다 메소드 이름을 로그로 출력해 주는 코드를 끼워넣는 것은 좀 귀찮은 일이다. 그래서 매크로를 한번 만들어 봤다.
Log.h
#define LogMethod() LogMethodBody(self, _cmd, __FILE__, __LINE__) #define LogFunction() LogFunctionBody(__PRETTY_FUNCTION__, __FILE__, __LINE__)
Log.m
#import <Foundation/Foundation.h>
void LogMethodBody(id self, SEL _cmd, const char *file, int line)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (self == [self class])
{
NSLog(@"%@:%d: +[%@ %@]",
[[NSString stringWithCString:file encoding:NSUTF8StringEncoding] lastPathComponent],
line,
NSStringFromClass(self),
NSStringFromSelector(_cmd));
}
else
{
NSLog(@"%@:%d: -[%@ %@] (%p)",
[[NSString stringWithCString:file encoding:NSUTF8StringEncoding] lastPathComponent],
line,
NSStringFromClass([self class]),
NSStringFromSelector(_cmd),
self);
}
[pool release];
}
void LogFunctionBody(const char *function, const char *file, int line)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(@"%@:%d: %s()",
[[NSString stringWithCString:file encoding:NSUTF8StringEncoding] lastPathComponent],
line,
function);
[pool release];
}
이렇게 하면 삽질을 좀 줄일 수 있다. 흐흐.
이전 포스트에서 Objective-C 메소드의 경우 method signature라는 것이 있다는 설명을 했었다. 그래서 Objective-C Runtime API를 이용하면 메소드 호출시 넘어온 인자들까지 확인가능하다. 이 부분은 생략하도록 하겠다. ㅋㅋ
이 글은 스프링노트에서 작성되었습니다.
먼저 다음과 같은 메소드가 있다.
- (void)drawImage:(NSImage *)image withFrame:(NSRect)frame inView:(NSView *)view
{
NSSize size = [image size];
NSRect rect = NSMakeRect(0, 0, size.width, size.height);
[view lockFocus];
[image drawInRect:frame fromRect:rect operation:NSCompositeCopy fraction:1.0];
[view unlockFocus];
}
"메소드"란 위의 소스 그 자체라 말할 수 있다. 즉, 메소드 이름, 인자 갯수, 각 인자들의 데이터타입, 리턴값의 데이터타입, 그리고 그 내용으로 구성된다. 위의 소스는 이 모든 것들을 모두 표현하고 있다. 그럼, 이 메소드의 구성 요소들을 용어로 정리해 보자.
| 설명 | 예제 값 | |
| selector | 메소드의 이름 | drawImage:withFrame:inView: |
| method signature (type encoding) | 인자들 및 리턴값의 타입정보 | v@:{_NSRect={_NSPoint=ff}{_NSSize=ff}}@ |
| implementation | 실행코드 | 함수 포인터 |
자, 이제 이 메소드를 다음과 같이 호출해 보자.
[myCell drawImage:myImage withFrame:NSMakeRect(0, 0, 32, 32) inView:myView];
이렇게 메소드를 호출하는 것을 Objective-C에서는 "메세지를 보낸다(send message)"라고 한다. 메세지는 다음과 같은 네가지로 구성된다.
| 설명 | 예제 값 | |
| receiver | 메세지를 받는 객체 | myCell |
| selector | 호출될 메소드의 이름 | drawImage:withFrame:inView: |
| arguments | 호출시 넘어가는 인자들 | myImage, NSMakeRect(0, 0, 32, 32), myView |
| return value | 호출후 넘어오는 값 | void |
Objective-C 런타임 및 Cocoa 프레임워크에서는 위의 용어들을 다음과 같이 추상화해 놓았다.
| Objective-C Runtime | Cocoa Framework | |
| selector | SEL, @selector() | SEL, @selector() |
| method signature (type encoding) | const char * | NSMethodSignature |
| implementation | IMP | IMP |
| send message | objc_msgSend() | NSInvocation, [NSObject performSelector:withObject:] |
이 글은 스프링노트에서 작성되었습니다.
Objective-C는 C++에 비하면 상당히 간단한 객체지향언어라 할 수 있다. 특징은 C++이 메소드 바인딩(method binding)시에 기본적으로 정적 바인딩(static binding)을 하고 가상함수(virtual function)을 이용해 동적 바인딩(dynamic binding)을 할 수 있는데 반해, Objective-C는 기본적으로 동적 바인딩을 하고, 정적 바인딩은 지원하지 않는다는 것이다. (정적 바인딩을 지원하지 않기 때문에 보다 간단하다라고 주장하는 것인가? 설마... ㅋㅋ)
하지만, Objective-C가 C의 superset이라는 점에서 불가능할 것이 있겠는가? 어디 Objective-C에서도 정적 바인딩을 해볼까나? 흐흐.
#import <Foundation/Foundation.h>
@interface BaseObject : NSObject
- (void)sayHello;
@end
@interface MyObject : BaseObject
@end
@implementation BaseObject
- (void)sayHello
{
NSLog(@"BaseObject: hello.");
}
@end
@implementation MyObject
- (void)sayHello
{
NSLog(@"MyObject: hello.");
}
@end
void sayHelloDynamic(BaseObject *obj)
{
[obj sayHello];
}
void sayHelloStatic(BaseObject *obj)
{
IMP sayHello;
sayHello = [[BaseObject class] instanceMethodForSelector:@selector(sayHello)];
sayHello(obj, @selector(sayHello));
}
int main()
{
BaseObject *obj = [[MyObject alloc] init];
sayHelloDynamic(obj);
sayHelloStatic(obj);
[obj release];
return 0;
}
뭐, 엄밀히 말하면 정말로 정적바인딩을 한 것은 아니라 할지 몰라도, 흉내는 내었다. 이런 걸 사용할 일이 없을 것이지만, 굳이 할려면 이렇게 하면 가능은 하다는 것이 요점이다. 이뭥미?
이 글은 스프링노트에서 작성되었습니다.


