[iOS_OSX] 웹 컨텐츠에 접근하기

URL Loading System

파운데이션 프레임워크 안에는 URL 기반으로 네트워크상에 위치한 리소스에 접근하는 도구들이 정의되어 있다. 네트워크를 통한 리소스 접근은 웹 브라우저를 생각해보면 되는데, 서버에 요청을 보내어 그 요청에 맞는 결과물을 전송받게 되는데, 이러한 일련의 과정을 제어하는 몇 가지 클래스를 묶어서 URL Loading System이라고 한다. 물론 파운데이션에는 이 URL Loading System 외에도 인증, 캐싱, 프로토콜을 제어할 수 있는 추가적인 클래스들이 정의되어 있어서 인터넷 혹은 네트워크와 관련된 기능들이 잘 추상화 되어 있다. 따라서 특별한 경우가 아니면 BSD소켓 프로그래밍까지는 잘 몰라도 네트워크 기능을 어느 정도 구현할 수 있다.

잠깐 코코아 터치의 네트워크 관련 프레임 워크들을 살펴보면, 가장 하층부에는 BSD Socket이 자리잡고 있다. 그 바로 위쪽에 CoreNetwork 프레임 워크가 있고, 또 CoreFoundation Networking 이 있다. 이러한 하부 구조 위의 최상단에는 Foundation 의 URL Loading System 이 있다.

어쨌거나 URL 로딩 시스템은 일반적인 http, https, ftp, file 등의 프로토콜에 필요한 기능을 거의 모두 구현해주고 있으므로 웹과 관련한 네트워킹은 비교적 손쉽게 처리할 수 있다.

NSURLRequest / NSURLResponse / NSURLConnection

URL 로딩 시스템을 이루는 세 줄기는 NSURLRequest, NSURLResponse, NSURLConnection 의 세 개의 클래스이다. 네트워크를 통해 요청을 보내고, 응답을 받고, 응답이 성공적인 경우 그에 따라 컨텐츠를 전송받게 된다. 이 과정의 각 단계에서 주요한 키 역할을 하는 것이 이 세 종류의 클래스이다.

NSURLRequest

서버에 보낼 요청을 표현하는 객체이다. 요청을 받을 곳의 URL을 담아두는데, POST 방식의 요청인 경우에는 bodyData에 필요한 인자값들을 담아둘 수 있다.

NSURLRequest *theRequest =
[NSURLRequest requestWithURL:[NSURL urlWithString:@"http://www.apple.com"]
                 cachePolicy:NSURLRequestUseProtocolCachePolicy
             timeoutInterval:60.0];

요청을 만들었으면 connection을 만든다. connection을 생성하면 자동으로1 연결을 시도한다. 서버로 연결되면 reponse를 받고 데이터를 내려받게 되는데, 이 때 각 상황에 맞는 동작을 처리하기 위해서 connection의 delegate가 필요하다. 델리게이트는 1) 응답을 받거나 2) 데이터를 다운로드하고 3) 다운로드가 완료되거나 4) 오류로 중지되었을 때의 상황을 처리하기 위해서 다음 4가지 메소드를 구현해야 한다.

NSURLConnection

연결 객체를 생성하고 실제로 연결하는 과정은 다음과 같다. recievedData는 연결을 통해 내려받는 데이터를 저장할 변수로 따로 선언해 두었다고 가정한다. 연결 객체가 성공적으로 만들어지면 데이터를 담을 변수의 객체를 만들고 이를 초기화한다.

 NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if(connection) {
    receivedData = [NSMutableData data]; [connection start];
}

Connection 델리게이트

서버에 요청을 보내면 서버는 실제 요청한 데이터를 내려보내기 전에 응답(NSURLResponse)을 먼저 보낸다. 응답에는 내려보내는 데이터의 MIME 타입과 같은 정보들이 담겨 있다. 응답을 보낸 직후 서버는 요청한 URL의 컨텐츠를 전송해준다. connection 객체가 응답을 받게 되면 델리게이트에게 -connection: didReceiveResponse: 메시지를 보내게 된다.

이후 데이터를 다운로드 받는 중간 중간에 계속해서 -connection: didReceiveData: 를 보내 전송 받은 데이터를 델리게이트에게 전달한다. 델리게이트는 미리 생성해놓은 NSMutableData 객체에 전달받는 데이터를 더해준다.

데이터 전송이 완료되면 connection 객체는 델리게이트에게 -connectionDidFinishLoading: 메시지를 보낸다. 만약 오류가 나서 전송이 실패하면 -connection: didFailWithError: 메시지를 보내는데, 여기서는 이미 받았던 데이터의 length를 0으로 만들어 중단된 데이터를 제거한다.

다음은 델리게이트 메소드의 예시이다.

-(void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response
{
    NSLog(@"Resoponse - MIME TYPE : %@", response.MIMEType);
}

-(void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
{
    [receivedData appendData:data]
}

-(void)connection:(NSURLConnection*)connection didFailWithError:(NSError *)error
{
    NSLog(@"Fail to loading - %@", error.localizedReason);
    [receivedData setLength:0];
}

데이터를 전송받은 다음에는 이 데이터를 사용하면 된다. 예를 들어 theRequest의 URL이 특정한 이미지 파일이었다면 다음과 같이 이미지를 스크롤뷰에 넣어 표시할 수 있다. self 가 뷰 컨트롤러라고 가정할 때의 코드이다.

-(void)connectionDidFinishLoading:(NSURLConnection*)connection
{
    UIImage *image = [UIImage imageWithData:receivedData];
    UIImageView *imaageView = [[UIImageView alloc] initWithImage:image];
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.view.bound];
    scrollView.contentSize = image.size;

    [self.view addSubView:scrollView];
}

지난 번에 스크롤 뷰와 관련한 글에서도 살펴보았듯이 만약 스크롤뷰에서 확대/축소를 지원하려면 최대/최소 scale 값을 지정해주고, 뷰 컨트롤러가 UIScrollView의 델리게이트가 되어 어떤 뷰를 확대 축소할 것인지 알려주는 메소드를 구현해주어야 한다.


  1. 실제로는 자동으로 접속을 시도하지 않는 것 같다. start 메시지를 보내서 수동으로 연결을 시도하도록 해야 한다.

    -(void)connection: didReceiveResponse:
    -(void)connection: didReceiveData:
    -(void)connection: didFailWithError:
    -(void)connectionDidFinishLoading: 

[Cocoa] NSFetchedResultsController

코어데이터와 UITableView와의 연결

지금까지 몇 개의 예제를 통해 코어데이터를 사용해서 일련의 데이터를 디스크에 읽고 쓰며, 이를 관리하는 방법에 대해 살펴보았는데, 이런 작업을 보다 쉽게 만들어주는 컨트롤러가 있었으니, 바로 NSFetchedResultsController이다.

이 클래스는 코어데이터의 컨텍스트를 기반으로 저장소로부터 조건에 맞는 객체를 읽어들여서, UITableView의 데이터소스 메소드에서 쉽게 사용할 수 있는 형태로 제공한다. 또한 읽어온 객체에 대해 추적 기능을 가지고 있어서 managed object에 어떤 변경이 발생할 때 이를 감지하여 적절하게 테이블 뷰에서의 변경을 만들어낼 수 있다. [Cocoa] NSFetchedResultsController 더보기

UIImage를 카메라롤에 저장하기

UIImage를 아이폰의 카메라롤에 저장하는 과정은 사실 간단하다. 카메라롤은 내부에 사진을 정리/저장할 수 있는 체계를 가지고 있는 시스템이고, 여기에 특정 이미지를 저장하겠다는 함수를 호출하는 것으로 해당 동작을 처리할 수 있다. UIImageWriteToSavedPhotosAlbum()이라는 이름을 잘못쓰기 쉽게끔 지어놓은 UIKit 자유 함수가 여기에 사용된다. 이 함수의 원형은 다음과 같다.

void UIImageWriteToSavedPhotosAlbum(
    UIImage*  image, 
    id        completeionTarget, 
    SEL       completionselector, 
    void*     contextInfo
);

이 함수는 넘겨 받은 이미지를 카메라롤에 저장하고, 저장 작업이 완료되면 지정된 타깃에 지정된 메시지를 보낸다. 각 파라미터들은 원형에서도 짐작할 수 있겠지만, 다음과 같다.

  • image : UIImage 객체
  • completionTarget : 저장이 완료된 후 콜백을 받을 객체.  콜백처리가 귀찮으면 그냥 nil을 넘긴다.
  • completionSelector: target이 받게될 완료 콜백 메소드
  • contextInfo : 특정한 컨텍스트 정보를 넘겨서 이 정보를 다시 콜백 메소드가 받아 처리한다. 보통의 경우에는 그냥 nil을 넘긴다.

따라서 이미지를 저장하고 완료 콜백을 호출받고 싶다면 콜백을 우선 작성해야 한다. 콜백 함수는 저장하라고 넘겨준 이미지와, 에러 여부를 판단할 NSError*, 그리고 저장 시에 넘겨준 context 정보를 받게 되므로 다음과 같은 식으로 정의할 수 있다.

-(void) image:(UIImage*) image
     didFinishedSavingWithError:(Error*)error
     contextInfo:(void*)contenxtInfo;

따라서 다음과 같이 사용하여 호출한다.

UIImageWriteToSavedPhotosAlbum(
    theImage, 
    self, 
    @selector(image:didFinishedSavingWithError:contextInfo:), 
    nil
);

Swift 가이드

Swift에서 위의 C함수는 다음의 형태로 브릿징된다.

func UIImageWriteToSavedPhotosAlbum(_ image: UIImage,
                                    _ compeletionTarget: Any?,
                                    _ completionHandler: Selector?,
                                    _ contextInfo: UnsafeMutableRawPointer?)

사용하는 방법 자체는 별반 다르지 않다.

// in view controller class...

func image(_ UIImage, didFinishSavingWith error: NSError?, contextInfo context: UnsafeMutableRawPointer?) { ... }

func saveImage() {
  if let image = self.image {
    UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWith:context:)))
  }
}

만약 UIImage를 별도의 파일로 저장하려고 한다면, 이미지가 특정한 파일로 표현된 표현형 데이터를 얻고, 이를 파일에 쓰는식으로 처리해야 한다.

[UIKit] UIView를 이미지로 캡쳐하기, UIImage를 파일로 만들기

UIView를 UIImage로 캡쳐하는 방법

NSView를 이미지로 캡쳐하는 방법에는 여러가지가 있는데, 캡쳐한 이미지를 파일로 저장하기 간편하게 쓰는 방법은 NSBitmapImageRep 클래스를 사용해서 현재 포커스된 뷰의 영역을 바로 캡쳐하는 것이다. (그 외에 PDF데이터를 바로 추출하거나, 뷰의 CALayer를 NSImage 상에서 렌더하는 방법등 여러 가지 방법이 있을 수 있다.)

코코아 터치에서는 NSBitmapImageRep 와 같은 클래스가 없다. 대신에 비트맵 이미지를 그래픽 컨텍스트에서 얻을 수 있다는 점을 이용하여 다음과 같은 방법을 쓸 수 있다.

-(UIImage*)captureView:(UIView *)theView
{
    UIGraphicsBeginImageContextWithOptions(theView.bounds.size,
            theView.isOpaque, 1.0);
    ;
    UIImage *resultImage =
        UIGraphicsGetImageFromCurrentImageContext();
    return UIImage;
}

그럼 UIKit에서 이미지를 파일로 저장하려면 어떻게 해야할까? 코코아에서는 NSBitmapImageRep이 그래픽 포맷에 대응하는 데이터를 제공했는데, UIKit에서는 이 클래스를 사용할 수 없다고 했다.

UIKit은 이를 위해 다른 함수를 제공하고 있다. UIImageJPEGRepresentation()UIImagePNGRepresentation()이 그것이다. 즉 모바일 기기에서 통용할 수 있는 그래픽 포맷은 사실상 PNG, JPEG 이므로 이 둘을 생성하는 함수를 아예 프레임워크가 지원하고 있다.

이들 함수는 각각 다음과 같이 선언되어 있으며, 이 함수를 통해 얻은 데이터를 디스크에 기록하면 바로 JPEG, PNG 파일을 얻을 수 있다. 참고로 compressionQuality는 0.0~1.0으로 1.0이 가장 좋은 화질을 의미한다.

NSData * UIImagePNGRepresentation ( UIImage *image);
NSData * UIImageJPEGRepresentation (
        UIImage *image,
        CGFloat compressionQuality );

물론 이미지를 바로 카메라롤에 저장하는 것과 이미지를 파일로 만드는 것은 좀 다른 이야기이긴하다.

참고 : NSView를 이미지로 만들기

[iOS] UIScrollView 사용법

업데이트

UIScrollView를 Swift에서 사용하는 방법에 대한 (적어도 이 글 보다는 나은) 새 버전을 참고하세요.

UIScrollView는 gesture recognizer를 내장하여 실제 뷰 영역보다도 큰 영역을 스크롤하여 내용을 볼 수 있도록 해주는 클래스이다. 사진 앨범 앱의 사진 보기 화면에서 이 스크롤뷰가 사용된다. (카메라롤의 사진 목록 역시 스크롤뷰로 구현되어 있다.)

스크롤뷰는 관성 이동은 물론 내부 컨텐츠를 확대/축소하는 방법을 아주 간단히 처리할 수 있어 주로 이미지와 관련된 화면에서 상당히 유용하게 활용할 수 있다.

스크롤뷰를 사용하는 방법은 UIViewController와 거의 유사하다. 인스턴스를 생성해서 하위뷰를 추가해주면 된다. 스크롤뷰의 뷰 크기는 실제 화면에 노출되는 영역의 크기이고, 실제 전체 컨텐츠의 영역을 ContentSize로 지정해주어야 스크롤이 제대로 동작한다.

또한 줌을 위해서는 최대스케일 값과 최소 스케일 값을 지정해야 하며 (이는 IB에서도 할 수 있다.) 실제 줌 동작에 반응하기 위해서는 델리게이트가 어떤 뷰가 줌을 받게 되는지를 지정해 주어야 한다. 아주 간단한 예제를 통해 알아보도록 한다.

스크롤뷰 스터디 샘플

설명의 편의를 위해 이번에는 IB를 전혀 사용하지 않고 코드로만 작업해 본다. 우선 프로젝트를 신규로 만든다. 이 때 템플릿은 Single View Application을 사용한다. 또한 스크롤 뷰를 위해 큼지막한 이미지를 하나 더 준비한다. 프로젝트가 생성되면 Xcode 창의 왼쪽 파일 네비게이션 영역으로 파일을 끌어다 놓으면 프로젝트에 이미지 파일을 추가할 수 있다.

ViewController.m

오늘은 이 파일에서 모든 것을 처리해보자. 앱이 실행되면 루트 뷰에 스크롤뷰를 하나 추가하는데, 이 스크롤뷰에는 UIImageView가 하나 추가된다. 이 이미지뷰 안에 방금 추가한 이미지를 넣어서 스크롤이 되도록 해 볼 것이다.

먼저 파일 이름을 따로 매크로로 만들어 두고, 바로 private interface를 정의하도록 한다.

#import "ViewController.h"

#define SCV_IMAGE_FILENAME @"이미지파일.JPG"

@interface ViewController() <UIScrollViewDelegate>
@property (strong, nonatomic) UIScrollView *scrollView;
@property (strong, nonatomic) UIImageView *imageView;
@property (strong, nonatomic) UIImage *anImage;
@end

인터페이스를 정의하면서 뷰컨트롤러가 UIScrollViewDelegate 프로토콜을 따르도록 했다. 이는 줌인/줌아웃을 어떤 뷰가 받을지를 스크롤뷰에게 알려주는 역할을 하기 위해서다. (물론 imageView가 하게 된다.)

다음은 synthesize 하면서 인스턴스 변수명을 함께 정해주자. (변수명 앞에 언더스코어를 붙이는 것은 혼동을 방지하기 위해서이다. 혹은 어떤 규칙이 있는지도 모르겠다.)
@synthesize scrollView = _scrollView, imageView = _imageView, anImage = _anImage;

이제 각 프로퍼티는 처음으로 호출될 때 초기화되도록 하면 된다. 이 때 스크롤뷰의 초기화 부분을 눈여겨 보라.

-(UIImage *)anImage
{
    if(!_anImage) _anImage = [UIImage imageWithName:SCV_IMAGE_FILENAME];
    return _anImage;
}

-(UIImageView *)imageView
{
    if(!_imageView) _imageView = [[UIImageView] alloc]initWithImage:self.anImage];
    return _imageView;
}

-(UIScrollView *)scrollView
{
    if(!_scrollView) {
        CGRect viewFrame = CGRectMake(0,0,320.0f,460.0f);
        _scrollView = [[UIScrollView alloc] initWithFrame:viewFrame];
        _scrollView.contentSize = self.imageView.frame.size;
        _scrollView.minimumZoomScale = 0.1f;
        _scrollView.maximumZoomScale = 3.0f;
        _scrollView.delegate = self;
        [_scrollView addSubview:self.imageView];
    }
    return _scrollView;
}

이제 앱이 실행되고 루트뷰가 로드될 때 스크롤뷰를 화면에 추가하도록 하자.

-(void)viewDidLoad
{
    [super viewDidLoad];
    [self.view addSubview:self.scrollView];
}

스크롤뷰에서 확대 축소하기

이제 앱을 빌드하고 실행하면 이미지가 화면에 표시되고, 드래그하여 스크롤이 되는 것을 확인할 수 있다. 하지만 아직 줌이 되지 않는다. 이는 위에서 이야기한 스크롤뷰에게 어떤 뷰가 줌이 되는지를 알려주지 않아서이다. UIScrollViewDelegate 프로토콜에 정의된 메소드 중 viewForZoomingInScrollView:를 추가해준다.

-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    return self.imageView;
}

이제 이미지를 두 손가락으로 확대/축소할 수 있게 됨을 볼 수 있다.