[iOS] CALayer 소개

2017. 5. 21. 03:01Mobile

iPhone 프로그래밍을 해보신 분이라면 UIView에 대해서 잘 아실것입니다. Button, TextArea, Slider, WebView 등 대부분이 UIView를 상속하여 구현되어 있습니다.

그러나 UIView가 내부적으로 어떻게 구성되어 있는지에 대해서는 잘 알려진 바가 없습니다. UIView는 내부적으로 CALayer를 통해 구현됩니다. CALayer를 통해 여러 시각적 효과를 쉽게 처리 할 수 있으므로 알아둔다면 많은 도움이 될 것입니다. 뿐만아니라 Core Animation과 어떻게 상호작용하는지 이해하는 것도 중요합니다. 이 주제에 대해서는 다음에 다룰 예정입니다.

여기에서는 CALayer를 사용하는 기본적인 방법을 배우게 될 것입니다. 단순한 테스트 앱을 만들어서 레이어를 하나 만들고 어떻게 동작하는지 확인해볼 예정입니다. 이를 통해 레이어란 무엇인지, 어떤 Property를 다룰 수 있는지, 이미지와 사용자 정의 개체를 내부에 어떻게 그리는지 알수 있습니다.

이 블로그 포스트는 iPhone 프로그래밍에 대해 기본적인 지식이 있다고 가정하고 작성되었습니다. 만약 iPhone 프로그래밍 입문자 이시라면 먼저 아이폰앱에 대한 기초 자료를 먼저 접하고 오시길 권장합니다. 또한 이 포스트는 2010년에 게시된 원문을 바탕으로 하며 Object-C코드를 기준으로 작성되었습니다. (그러나 Swift에도 적용이 가능한 내용입니다.)

CALayer란 무엇인가?

CALayer란 시각적 개체를 그려내는 사각 영역에 대한 Class입니다.

그러나 이러한 역할은 UIView의 역할이 아니였냐고 반문하실 수 있습니다. 그 말도 맞지만 좀더 정확히 표현하면 모든 UIView는 내부적으로 CALayer를 기본적으로 포함하고 있으며 이를 통해 화면에 그려지게 되는 것입니다. 모든 UIView는 기본적으로 CALayer를 하나 생성하며 다음과 같은 코드를 통해 접근하실 수 있습니다.

CALayer *myLayer = myView.layer;

CALayer를 직접 통제함으로써 얻게 되는 이점은 시각적으로 어떻게 출력될지에 대한 다양한 Property를 제공한다는 것입니다. 몇가지 예를 들면 아래와 같은 설정을 하실 수 있습니다.

  • 레이어의 위치와 크기
  • 레이어의 배경색
  • 레이어에 그려질 컨텐트 (이미지를 출력하거나 혹은 Core Graphic를 통해 그려진 그래픽 등)
  • 레이어의 모서리가 동글게 그려져야 하는지
  • 레이어에 그림자를 추가하기
  • 레이어에 외곽선을 그려주기

이러한 Property를 통해 다양한 효과를 손쉽게 만들어낼 수 있습니다. 예를들어 이미지를 불러와 회색 외곽선을 둘러주고, 그 주위에 그림자를 부여할 수 있습니다. 포토샵 작업을 하지도 않으면서, 혹은 Core Graphic 코딩을 하지 않으면서 몇 줄의 CALayer Property 설정으로 다양한 효과를 만들어 낼 수 있습니다.

CALayer Property가 제공하는 또다른 장점은 대부분의 Property들은 애니매이션 처리가 가능하다는 점입니다. 예를 들어 그림자가 부여된 이미지를 사용자가 터치하면 그림자를 잠시 감추어 버튼이 눌린 것처럼 나타낼 수 있습니다.

CALayer를 단독으로 사용할 수도 있지만, CALayer를 상속한 Class들을 사용할 수 도 있습니다. 예를들어 CAGradientLayer, CATextLayer, CAShapeLayer 등등 여러 Class들이 제공되고 있습니다. UIView가 사용하는 기본 레이어는 CALayer이며 이부분에 대해 좀더 상세하게 다루어 봅시다.

CALayer 사용해보기

CALayer가 무엇인지 가장 빠르게 이해하는 방법은 한번 사용해보는 것입니다. 따라서 XCode를 실행하신 후 새로운 프로젝트를 생성합니다. iOS/Application/View-based Application 프로젝트를 생성한 후 LayerFun 이라는 이름을 주었습니다.

View-based Application 템플릿은 하나의 View Controller를 생성합니다. 그리고 View Controller는 하나의 최상위 View를 가지고 있습니다. 위에서 배웠듯이 모든 View는 하나의 최상위 레이어를 가지고 있습니다.

이러한 레이어는 위에서 언급했듯이 layer Property를 통해 접근할 수 있습니다.

가장 먼저 CALayer와 Core Animation을 사용하기 위해서는 View-based 템플릿에 기본적으로 포함되지 않는 QuartzCore framework를 추가해야 합니다. 따라서 프로젝트의 Frameworks group를 클릭하여 Add/Existing Framework를 선택하신 후 QuartzCore framework를 추가해주세요.

QuartzCore framework를 추가하셨다면 아래와 같이 LayerFunViewController.m을 열고 다음과 같이 작성해보세요.

// Core Animation을 위해 QuartzCore.h 헤더를 추가합니다.
#import <QuartzCore/QuartzCore.h>
 
// viewDidLoad에 아래 라인을 추가해 보세요.
self.view.layer.backgroundColor = [UIColor orangeColor].CGColor;
self.view.layer.cornerRadius = 20.0;
self.view.layer.frame = CGRectInset(self.view.layer.frame, 20, 20);

위의 코드가 정확히 무엇인지 하나하나 뜯어봅시다.

  • 레이어에 접근하기 위해 self.view.layer를 사용하고 있습니다. 아시다시피 self.view를 통해 View Controller의 view에 접근하실 수 있습니다. 또한 view.layer를 사용하여 내부적으로 자동생성된 레이어에 접근하실 수 있습니다. 여기서 자동생성된 레이어는 CALayer입니다.
  • 레이어의 배경색을 오랜지 색으로 부여하고 있습니다 .backgroundColor는 배경색을 지정하는 Property이며 CGColor를 받습니다. CGColor는 UIColor의 CGColor Property를 통해 쉽게 생성하실 수 있습니다.
  • 다음으로 cornerRadius Property를 통해 모서리를 20 포인트 만큼 둥글게 처리하였습니다. 여기서 20이라는 값은 원의 반지름을 의미하며 해당 반지름 크기만큼 모서리가 둥글게 변화합니다.
  • 마지막으로 20포인트만큼 크기를 축소시켰습니다. CGRectInset 메서드를 통해서 손쉽게 크기를 설정하실 수 있습니다. CGRectInset는 frame과 축소될 크기 값(X와 Y)을 매개변수로 받으며 새로운 frame을 반환합니다.

위 코드를 실행시키면 아래와 같이 둥근 모서리를 가진 오랜지 사각형이 20포인트만큼 축소되어 출력됩니다.

A Simple CALayer

CALayer와 하위 레이어

UIView가 여러 하위 View를 가질 수 있었던 것처럼 CALayer도 여러 하위 레이어를 가질 수 있습니다. 새로운 CALayer는 다음과 같이 생성할 수 있습니다.

CALayer *sublayer = [CALayer layer];

CALayer를 생성하면 여러 Property를 설정하실 수 있습니다. 하지만 가장 먼저 다루게 될 Property는 크기와 위치에 관련된 Property일 것입니다. 이는 frame (혹은 boudns/position) Property를 통해 설정할 수 있습니다. 크기와 어디에 배치될지 결정했다면 이제 다음과 같은 코드로 다른 레이어의 하위 레이어로 추가하실 수 있습니다.

[myLayer addSublayer:sublayer];

하위레이어를 추가하는 방법을 알았으므로 위에서 viewDidLoad 메서드에 작성했던 코드 다음에 다음 코드를 추가해봅시다.

CALayer *sublayer = [CALayer layer];
sublayer.backgroundColor = [UIColor blueColor].CGColor;
sublayer.shadowOffset = CGSizeMake(0, 3);
sublayer.shadowRadius = 5.0;
sublayer.shadowColor = [UIColor blackColor].CGColor;
sublayer.shadowOpacity = 0.8;
sublayer.frame = CGRectMake(30, 30, 128, 192);
[self.view.layer addSublayer:sublayer];

위의 코드는 단순히 새로운 레이어를 만들고 배경색상과 그림자를 추가하고 있습니다. 마지막으로 레이어의 크기를 결정하고 다른 레이어의 하위레이어로 추가하였습니다. 눈여겨 보아야할 점은 이러한 작업들은 부모 레이어의 frame에 상대적이라는 것입니다. 상위 레이어가 20, 20의 좌표에서 시작하였고 하위 레이어는 30, 30의 위치에 배치하고 있으므로 결과는 50, 50 좌표에 레이어가 그려지게 됩니다.

코드를 실행해보면 아래와 같이 파란색 사각형에 그림자가 추가된 하위 레이어가 나타납니다.

A CALayer Sublayer with a Shadow

CALayer에 이미지 추가하기

CALayer의 멋진 특징 중 하나는 내부에 이미지를 포함할 수 있다는 것입니다. 여기서는 파란색 박스를 이미지로 대체해 보도록 합시다. 이 예제에서 사용한 이미지는 여기에서 다운로드하실 수 있습니다.

이미지를 프로젝트에 추가하신 후 위에서 작성하신viewDidLoad 코드의 마지막 라인인 [self.view.layer addSublayer:sublayer]; 바로전에 다음 코드를 추가해 봅시다.

sublayer.contents = (id) [UIImage imageNamed:@"BattleMapSplashScreen.jpg"].CGImage;
sublayer.borderColor = [UIColor blackColor].CGColor;
sublayer.borderWidth = 2.0;

위 코드는 이미지를 레이어의 컨텐트로 지정하고 있으며 추가적으로 검정색 외곽선을 추가하고 있습니다. 코드를 실행해보면 파란색 박스 대신에 이미지가 로드된 것을 보실 수 있습니다.

A CALayer with Image Content

Corner Radius와 Image Content에 대해 알아둬야할 점

출력되는 이미지의 모서리를 둥글게 만들고 싶다면 어떻게 해야할까요? 이미지가 추가된 레이어에 cornerRadius를 설정하면 이미지의 모서리가 둥근 모서리를 뚫고 나와있는 모습을 보실 수 있습니다.

따라서 이미지가 둥근 모서리를 뚫고 나오지 않게 하기 위해서는 하위 레이어의 masksToBounds Property를 YES로 설정해야 합니다. 그러나 masksToBounds를 YES로 설정하면 그림자가 나타나지 않게 됩니다. 왜냐하면 그림자도 마스크(가려지게)되기 때문입니다.

이를 해결하기 위해서는 2개의 레이어를 사용해야 합니다. 가장 뒷쪽의 레이어는 외곽선과 그림자를 출력하며 이미지를 출력하는 레이어에는 마스크를 적용하시면 됩니다. 2개의 레이어를 통해 이미지와 외곽선 그림자를 출력할 수 있게 됩니다. 레이어의 순서는 가장 마지막에 추가된 레이어가 상단에 배치되게 됩니다.

아래와 같이 코드를 수정해보도록 합시다.

CALayer *sublayer = [CALayer layer];
sublayer.backgroundColor = [UIColor blueColor].CGColor;
sublayer.shadowOffset = CGSizeMake(0, 3);
sublayer.shadowRadius = 5.0;
sublayer.shadowColor = [UIColor blackColor].CGColor;
sublayer.shadowOpacity = 0.8;
sublayer.frame = CGRectMake(30, 30, 128, 192);
sublayer.borderColor = [UIColor blackColor].CGColor;
sublayer.borderWidth = 2.0;
sublayer.cornerRadius = 10.0;
[self.view.layer addSublayer:sublayer];
 
CALayer *imageLayer = [CALayer layer];
imageLayer.frame = sublayer.bounds;
imageLayer.cornerRadius = 10.0;
imageLayer.contents = (id) [UIImage imageNamed:@"BattleMapSplashScreen.jpg"].CGImage;
imageLayer.masksToBounds = YES;
[sublayer addSublayer:imageLayer];

코드를 실행하보면 정상적으로 둥근모서리의 이미지와 그림자가 출력되는 것을 확인하실 수 있습니다.

A CALayer with Image Content and Corner Radius

CALayer에 사용자 정의 그래픽을 출력해보기

이미지를 추가하는 것 외에도 Core Graphic을 통해서 직접 그래픽을 그릴 수도 있습니다. 먼저 레이어의 그리기 작업을 대신하는 Class를 추가하고 drawLayer:inContext 메서드를 구현하면 됩니다. 이 메서드내에 Core Graphic를 통해 원하는 그래픽을 그려내는 코드를 추가하시면 됩니다.

새로운 CALayer를 생성하고 여기에 새로운 그래픽 패턴을 그려보도록 합시다. 그리기 작업을 진행할 Class는 View Controller를 사용하도록 합시다. 따라서 View Controller에 drawLayer:inContext 메서드를 추가하고 패턴을 그리는 코드를 추가할 것입니다. 그래픽을 그리는 코드는 Core Graphics 101:Patterns에서 사용한 예제 코드를 사용할 것입니다.

viewDidLoad 최하단에 다음 코드를 추가하도록 합시다.

CALayer *customDrawn = [CALayer layer];
customDrawn.delegate = self;
customDrawn.backgroundColor = [UIColor greenColor].CGColor;
customDrawn.frame = CGRectMake(30, 250, 128, 40);
customDrawn.shadowOffset = CGSizeMake(0, 3);
customDrawn.shadowRadius = 5.0;
customDrawn.shadowColor = [UIColor blackColor].CGColor;
customDrawn.shadowOpacity = 0.8;
customDrawn.cornerRadius = 10.0;
customDrawn.borderColor = [UIColor blackColor].CGColor;
customDrawn.borderWidth = 2.0;
customDrawn.masksToBounds = YES;
[self.view.layer addSublayer:customDrawn];
[customDrawn setNeedsDisplay];

위 코드에서 주목할 점은 delegate와 setNeedsDisplay 부분입니다.

  • 레이어의 delegate Property에는 self를 설정하였습니다. self는 View Controller를 의미합니다. 따라서 View Controller가 레이어의 그리기를 대신한다는 의미입니다. 반드시 View Controller에는 drawLayer:inContext 메서드를 구현해야 합니다.
  • 레이어를 추가한 후에는 setNeedsDisplay메서드를 호출하여 레이어가 새로 그려져야 함을 알려야 합니다. setNeedsDisplay 메서드를 호출하는 작업은 깜빡할 수 있는데 이 경우 drawLayer:inContext 메서드는 호출되지 않게 됩니다.

이제 View Controller에 drawLayer:inContext 메서드를 아래와 같이 추가하도록 합시다.

void MyDrawColoredPattern (void *info, CGContextRef context) {
 
    CGColorRef dotColor = [UIColor colorWithHue:0 saturation:0 brightness:0.07 alpha:1.0].CGColor;
    CGColorRef shadowColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.1].CGColor;
 
    CGContextSetFillColorWithColor(context, dotColor);
    CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 1, shadowColor);
 
    CGContextAddArc(context, 3, 3, 4, 0, radians(360), 0);
    CGContextFillPath(context);
 
    CGContextAddArc(context, 16, 16, 4, 0, radians(360), 0);
    CGContextFillPath(context);
 
}
 
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context {
 
    CGColorRef bgColor = [UIColor colorWithHue:0.6 saturation:1.0 brightness:1.0 alpha:1.0].CGColor;
    CGContextSetFillColorWithColor(context, bgColor);
    CGContextFillRect(context, layer.bounds);
 
    static const CGPatternCallbacks callbacks = { 0, &MyDrawColoredPattern, NULL };
 
    CGContextSaveGState(context);
    CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
    CGContextSetFillColorSpace(context, patternSpace);
    CGColorSpaceRelease(patternSpace);
 
    CGPatternRef pattern = CGPatternCreate(NULL,
                                           layer.bounds,
                                           CGAffineTransformIdentity,
                                           24,
                                           24,
                                           kCGPatternTilingConstantSpacing,
                                           true,
                                           &callbacks);
    CGFloat alpha = 1.0;
    CGContextSetFillPattern(context, pattern, &alpha);
    CGPatternRelease(pattern);
    CGContextFillRect(context, layer.bounds);
    CGContextRestoreGState(context);
}

위 코드의 내용은 Core Graphics 101: Patterns의 튜토리얼 코드를 복사한 뒤 약간 수정한 것입니다. (단지 색상과 context, layer bound 설정이 변경되었습니다.) 위의 코드에 대해 좀더 상세하게 다루고 싶은 분은 해당 링크를 통해 확인바랍니다.

코드를 실행해보면 아래와 같이 파랑 배경에 검은 패턴이 나타나는 것을 보실 수 있습니다.

원문링크: https://www.raywenderlich.com/2502/calayers-tutorial-for-ios-introduction-to-calayers-tutorial