Tag Archives: iPhone

iOS Framework 이미지 리소스 사용하기

Cocoa Touch Framework가 자기 자신이 가진 이미지 리소스에 접근하기 위해서는 몇가지 추가적인 조치가 필요합니다. UIImage클래스가 가진 imageNamed 메소드를 사용하면 리소스의 이름만으로 이미지를 불러올 수 있지만 MainBundle 기준으로 동작하기 때문에 불러오지 못하는 경우가 발생합니다. 한번 확인을 해보도록 하겠습니다. 이 글은 [XCode 6 Framework 만들기]의 결과물을 활용합니다.

ios_framework_image_01

먼저 테스트로 사용할 이미지 파일을 추가합니다. 저는 골프GTI이미지를 사용해 보겠습니다. 그리고 Person 클래스에 간단하게 이미지를 반환하는 메소드를 추가해 보겠습니다.

#import <UIKit/UIKit.h>

@interface Person : NSObject

- (instancetype)initWithName:(NSString *)name age:(int)age;
- (void)info;
- (UIImage *)image;

@end
#import "Person.h"

@interface Person ()

@property(strong, nonatomic) NSString *name;
@property(nonatomic) int age;

@end

@implementation Person

- (instancetype)initWithName:(NSString *)name age:(int)age
{
    if(self = [super init]) {
        self.name = name;
        self.age = age;
    }
    return self;
}

- (void)info
{
    NSLog(@"The person's name is %@ and his age is %d", self.name, self.age);
}

- (UIImage *)image
{
    return [UIImage imageNamed:@"golf"];
}

@end

image라는 메소드를 하나 추가했습니다. 그 내용은 golf라는 이름의 이미지를 로드하여 반환다는것이 전부입니다. 이번에는 ViewController.m에 이미지를 받아 출력하는 코드를 추가합니다.

#import "ViewController.h"
#import <HelloFramework/HelloFramework.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    Person *person = [[Person alloc] initWithName:@"Hello" age:19];
    [person info];

    // 이미지를 화면에 출력합니다.
    UIImageView *imageView = [[UIImageView alloc] initWithImage:[person image]];
    [self.view addSubview:imageView];
    
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

이제 시뮬레이터 환경에서 한번 실행해 보도록 하겠습니다.

ios_framework_image_02

아무것도 화면에 출력되지 않는군요… 이미지를 정상적으로 로드하지 못하여 발생하는 문제입니다. MainBundle이 아닌 Framework의 Bundle에서 이미지를 읽어오도록 코드를 수정해 보도록 하겠습니다. Framework 프로젝트의 Person.m의 내용을 수정합니다. 수정후에는 Build를 수행하여 HelloApp 프로젝트에 적용하는것을 잊지 맙시다.

- (UIImage *)image
{
    NSBundle *frameworkBundle = [NSBundle bundleForClass:[self class]];
    return [UIImage imageNamed:@"golf" inBundle:frameworkBundle compatibleWithTraitCollection:nil];
}

위와 같은 방법으로 Framework 자기 자신의 Bundle에 속해있는 리소스에 접근할 수 있게 됩니다. 하지만 위의 API는 iOS 8부터 지원하는 API이므로 적절하지 않아 보입니다. 버전에 구애받지 않고 사용할 수 있는 방법은 다음과 같은 방법이 있습니다.

- (UIImage *)image
{
    NSBundle *frameworkBundle = [NSBundle bundleForClass:[self class]];
    NSString *imagePath = [frameworkBundle pathForResource:@"golf" ofType:@"jpg"];
    return [UIImage imageWithContentsOfFile:imagePath];
}

이제 확인을 해보겠습니다. 다시 한번 Framework를 빌드하고 시뮬레이터를 실행해 보겠습니다.

ios_framework_image_03

크기가 적절하게 이쁘게 나오지는 않았지만 정상적으로 이미지가 출력되는것을 확인할 수 있습니다. 하지만 레티나 이미지(@2x)를 구별하여 이미지를 로드하게 하려면 좀 더 추가적인 작업이 필요합니다.

#import <UIKit/UIKit.h>

@interface UIImage (Bundle)

+ (UIImage*)imageNamed:(NSString *)imageName bundle:(NSBundle*)bundle;

@end
#import "UIImage+Bundle.h"

@implementation UIImage (Bundle)

+ (UIImage*)imageNamed:(NSString*)imageName bundle:(NSBundle*)bundle
{
    // Extract base name and extension
    NSString* imageBaseName = [imageName stringByDeletingPathExtension];
    NSString* imageExt = [imageName pathExtension];
    if (!imageExt.length) imageExt = @"png";
    
    NSString* imagePath = nil;
    
    // Try retina version if available
    BOOL isRetina = ([[UIScreen mainScreen] scale] == 2.0);
    if (isRetina)
    {
        NSString* retinaImageBaseName = [NSString stringWithFormat:@"%@@2x", imageBaseName];
        imagePath = [bundle pathForResource:retinaImageBaseName ofType:imageExt];
    }
    
    // If not a Retina screen or retina image not found, try regular image
    if (!imagePath)
    {
        imagePath = [bundle pathForResource:imageBaseName ofType:imageExt];
        isRetina = NO;
    }
    
    // Build the image
    UIImage* image = [UIImage imageWithContentsOfFile:imagePath];
    // If retina version, set the scale appropriately
    if (isRetina)
    {
        image = [UIImage imageWithCGImage:image.CGImage scale:2.0 orientation:UIImageOrientationUp];
    }
    
    return image;
}

@end

이런식으로 카테고리를 만들어 사용하면 용이할 것 같습니다. [출처]

XCode 6 Framework 만들기

xcode6_framework_01

XCode 6버전에 들어서면서 손쉽게 만들기 어려웠던 Cocoa Touch Framework 프로젝트를 만들 수 있게 되었습니다. 이 프로젝트의 경우 Target이 Framework가 되며 실제로 빌드를 할 경우 XCode DerivedData 디렉토리안에 빌드된 Framework가 생성됩니다. 이렇게 빌드된 Framework를 당신이 원하는 프로젝트에 드래그&드롭을 하게 되면 간단하게 Framework가 추가되며 안에 있는 기능들을 호출할 수 있게 됩니다.

Framework 만들기

지금부터 간단한 Framework를 제작하고 예제앱에 추가하여 사용하는 방법을 구현해 보도록 하겠습니다. 지금부터 진행할 예제는 하나의 Workspace안에 두개의 프로젝트(Framework, App)로 구현할 것인데 Workspace 없이 별개의 단일 프로젝트를 각각 구현하셔도 괜찮습니다.

xcode6_framework_02

File > New > Workspace를 선택하여 개발을 할 워크스페이스를 하나 만들어줍니다. 저는 HelloFrameworkExample로 만들어보겠습니다.

xcode6_framework_03

그럼 완전 아무것도 없는 빈 IDE가 등장합니다. 다시 File > New > Project 메뉴를 선택하여 프로젝트를 추가해줍니다.

xcode6_framework_04 xcode6_framework_05

스크린샷과 같이 Cocoa Touch Framework를 선택하고 Next를 눌러 진행합니다. 다음은 프로젝트의 기본 설정을 하게 되는데 적당히 본인의 환경에 맞춰주시면 됩니다. 언어의 경우 Swift, Objective-C 둘중에 하나를 선택할 수 있는데 저는 일단 Objective-C로 진행을 하도록 하겠습니다.xcode6_framework_06

마지막에 소스가 저장될 디렉토리를 설정하는데 하단에 Add to에 현재 추가할 Workspace를 선택해주어야 작업할 워크스페이스에 추가됩니다. Git Source Control은 예제 프로젝트이므로 사용하지 않을 생각입니다.xcode6_framework_07

생성된 Cocoa Touch Framework의 디렉토리 구조도는 위와 같습니다. Framework Import시에 가장 기본으로 불려질 HelloFramework.h와 프로젝트 설정이 담겨있는 Info.plist와 테스트 코드들이 담겨있습니다.

소스파일이 아닌 프로젝트 자체를 선택했을때 오른쪽에 뜨는 프로젝트 설정에 주목할 부분은 Deployment Target 설정입니다. Framework가 갖는 특성이 다양한 환경에서 실행되어야 하는 라이브러리라고 볼 때 더 많은 환경에서 구동되도록 구현하는것은 당연해 보입니다. Deployment Target의 경우 실행 가능한 가장 낮은 버전을 설정하게 되는데요. 현재 시점에서 선택할 수 있는 합리적인 버전은 5.1.1이지 않나 생각합니다.

2015년 2월부터 Apple에서는 앱스토어에 등록하는 모든 앱이 64비트를 지원할것을 강제화 했습니다. Framework가 32비트와 64비트를 동시에 지원할 수 있는 가장낮은 Deployment Target은 5.1.1입니다. 실제로 더 낮은 버전을 설정하면 다음과 같은 경고 메시지를 볼 수 있습니다.

xcode6_framework_08

위의 Deployment Target 설정과 연계하여 Build Setting에 대해서도 확인해 봅시다.

xcode6_framework_09

여기서 주목해야 하는 설정은 Architectures와 Valid Architectures 설정입니다. 보면 32비트 아키텍쳐인 armv7/armv7s와 64비트 아키텍쳐인 arm64를 모두 지원하는것을 알 수 있습니다. 그 다음으로 확인해야 하는 부분은 Build Active Architecture Only 설정입니다. 기본 설정은 Debug에 한해 Yes로 되어있습니다만 이렇게 되어있을 경우 Simulator 환경에서는 i386으로만 빌드가 됩니다. 하지만 당신의 환경이 x86_64 아키텍쳐 환경을 필요로 할 수 있으므로 No로 바꾸어 가능한 모든 환경에서 동작하도록 빌드를 합시다.

xcode6_framework_10

이제 간단한 예제 API를 만들어봅시다. File > New > File 메뉴를 선택하거나 마우스 우클릭 후 Add File을 선택하여 Cocoa Touch Class를 추가합니다.

xcode6_framework_11

Person이라는 클래스를 추가해보겠습니다. IDE에 새로운 두개의 파일(.h/.m)이 생성된것을 확인할 수 있습니다. 다음과 같이 간단한 데모 코딩을 해보겠습니다.

#import <Foundation/Foundation.h>

@interface Person : NSObject

- (instancetype)initWithName:(NSString *)name age:(int)age;
- (void)info;

@end
#import "Person.h"

@interface Person ()

@property(strong, nonatomic) NSString *name;
@property(nonatomic) int age;

@end

@implementation Person

- (instancetype)initWithName:(NSString *)name age:(int)age
{
    if(self = [super init]) {
        self.name = name;
        self.age = age;
    }
    return self;
}

- (void)info
{
    NSLog(@"The person's name is %@ and his age is %d", self.name, self.age);
}

@end

위의 코드에 대한 내용은 굳이 설명하지 않아도 괜찮을 것이라 생각하고 패스하겠습니다^^ 하지만 초기화후에 info를 호출하여 NSLog로 값들을 출력한다는것만 기억해두도록 합시다.

xcode6_framework_12이렇게 추가된 클래스가 Framework 외부의 프로젝트에서 접근가능해야 할 경우 위와 같은 설정이 필요합니다. 클래스의 헤더파일을 선택하면 (위의 예시는 Person.h) 오른편에 Target Membership란이 보이는데 Framework와 Test가 보입니다. Framework 오른편에 Visibility 설정을 할 수 있습니다. Public으로 바꿔주어야 이 Framework를 사용하는 다른 프로젝트들이 자유롭게 이 클래스를 사용 할 수 있습니다. 기본적으로 Project로 되어있는데 Project의 경우 해당 프로젝트 이내(여기서는 Framework 자기 자신)에서만 접근이 가능하며 Private는 프로덕트에는 포함이 되지만 사용되기 원하지 않는 개발단계의 클래스들을 지정할 때 사용합니다.

마지막으로 HelloFramework.h 헤더파일에 추가한 클래스를 Import 선언해 줍니다. 여기에 선언하는 모든 클래스는 이 Framework를 Import하였다면 자유롭게 사용할 수 있게 됩니다. Framework 내부에서만 사용될 클래스는 여기에 선언하지 않도록 합니다.

#import <UIKit/UIKit.h>

//! Project version number for HelloFramework.
FOUNDATION_EXPORT double HelloFrameworkVersionNumber;

//! Project version string for HelloFramework.
FOUNDATION_EXPORT const unsigned char HelloFrameworkVersionString[];

// In this header, you should import all the public headers of your framework using statements like #import <HelloFramework/PublicHeader.h>

// 외부에서 자유롭게 접근할 클래스의 헤더들을 다음처럼 선언해 줍니다.
#import <HelloFramework/Person.h>

이것으로 예제 Framework를 만들어 보았습니다. 이제 한번 빌드를 해보도록 하겠습니다. Product > Build 메뉴를 선택합니다. 특별히 잘못한 것이 없다면 오류 없이 빌드가 성공할 것입니다.

xcode6_framework_13

처음에 보면 Products 이하의 파일들은 빨간색으로 표시되고 있습니다. 파일이 없는(Missing) 상태임을 뜻합니다. 빌드를 하면 생길것이라 생각되어 빌드를 해도 변화가 없는것을 확인할 수 있습니다. Products 이하의 파일들은 실제 환경인 Device 환경에서 빌드된 파일들을 표시합니다. 처음에는 기본적으로 Simulator가 선택되어있어 Simulator 빌드가 수행되므로 여기에 표기가 되지 않습니다.

상단의 Build Scheme 설정 옆에 iOS Device / Simulator를 선택할 수 있는 부분이 있습니다. 여기서 iOS Device를 선택합니다. 이후에 다시 Build를 수행하면 Products 이하의 바이너리들이 검정색으로 바뀝니다. 빌드가 성공했군요.xcode6_framework_14

빌드된 .framework파일을 선택하고 오른쪽의 Inspector를 보면 Full Path가 표시됩니다. 마지막에 화살표 아이콘이 있는데 저 아이콘을 누르면 해당 디렉토리로 바로 이동하여 Framework 파일의 위치를 보여줍니다. 이창을 띄워둔 상태로 예제앱을 만들어보도록 하겠습니다.

예제앱 만들기

이번에는 만들어둔 Framework를 활용한 간단한 예제앱을 만들어보도록 하겠습니다. File > New > Project를 선택하여 새로운 프로젝트를 생성합니다. 프로젝트 이름은 HelloApp으로 하겠습니다.

xcode6_framework_15 xcode6_framework_16

간단히 Framework가 동작하는것을 확인하기만 할것이기에 무엇을 선택해도 상관없지만 Single View Application으로 만들어보겠습니다.

xcode6_framework_17

마찬가지로 Add to 설정에서 기존에 만들어둔 Workspace를 재활용 할 수 있도록 설정해 줍니다. 이제 HelloApp도 XCode에 추가되어 작업할 수 있게 되었습니다.

xcode6_framework_18

아까 열어두었던 창의 HelloFramework.framework 파일을 드래그하여 추가해줍니다. 여기서 복사를 하여도 되고 Framework가 계속 변화가 이루어지는 상황이라면 Reference만 추가해놓고 Framework의 업데이트가 바로 적용되도록 할 수도 있습니다. 이제 Framework를 Import하여 사용해 보도록 하겠습니다. 첫번째 뷰인 ViewController.m에 추가해보겠습니다.

#import "ViewController.h"
#import <HelloFramework/HelloFramework.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    // Framework의 Person 클래스를 사용
    Person *person = [[Person alloc] initWithName:@"Hello" age:19];
    [person info];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

Person클래스의 info메소드를 호출하게 되면 NSLog를 통해 이름과 나이를 출력하게 만들어져있는것을 이전에 확인하였습니다. 정말로 제대로 호출이 되는지 실행을 해보겠습니다.

xcode6_framework_19

실행전에 Build Scheme과 적당한 실행 디바이스가 선택되어있는지 확인하고 빌드하시기 바랍니다. 저는 일단 심뮬레이터에서 구동해보겠습니다.

xcode6_framework_20

원하는 값이 정상적으로 출력이 되었군요^^