Thoughts2011.11.02 09:29
가상현실 인터페이스가 한 때 뜬 적이 있었습니다. 보통 가상현실을 구현하기 위해 사용하는 인터페이스로는 HMD(Head-Mounted Display)를 썼죠. 주로 안경이나 헬멧 형태로 공급되었는데, 이걸 뒤집어 쓰고 전원을 켠 다음 눈을 뜨면, 눈 앞에 컴퓨터가 제공하는 가상현실이, 마치 실제 환경인양 제공되는 형태였습니다. 모니터 대신인 거죠.

그러다 증강현실이 떴습니다. 뭐 가상현실까지는 진짜 필요 있느냐. 굳이 가상현실이 필요하다면, 우리가 쓰는 단말과 카메라를 이용해서, 우리가 눈으로 보는 풍경에 정보를 덧 입히면 되는 것이 아니겠느냐. 그래서 나온 것이 증강 현실이었죠. 현실의 풍경에다 스마트폰이나 패드를 갖다대면, 그 화면을 통해 현실 시계의 오브젝트들에 정보를 투사하는 것이 증강 현실입니다,  사람이 보는 풍경을 컴퓨터가 제공하는 환경으로 대체해 버리는 가상 현실은, '무엇을 어디에 투사할 것이냐'라는 관점에서 보면 증강 현실과는 180도 다른 접근법을 취하고 있는거라 볼 수 있겠어요.

최근에, iPad와 HMD를 결합해서 가상현실과 유사한 인터페이스를 제공하는 프로젝트가 공개되었습니다.



위의 동영상을 보시면, iPad와 HMD가 연동해서 사용자 눈 앞에 헬리콥터의 형상이 3D로 제공되는 것을 보실 수 있습니다. 증강 현실을 한 단계 발전시킨 거라고 볼 수 있겠습니다. 보통 이와 비슷한 프로젝트들은 증강 현실 이미지를 컴퓨터 안에다 투사하는데 (아래 동영상 참고) 위의 경우에는 HMD를 통해 사용자의 눈에 투사함으로써, 증강 현실이 제공하는 환경이 사용자의 실제 생활 안에 반영된 것 같은 경험을 제공합니다.


이 프로젝트에서 사용된 HMD는 다음과 같은 모습입니다.


예전의 거대하던 HMD와는 비교할 수도 없이 작아졌군요. 예전에는 대략 이정도 크기였죠. 아무리 작아도.


물론 크기라는 것이 '어느 정도의 경험을 제공하느냐'에 좌우되는 부분이니까, 꼭 크다고 나쁘거나 낡았다고 볼 수는 없지만 말이죠. 아무튼 iPad와 HMD를 결합한 증강 현실은, iPad를 통해 (혹은 기타 다른 태블릿을 통해) 정보를 이용하는 사용자에게 또다른 경험을 제공해 줄 것이 분명합니다.

전자책이라면 전자책에서 공룡이 뛰쳐나오는 경험을 아이들에게 제공해 줄 것이 분명해 보이고, CAD 프로그램이라면 작업자에게 결과물을 3D로 조감해 볼 수 있는 환경을 제공할 것이 분명해 보여요. 특히 iPad를 어떻게 움직이느냐에 따라 보는 각도를 달리해 볼 수 있다는 점은, 그 조작계가 아주 직관적이라는 점에서 대단한 강점이 있죠.

이런 프로젝트가 현재 단말 독자적으로만 제공되는 증강 현실 어플리케이션들을 대체할 수 있는지는 좀 더 두고봐야 하겠지만, HMD 가격이 충분히 낮아진다면 가능성이 있을 것 같습니다. 뭣보다, 재미있잖아요.

현재 위에서 보여드린 HMD, iWear VR 920 모델은 엔화 기준으로 40만엔4만엔 정도에 팔리고 있습니다.

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 엔화 40만엔이면 가격이 상당하네요 ㅋㅋㅋ

    2011.11.02 20:02 신고 [ ADDR : EDIT/ DEL : REPLY ]

Languages/Objective-C2011.02.08 11:02
뷰에 그려지는 내용은, 사실 CALayer 객체 위에 그려집니다. 뷰에 대해서 animation을 적용할 수 있듯이, CALayer 객체에 대해서도 animation을 적용할 수 있습니다.

CALayer에 대한 reference를 찾아보시면 아시겠습니다만, CALayer 객체에는 여러 개의 애니메이션 가능한 프라퍼티들이 있습니다. CALayer는 key-value coding을 지원하기 때문에, 키 값을 사용해 이들 프라퍼티에 애니메이션을 지정할 수 있습니다. 

이런 애니메이션은 적용하기도 간단합니다. 물론 복잡한 애니메이션을 구현하려면 좀 심각한 코딩을 해야 하겠습니다만, 레이어 하나 위에 이미지를 그리고 그 이미지가 깜빡거리게 만드는 정도는 대단히 간단하게 처리할 수 있습니다. 아래의 코드를 보시죠.

UIImage* img = [UIImage imageNamed: @"testimg.png"];

CALayer* sub = [CALayer layer];

sub.contents = img.CGImage;

sub.frame = ...

[self.layer addSublayer:sub];

CABasicAnimation* ani = [CABasicAnimation animationWithKeyPath:@"opacity"];

ani.duration = 2.0;

ani.repeatCount = HUGE_VALF;

ani.autoreverses = YES;

ani.fromValue = [NSNumber numberWithFloat:0.0];

ani.toValue = [NSNumber numberWithFloat:1.0];

ani.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut];

[sub addAnimation:ani forKey:@"opacity"];


우선 새 레이어를 하나 만들구요. 그 위에 이미지를 얹습니다. 그런 다음 뷰의 backing layer 위에 해당 레이어를 얹습니다. 그런 다음 애니메이션 객체를 하나 만들어서 해당 애니메이션이 어떻게 실행될 지 지정한 다음에 해당 애니메이션을 새로 만든 레이어에 붙입니다.

유의해서 볼 부분은, 해당 에니메이션이 CALayer의 어떤 프라퍼티에 적용될 것인지를 적용하는 부분입니다. CALayer에는 opacity라는 프라퍼티가 있습니다. 이 프라퍼티는 레이어가 얼마나 투명한지를 결정하는 프라퍼티입니다. 위의 애니메이션은 그 지속 시간(duration)이 2초이며, 영원히(HUGE_VALF) 반복됩니다. opacity 값은 0.0에서 1.0으로 변했다가 다시 원래 상태로 돌아갑니다(autoreverse = YES). 

따라서 위에서 만들어 붙인 레이어에 그려진 이미지는 주기적으로 나타났다 사라지는 것 처럼 보이게 됩니다. 간단하죠?

레이어를 실제로 조작하는 것은 UIView를 가지고 프로그래밍하는 것에 비해 저수준(low-level) 연산처럼 보이기도 합니다만, 잘만 쓰면 뷰를 새로 만들지 않고도 프로그래밍할 수 있기 때문에, 성능 문제를 개선하는 데도 도움을 줄 수 있습니다.

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 나그네

    sub.contents = img.CGImage 부분에서 Cannpt convert CGImage to objc_object in argument passing 이 떠요 ㅠㅠ

    2011.04.22 20:54 신고 [ ADDR : EDIT/ DEL : REPLY ]
  2. 부따부따

    제가 깜박이는 애니메이션이 특정 버튼을 누르면 시작하고,

    이 버튼에 대한 프로세스가 끝나면 원래 상태로 돌아가는 것을 구현 하려고 하는데요

    실시간 영상을 처리하는 가운데 overlay에 대해 해당 애니메이션을 지정해줬는데

    좀처럼 쉽게되질 않네요 도움 주시면 감사하겠습니다.

    2011.07.15 17:36 신고 [ ADDR : EDIT/ DEL : REPLY ]

Languages/Objective-C2011.02.08 09:34
보통 비동기적으로 프로그래밍 한다고 하면 많은 분들이 쓰레드 프로그래밍을 떠올립니다. 네. pThread같이 비교적 알기쉬운 쓰레드 메커니즘을 사용하면 아주 간단하게 비동기적 프로그래밍을 할 수 있죠. 그런데 iPhone이나 iPad는 좀 더 간단한 메커니즘을 제공합니다. 쓰레드 프로그래밍을 단 한줄도 하지 않고서도, 시스템이 제공하는 쓰레드의 도움을 받아 비동기 프로그래밍을 할 수 있죠. 그리고 이렇게 프로그래밍 하면 프로그램을 구성하는 클래스 사이에 의존 관계를 꽤 간단히 끊어버릴 수 있습니다.

결론부터 이야기하자면 NSNotification을 이용하자는 것인데요. 프로그램 내의 한 모듈에서 다른 모듈로 어떤 사건의 발생을 통지하여, 보내는 쪽 클래스의 실행 궤적과 받는 쪽의 실행 궤적이 비동기적으로 동시 수행될 수 있도록 하자는 것이죠. 

NSNotification을 이용할 때 많은 분들이 [NSNotificationCenter defaultCenter]로 얻어낸 NSNotificationCenter 객체의 postNotification 메소드를 호출하는 것을 떠올리시는데, NSNotificationCenter의 클래스 레퍼런스에도 나와 있습니다만, NSNotificationCenter를 직접 이용하면 동기적인 처리밖에 안됩니다. 

A notification center delivers notifications to observers synchronously.

무슨 이야긴가 하면, postNotification을 호출하면 해당 notification에 observer로 등록한 객체의 callback 루틴이 수행 종료될 때 까지 그 자리에서 가만히 대기하고 있게 된다는 것입니다. 그러니, 비동기적으로 프로그래밍해서 어느 정도의 병행성(concurrency)을 얻고자 한다면 이렇게 프로그래밍을 하면 안되죠. 

따라서, NSNotificationQueue를 이용해야 합니다.

Notification을 날리는 쪽에서는 다음과 같이 하면 됩니다.

[[NSNotificationQueue defaultQueue] enqueueNotification:[NSNotification notificationWithName:@"dataLoad" object:nilpostingStyle:NSPostWhenIdle];


위의 코드는 NSNotificationQueue에 "dataLoad"라는 이름의 NSNotification 객체를 넣는 코드입니다. 이 객체는 언제 notification observer에게 전달될까요? manual에 따르면, 'run loop가 idle할 때'로 되어 있습니다. NSPostWhenIdle로 지정했으니까요. 

Notification을 받을 쪽 코드는 NSNotificationCenter를 사용할 때와 같습니다. 다음과 같이 하면, "dataLoad"라는 이름의 Notification이 떴을 때 _dataLoad 메소드가 callback으로 수행되도록 할 수 있습니다.

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_loadData) name:@"dataLoad" object:nil];


한가지 주의할 것은 Notification을 queue에 집어넣을 때 NSPostNow라고 하면 비동기적으로 notification이 날아가는 게 아니라 동기적으로 처리된다는 점이에요. reference에 실린 아래의 글을 인용하는 것으로, 이번 글은 마치도록 하죠.

A notification queued with NSPostNow is posted immediately after coalescing to the notification center. You queue a notification with NSPostNow (or post one with postNotification:) when you do not require asynchronous calling behavior. For many programming situations, synchronous behavior is not only allowable but desirable: You want the notification center to return after dispatching so you can be sure that observing objects have received and processed the notification. Of course, you should use enqueueNotification... with NSPostNow rather than use postNotification: when there are similar notifications in the queue that you want to remove through coalescing.


물론, 이런 질문도 있을 수 있겠어요. 그럼 그냥 NSPostNow를 써서 큐에 넣는 대신, NSNotificationCenter의 postNotification 메소드를 부르면 되지 않느냐? 그런데 그렇게 하면 유사 이벤트 병합기능을 써먹을 수 없게 되죠. NSNotificationQueue는 (위에도 적혀 있습니다만) 같은 이름의 이벤트가 큐에 들어오면 합쳐버리거든요.

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 좋은글 잘 읽었습니다.
    그런데, nsthread 에서는 NSPostNow 외에는 호출이 안되네요..
    혹시 가능한 방법을 찾을 수 있겠습니까?

    2011.02.21 18:27 신고 [ ADDR : EDIT/ DEL : REPLY ]

Languages/Objective-C2011.01.20 17:35
UIScrollView를 사용하여 Zooming, 페이지 단위 스크롤링을 고려하려면 주의해야 할 것이 있습니다. 그건 바로 "하나의 UIScrollView 하위 클래스에 Zooming 기능과 스크롤링 관련 기능을 동시에 우겨넣으면 안된다"는 것이죠.

같이 우겨넣어 프로그래밍을 할 수 있을지도 모르겠습니다만, 아마 "굉장히" 프로그래밍하기 어려울 겁니다.

그럼 확대/축소도 되고 좌우로 스크롤링도 되는 PDF 뷰어 같은걸 구현하려면 어떻게 해야 하나요?

가장 간단한 방법은 스크롤 뷰를 중첩(nesting)하는 겁니다. 안쪽 스크롤 뷰에는 확대/축소 기능을 넣고, 바깥쪽 스크롤 뷰에는 좌우 스크롤 기능을 구현하는 것이죠.

확대 축소 기능에 대해서는 지난번 글에 잠깐 다루었는데, 좌우 스크롤 기능과 함께 사용하려면 지난번 글처럼 기능을 무조건 단순화 하면 곤란합니다. 좌우 스크롤하려면 다음과 같은 요구사항을 만족해야 하기 때문이죠.

(1) 끊김없는 좌우 스크롤
(2) 다음번에 나타날 화면의 caching
(3) 페이지 단위 좌우 스크롤

(3)번은 만족시키기가 간단합니다. 스크롤 뷰의 pagingMode를 YES로 설정하면 되거든요. 그렇게 하면 화면 크기를 (엄밀하게 말하면 스크롤 뷰의 bounds 속성 값) 페이지 크기로 인식하고 스크롤을 합니다.

(1)을 할떄는 생각할 것이 좀 많습니다. '끊김 없는'이라는 것은 '좌우 스크롤을 할 때 멈칫 거리는 일이 없어야 함'을 의미하거든요. 모든 페이지를 하나의 스크롤 뷰의 서브 뷰로 몽땅 다 로딩해 버린다음에 스크롤 하면 별문제겠습니다만 그렇게 하는 건 오버헤드가 너무 큽니다. 그러니 보통은 좌우 스크롤을 구현할 때 이렇게 해야 하죠.

a. 스크롤 뷰의 contentSize는 스크롤 뷰 안에 '모든 페이지'를 우겨넣는다고 가정하고 잡습니다.

그래야 하는 이유는? 그래야 스크롤 뷰가 '어디서부터 어디까지 스크롤 해야 하는지' 제대로 인식할 수 있기 때문이죠.

b. 스크롤 뷰 안에 컨텐트를 (그러니까 서브 뷰들을) 추가할 때에는 딱 세 개 (혹은 다섯 개?) 만 합니다.

화면에 보이는 것은 그 중 가운데고, 왼쪽 오른쪽에 있는 것들은 각각 손가락을 오른쪽으로 긁었을 때, 왼쪽으로 긁었을 때 화면에 나타나야 하는 것들이죠.

c. 스크롤 뷰가 실제로 스크롤 되면, 위의 세 개 뷰 중에 왼쪽에 있는 것을 오른쪽으로 옮기고, 가운데 있는 것은 오른쪽, 맨 오른쪽에 있는 것은 폐기처분하거나 (release) 아니면 맨 왼쪽으로 돌려서 재활용합니다. (재활용 할 때에는, 재활용 한 뷰에 '그려졌던' 내용이 잠시라도 화면에 다시 표시되는 일이 없도록 주의해야 합니다.)

이렇게만 하면 서브뷰 딱 세 개만 가지고도 가로 방향 스크롤을 구현할 수 있습니다. (세로 방향도 마찬가지.) 굉장히 간단하게 들리는데, 무슨 문제가 있나요?

보통 '왼쪽으로 스크롤이 일어났다' 아니면 '오른쪽으로 스크롤이 일어났다'는 뭘 가지고 판단하나요? 스크롤 뷰의 delegate 함수 중에 scrollViewDidEndDecelerating 함수를 구현해서 할 수도 있겠지만, 테스트 해 본 바에 따르면 굉장히 페이지를 빠른 속도로 넘기는 경우에 문제가 발생할 가능성이 있습니다.

그래서 보통은 스크롤 뷰의 contentOffset.x 값이 오른쪽이나 왼쪽 페이지의 중간 지점을 넘어갔을 때 '스크롤이 일어났다'고 판단하게 되는데, 그러면 그 때 무슨 일을 해야 하느냐 하면 앞서 말한 대로 페이지들의 위치를 옮기고 재배치하는 등의 작업을 해 줘야 하거든요.

근데 재배치 될 뷰 안의 drawRect나 drawLayer 함수 안에서 뭔가 굉장히 많은 일을 하게 되면 바로 그 순간에 스크롤이 멈칫거리는 현상이 발생하게 됩니다. (Apple의 이미지 스크롤 관련 예제에서도 동일한 문제가 발생해요.) PDF 페이지를 렌더링 하는 것도 PDF 파일에 따라서는 막대한 오버헤드를 유발하기 때문에 마찬가지죠.

이 문제를 극복하기 위해서는 drawRect는 가급적 피하고 drawLayer를 쓰며, layer 클래스로는 CATiledLayer를 쓰는 것이 바람직합니다. CATiledLayer는 렌더링을 할 때 쓰레드를 만들어서 백그라운드에서 렌더링을 하기 때문에, drawLayer가 스크롤을 멈칫거리게 한다던가 하는 일이 적어요.

물론 화면에 뜨는 스피드는 좀 느려지긴 합니다. 이런 trade-off에 대해서는 개발자가 적절히 생각하고 선택을 해야겠죠. (저는 못해 드립니다. ㅎㅎ)
신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Objective-C2011.01.17 16:18
보통 Zoom 효과를 구현할 때 많이 사용하는 것이 UIScrollView입니다. 특히 PDF의 확대 효과에 관해서라면 Apple Developer Center에서 제공하는 ZoomingPDFViewer 예제를 많이들 참고하시죠.

이 예제에서는 세 개의 뷰를 사용해서 Zoom 효과를 구현합니다. 일단 백그라운드에 pdf를 먼저 그리구요. 그 앞에 zooming을 구현할 pdf view 두 개를 둡니다. 하나는 확대 이전 뷰, 하나는 확대 다음 뷰. (간단히 설명하자면 그렇습니다) zooming이 진행중인 동안에는 확대 이전 뷰를 사용해서 rendering하다가, 확대가 끝나고 나면 확대 다음 뷰를 확대된 크기만큼 새로 만들어서 (그러기 위해서는 얼마나 확대되었는지를 기억하고 있어야 합니다) 뷰가 보이는 계층 맨 위쪽에 붙여 버리는 것이죠. 

그런데 이 세 개의 뷰가 정말 필요한 걸까요? 이건 ZoomingPDFViewer 예제를 처음 봤을때 제 스스로 던진 질문이었습니다. 이 문제를 풀기 위해서, 과연 zooming이 진행 중일때 scroll view의 content view에 해당하는 뷰 (그러니까 확대 대상인 뷰)에 어떤 일이 벌어지는 지를 살펴봤습니다.

결론부터 이야기하자면, content view의 frame 속성(CGRect 값입니다)이 끊임없이 변합니다. 하지만 content view의 bounds 속성 값은 전혀 변하지 않습니다. 그러니, 다음과 같이 했다고 해 봅시다.

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView

{

    return pdfView;

}


위의 코드는 확대 대상 content view를 scroll view에게 반환하기 위해 구현하는, UIScrollViewDelegate 프로토콜 함수의 코드입니다. pdfView는 self.bounds를 사용해 자기 뷰 내부를 렌더링합니다. 그런데 frame 객체의 크기는 zooming을 할수록 자꾸 커진단 말입니다. 그래도 self.bounds에는 변함이 없으니, 뷰가 커지면 커질수록 뷰 안에 그려진 내용은 해상도가 떨어져 보이게 됩니다. 

그럼 대체 scroll view는 self.bounds는 가만 냅두면서 self.frame은 어떻게 계속 뻥튀기하는 걸까요?

답은 pdfView의 (그러니까 UIView를 상속한 뷰 클래스 객체라면 누구나 갖고 있는) transform 프라퍼티에 숨어 있습니다. 이 프라퍼티의 값이 CGAffineTransformIdentity가 아닐 경우, frame 프라퍼티의 값은 계산된 값입니다. 보통은 frame이나 bounds나 값이 동일하지만, transform 프라퍼티의 값이 CGAffineTransformIdentity가 아닌 경우에는 frame은 bounds 프라퍼티에 transform을 적용해서 구해진 CGRect 값이 되는 것이죠.

그래서 다음과 같은 코드 안에서 frame의 값과 bounds의 값을 찍어보면 그 값이 서로 다르게 나옵니다.

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale

{

// ...

NSLog(@"scale = %f, pdfScale = %f", scale, pdfScale);

NSLog(@"frame = %f, %f, %f, %f", view.frame.origin.x, view.frame.origin.y, view.frame.size.width, view.frame.size.height);

NSLog(@"bounds = %f, %f, %f, %f", view.bounds.origin.x, view.bounds.origin.y, view.bounds.size.width, view.bounds.size.height);

// ...

}


그러니까 결국 UIScrollView는 content view의 transform 프라퍼티의 값을 적절히 변경해서, content view의 확대/축소를 구현한다는 결론을 내릴 수 있는 것이죠. (거기다 contentOffset 프라퍼티까지 함께 수정/적용해서 말이죠.)

자. 그럼 여벌의 view 없이도 확대 동작을 구현하려면, ZoomingPDFViewer의 코드를 어떻게 개선해야 하는 걸까요? transform 프라퍼티를 적당히 매만지도록 코드를 변경하면 되겠군요. 지금부터는 여러분께서 ZoomingPDFViewer의 코드를 같이 보고 계신다고 가정하고 설명을 진행하겠습니다. 우선, backgroundImageView를 scroll view의 서브 뷰로 붙이는 부분을 아예 없애 버립니다. 필요 없으니까요.

그런 다음, oldPDFView를 참조하는 코드도 전부 없애 버립니다. 역시 필요 없습니다. (그러면 아마 scrollViewWillBeginZooming 안의 코드가 전부 날아가 버릴겁니다.) 

자. 그런 다음에는 pdfView만 필요하니까, PDFScrollView의 initWithFrame 메소드 안에서 pdfView를 생성하는 부분의 코드를 다음과 같이 고칩니다.

pdfView = [[TiledPDFView alloc] initWithFrame:pageRect andScale:pdfScale];

[pdfView setPage:page];

[self addSubview:pdfView];


이렇게 한 다음에, scrollViewDidEndZooming의 코드를 다음과 같이 고칩니다.

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView 

         withView:(UIView *)view atScale:(float)scale

{

// set the new scale factor for the TiledPDFView

pdfScale *=scale;

pdfView.bounds = CGRectMake(0,0,

                      view.frame.size.width, view.frame.size.height);

        // make the frame size to the bounds size

view.transform = CGAffineTransformIdentity

       

[pdfView setNeedsDisplay];

}


그런 다음, TiledPDFView의 initWithFrame의 코드를 다음과 같이 수정합니다.

tiledLayer.levelsOfDetail = 1;

tiledLayer.levelsOfDetailBias = 1;

tiledLayer.tileSize = CGSizeMake(512, 512);


detail 레벨을 1로 주어야 줌이 된 다음에 해상도가 상승되는 것이 보이게 되더군요. 너무 높으면 마치 줌이 되는 동안에서 해상도가 상승되다가, 마지막에 뷰가 쓸데없이 다시 그려지는 것 처럼 보입니다.

이렇게 한 다음에 실행시켜 보면, 이제 뷰 하나만 가지고도 PDF 문서의 zooming, 그러니까 확대/축소가 정상적으로 처리됩니다. scrollViewDidZooming 안에서 bounds의 값이 frame과 같도록, transform 프라퍼티의 값을 강제로 CGAffineTransformIdentity로 변경했기 때문입니다. 

자. 그런데 한가지 문제가 남았습니다. 뭔가요?

네. 아마 이 예제를 한번이라도 실행해 본 분이라면 아실텐데요. 이 문제는 이전 버전이나 현 버전이나 똑같습니다. 이 문제는 content view의 크기가 계속 변하기 때문에 발생하는 문제입니다. content view가 예전과 동일한 instance를 가리키고 있고 bounds의 값에도 변화가 없는 경우에는 scrollViewDidEndZooming 메소드의 atScale:(float)scale 인자값 'scale'의 값이 항상 'content view의 원래 크기 대비, 현재 크기의 비율'을 나타내게 되는데, content view instance가 확대할 때 마다 계속 달라지거나 (종전 예제의 경우) bounds의 값이 똑같이 유지되지 않는 경우(현 예제의 경우)에는 인자값 'scale'의 값이 '종전 크기 대비, 현재 크기의 비율'을 나타내게 된다는 것이죠. 

따라서 scroll view의 maximumZoomScale 프라퍼티나 minimumZoomScale 프라퍼티 값을 제한하는 것으로는 '어디까지 확대하고 어디까지 축소할 것인지'를 결정할 수 없게 됩니다. 

그러니 scrollViewWillBeginZooming과 scrollViewDidEndZooming 메소드 안에서 pdfScale 값을 가지고 minimumZoomScale 프라퍼티와 maximumZoomScale 프라퍼티를 가지고 꽁수를 좀 부려야 됩니다. 

이에 관해서는 각자 알아서 해보시는 것으로 하고, (연습문제라고 해도 좋겠습니다. ㅎㅎ) ZoomingPDFViewer 예제 분석과 개선에 관한 이번 글은 마치도록 하죠. 

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 띵호

    좋은 글 잘 읽었습니다.

    님이 남겨주신 문제가 딱 제가 가지고 있는 문제네요 ㅋㅋㅋㅋ
    근데 pdfScale를 가지고 계속 씨름 중인데. 혹시..아닌가요??ㅜㅜ

    2012.05.29 02:27 신고 [ ADDR : EDIT/ DEL : REPLY ]
    • 띵호

      와우 거의 한시간 동안 씨름해봤는데....
      문제를 해결했습니다.
      머가 실마리인지 몰랐는데 minimumzommScale 과 maximumZoomScale 를 가지고 노니 해결되네요 ㅋㅋ

      감사합니다 수고하세요~~

      2012.05.29 03:00 신고 [ ADDR : EDIT/ DEL ]

Languages/Objective-C2011.01.11 15:18
아이패드 보급이 늘어가면서 아이패드 위에 PDF 뷰어같은걸 구현하시는 분들도 많이 늘어가는 것 같군요. 그런 소프트웨어 구현할 때 가장 많이 사용하게 되는 것이 CGContextDrawPDFPage라는 함수죠. 이 함수는 현재의 드로잉 컨텍스트 위에 PDF 페이지를 뿌립니다.

이 함수의 한가지 문제는 호출할 때 마다 굉장히 많은 메모리를 계속 잡아먹는다는 점이죠. 그래서 PDF 파일을 열어놓은 상태로 모든 페이지를 전부 다 보게 되는 경우, 메모리 관련 경고가 뜨게 될 수 있습니다.

이 문제를 해결하는 한가지 방법은 메모리 점유율이 증가하거나 메모리 관련 경고가 뜨는 경우 (그러니까 다시 말해 메인 View Controller의 didReceiveMemoryWarning이 호출되는 경우) PDF 파일을 닫았다가 다시 여는 것이 되겠습니다. 

그런데 그런 코드를 작성하려면 PDF 파일에 대한 레퍼런스 (CGPDFDocumentRef)를 들고 있는 클래스를 어디 한 군데로 제한하는 것이 바람직하죠. 그럴 때에는 Singleton 패턴을 써야 한다는 것은 아마 많은 분들이 알고 계시겠습니다만...

그런데 그냥 CGPDFDocumentRef만 singleton 패턴으로 관리하면 땡인걸까요?

그렇지 않습니다. 한 쪽에서 이 패턴 객체를 통해 레퍼런스를 가져가서 열심히 뭔가 작업을 하고 있는데, 다른 한쪽에서 이 레퍼런스를 닫아버린다거나 하게 되면 프로그램이 당장 돌아가시게 되죠. 

그러니까 결국은 Singleton 패턴을 통해 CGPDFDocumentRef에 대한 접근 자체를 통제해야 합니다. 몇 가지 방법이 있겠습니다. 레퍼런스 카운터를 통해서 모두가 document ref를 전부 다 썼다는 것이 보장되기 전까지는 닫지 않는다거나 하는 것도 한가지 방법이죠.

저는 메모리를 얼마 이상 쓰지 않아야 한다는 요구조건이 있어서, 다음과 같은 싱글턴 클래스를 만들고 block을 통해서 해당 PDF 문서에 대한 작업을 구현하도록 하는 클래스를 만들어서 쓰고 있습니다. 소스코드를 보시면...

//

//  PDFSingleton.h

//  Muine

//

//  Created by bjlee on 11. 1. 11..

//  Copyright 2011 buggymind.com. All rights reserved.

//


#import <Foundation/Foundation.h>



@interface PDFSingleton : NSObject {


}


+ (NSUInteger) do:(NSUInteger(^)(CGPDFDocumentRef))blk;


@end



//

//  PDFSingleton.m

//  Muine

//

//  Created by bjlee on 11. 1. 11..

//  Copyright 2011 buggymind.com. All rights reserved.

//


#import "PDFSingleton.h"



@implementation PDFSingleton


static CGPDFDocumentRef _pdf = NULL;

static int requestCount = 0;


+ (NSUInteger) do:(NSUInteger(^)(CGPDFDocumentRef))blk {


@synchronized ( self ) {

if ( ++requestCount == 6 ) {

CGPDFDocumentRelease(_pdf);

_pdf = NULL;

requestCount = 1;

}

if ( _pdf == NULL ) {

CFURLRef pdfURL = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("xxx.pdf"), NULL, NULL);

_pdf = CGPDFDocumentCreateWithURL((CFURLRef)pdfURL);

CFRelease(pdfURL);

}


return blk(_pdf);

}

}



@end


PDF 문서 페이지가 여섯번 이상 요청되면 강제로 닫았다가 다시 열도록 했구요. 해당 pdf 문서에 대한 사용자 작업은 block으로 처리했습니다. 사용 예를 보시면...

// ctx의 선언은 이 위 어딘가에 있음 ㅎ

[PDFSingleton do:^(CGPDFDocumentRef pdf) {

CGPDFPageRef page = CGPDFDocumentGetPage(pdf, index);

// ...

CGContextDrawPDFPage(ctx, page);

return (NSUInteger)0;

}];


그런데 이 코드에는 문제가 있습니다. 뭘까요?

네. 어떤 한 쓰레드에서 해당 PDF 파일을 사용해 뭔가 심각하고 오래 걸리는 일을 하고 있으면, 다른쪽에서는 그 일이 끝날 때 까지 무작정 기다려야 합니다.

그러니 동시성을 활용해 캐싱이라던가 프리패칭(pre-fetching)같은 것을 하고 싶다면, 이렇게 하면 곤란하겠죠. 연습문제 삼아, 각자 이 문제를 고민해보시기 바랍니다. :-)
신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Objective-C2011.01.04 18:46
이건 비단 MPMoviePlayerController에 국한된 이야기는 아닐 수 있습니다. 다만 이 문제가 '비동기적'인 오류를 유발할 수 있으며, 디버깅이 굉장히 까다로운 문제를 야기할 수 있다는 점을 지적하고 싶습니다. 생각해보면 매우 단순한 문제인데, 깨닫기가 어렵죠. 거기다 iOS 3.2버전과 iOS 4.0 버전에서 동작 방식이 다릅니다. (특히 시뮬레이터에서는 더더욱요.)

간단히 문제를 살펴보죠. 제가 MoviePlayer라는 UIView 클래스의 하위 클래스를 만들었다고 치겠습니다. 이 클래스의 생성자 안에서 MPMoviePlayerController의 객체를 하나 만든 다음에, 이 객체를 사용해서 영화를 재생하기 전에 다음과 같은 짓을 했다고 해 보죠.

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(movieStopped) 
                                                              name:MPMoviePlayerPlaybackDidFinishNotification 
                                                             object:moviePlayer];

이렇게 하면, 영화 재생이 끝나면 self, 그러니까 MoviePlayer 객체의 movieStopped 메소드가 호출됩니다. Notification 메커니즘 덕분이죠. 여기서 moviePlayer는 MPMoviePlayerController 객체입니다.

그런데, iOS 3.2에서는 영화가 정상적으로 재생이 끝난 경우에만 notification이 발생하고, 재생 도중에 moviePlayer에 대해서 [moviePlayer stop]을 날린 경우에 대해서는 Notification이 발생하지 않았었어요.

하지만 iOS 4.0에서는 이야기가 좀 다릅니다. [moviePlayer stop]한 경우에도 Notification이 발생하거든요. (실제 장비에서는 모르겠지만 적어도 시뮬레이터에서는 발생합니다.)

그러니, [moviePlayer stop]을 코드에 넣으려고 하고 있다면, 다음과 같이 해 주어야 합니다. 아니면 movieStopped 메소드 안에 removeObserver를 호출하는 부분을 두거나요. 그래야 안전합니다.

[[NSNotificationCenter defaultCenter] removeObserver:self];

[moviePlayer stop];


왜 그럴까요? 만약에 [moviePlayer stop]을 날린 이후에 MoviePlayer 객체를 dealloc했다고 해 보죠. 그러면 아까 addObserver 할 때 인자로 넘겼던 self가 온데간데 없이 메모리에서 사라지게 되거든요. 그러니 removeObserver를 해주지 않으면 사라진 객체에게 NSNotification이 날아가게 되는 것이죠.

그러면 어떻게 되나요?

(잠시 침묵)

네. 사라진 객체가 메시지를 받기 때문에 프로그램이 쭈욱 뻗습니다. (죽는단 소리죠.)

생각해보면 간단하기 짝이 없는데, 몇 시간의 삽질 과정이 없으면 깨닫기 힘든 경우도 많습니다. 이 글이 여러분의 삽질을 조금이라도 줄여주었으면 좋겠군요. :-)


신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Objective-C2011.01.04 16:19
[이전 글에 이어서..]

그럼 새로운 UIView 애니메이션은 어떻게 써먹느냐. Ruby 같은 프로그래밍 언어를 보면 함수를 실행할 때 그 함수에 다른 함수 바디를 동적으로 만들어서 넘길 수도 있도록 허용하고 있는데요. Objective-C에서도 이제 그런 식의 프로그래밍이 가능합니다. 우선 developer.apple.com에서 퍼온 API 설명부터 한번 보죠.

animateWithDuration:animations:completion:

Animate changes to one or more views using the specified duration and completion handler.

+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion
Parameters
duration

The total duration of the animations, measured in seconds. If you specify a negative value or 0, the changes are made without animating them.

animations

A block object containing the changes to commit to the views. This is where you programmatically change any animatable properties of the views in your view hierarchy. This block takes no parameters and has no return value. This parameter must not be NULL.

completion

A block object to be executed when the animation sequence ends. This block has no return value and takes a single Boolean argument that indicates whether or not the animations actually finished before the completion handler was called. If the duration of the animation is 0, this block is performed at the beginning of the next run loop cycle. This parameter may be NULL.

Discussion

This method performs the specified animations immediately using the UIViewAnimationOptionCurveEaseInOut and UIViewAnimationOptionTransitionNone animation options.

For example, if you want to fade a view until it is totally transparent and then remove it from your view hierarchy, you could use code similar to the following:


이 함수는 static method이고, 첫 번째 인자로는 Animation 지속시간, 두 번째 인자로는 애니매이션이 수행되는 동안 행해질 작업에 대한 코드 블럭, 그리고 세 번째 인자로는 애니메이션이 끝나면 실행될 코드 블럭이 넘어갑니다.

대충 어떻게 써먹느냐 하면...

[UIView animatedWithDuration:0.2
            animations:^{view.alpha = 0.0;}
            completion:^(BOOL finished){ 
                               if ( finished)
                                  [view removeFromSuperView];
                             }];

위의 코드는 0.2 초 에 걸쳐, 어떤 뷰를 서서히 투명하게 만들고, 투명화 작업이 끝나면 그 뷰를 부모 뷰에서 떼 버리는 코드입니다. 블럭의 시작은 ^으로 표시하고, 인자의 목록은 () 안에, 블럭 코드 바디는 {} 안에 둔다는 것을 알 수 있습니다.

이렇게 코딩하면 앞서 봤던 [UIView beginAnimations], [UIView commitAnimations]를 통해 구현한 경우보다 코드 사이즈가 줄어듭니다. 애니메이션 종료시 호출될 메소드를 따로 구현해 둘 필요가 없거든요. 블럭 문법에 익숙한 분들에게는 코드도 훨씬 더 깔끔해 보이죠.



신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Objective-C2011.01.04 14:19

[UIView beginAnimation]

이거 써서 애니메이션 효과 넣으신 분들 많으시죠. iOS 4.0 부터는 블럭 기반 문법을 사용하는 API를 사용하도록 하라는 권고안이 날아오는 바람에 앞으로는 사용하기 좀 껄끄러워 질 것 같긴 합니다만, 그럼에도 여전히 많이 쓰이고 있습니다.
 
기본적으로 이 계열 API는 다음과 같이 사용하는데요.

gameCenter.frame = before;
[self.view addSubview:gameCenter];
[UIView beginAnimations:nil context:nil];
gameCenter.frame = after;
[UIView commitAnimations];


이렇게 하면 gameCenter의 프레임 위치가 움직이는 것이 애니메이션 형태로 표현되죠.

그런데 이렇게 프로그래밍 해 본 분들이면 대충 아시겠습니다만, commitAnimations 한 다음에 [gameCenter removeFromSuperview] 한다거나 [gameCenter release] 하면 애니메이션 효과가 제대로 먹질 않는 경우가 많아요. 애니메이션이 끝나기도 전에 뷰가 화면에서 사라지기 때문이죠.

이를 방지하기 위해서는 다음과 같이 해야 합니다.
(아마 대충 다 아시겠습니다만... ㅎ)

[UIView beginAnimations:nil context:nil];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(clearGameCenter)];
gameCenter.frame = after;
[UIView commitAnimations];
 
 
그러니까 애니메이션이 끝난 다음에 호출될 메소드와 그 메소드가 정의되어 있는 객체를 넘기는 것이죠. 후자의 작업은 setAnimationDelegate를 통해서 하고, 전자의 작업은 setAnimationDidStopSelector를 통해서 합니다.
 
그러니까 애니메이션이 끝난 다음에 gameCenter UIView를 화면에서 지워버리고 싶다면, clearGameCenter 메소드 안에서 다음과 같이 하면 됩니다.
 
- (void) clearGameCenter {
    [gameCenter removeFromSuperview];
    [gameCenter release];
    gameCenter = nil;
}
 
간단하죠. 이런 일들이 iOS 4.0 이상에서는 좀 더 간단해 질 터인데, 그에 관해서는 제가 XCode를 업그레이드 한 다음에 살펴보도록 하겠습니다. ㅎㅎ
 

[다음에 계속]


신고
Posted by 이병준

소중한 의견, 감사합니다. ^^