개인작업을 하다보니 여러 앱에 빠지지않은 페이징기능을 구현해야할 경우가 생겨, 여러 방법을 찾아봤는데 어떻게된것이 페이지 컨트롤러를 분명 XCode에서 지원하는데도, 직접 구현하는 방법을 먼저 찾게되어버렸다. 

그런고로 이번 게시물에서 다룰것은 그 페이징기능을 ScrollView와 PageControl을 이용해 구현하는 방법이다. 사용해보지는 않았지만, 페이지 컨트롤러로 페이징을 구현하는 것보다 귀찮은 짓일것이라고 생각된다.



샘플소스는 아래의 링크로 다운 !

PageWithScrollView.zip



 

           



 스크롤뷰를 이용해 페이징을 구현하기에 앞서 스크롤뷰의 속성에 대해서 하나 알아둘 것이있다. 바로 ContentSize라는 개념인데, 이는 스크롤뷰가 보여줄 수 있는 내용, 즉 컨텐츠의 크기를 말하는 것으로 SIze와는 완전히 다른개념이다. 예컨데, 4inch 아이폰의 화면(320 x 568)만한 스크롤 뷰가 있을때 페이징 기능을 이용해 가로로 아이폰 화면만한 5개의 뷰를 보여주려고한다면 , 스크롤 뷰 Frame의 Size는 320x568이되겠지만 ContentSize는 스크롤 뷰의 가로길이 * 페이지 수인 1600x568이 되는것이다. 

 다시 요약하자면, 이 ContentSize는 스크롤뷰가 보여줄 수 있는 실제 내용의 크기로 이 사이즈가 설정되어야 스크롤뷰가 제 역할을 할 수 있는 것이다.


 이제 본격적으로 예제를 작성하기 위해 간단한 프로젝트를 하나 만들었다. Empty Project로 프로젝트를 생성하고 뷰컨트롤러와 스토리보드를 만들어준다. 스크린샷에는 나와있지 않지만 예제용 이미지도 프로젝트에 포함해두었다. 샘플을 확인!



# 클래스 / 파일명 일람 

  - MainStoryboard : 현재 예제를 구성할 뷰를 그려놓을 스토리보드

  - PageScrollViewController : 페이징을 구현할 ScrollView를 가지는 뷰 컨트롤러

  - PageViewController : ScrollView에서 보여줄 각 페이지의 내용 뷰 컨트롤러


본격적인 코딩에 들어가기 앞서, 스토리보드에서 간단한 구성요소들을 만들어주자 스토리보드에서 각 뷰가 가지는 요소들은, 스크린샷에서 확인하면된다. 구성요소들을 모두 위치시키고나면 PageScrollViewController의 Storyboard Identifier에는 PageScrollView , PageViewController에는 SinglePageView라는 값을 주자. 나중에 뷰를 불러올때 사용할 수 있다.

 스토리보드에서 구성요소들을 위치시킬때 주의해야할 점은 PageControl이 ScrollView의 하위에 속하면 안된다는 것이다. 그럴 경우 예제의 방법으로 구현했을때 정상적인 스크롤 이벤트가 발생하지 않으니 유의할 것! 




 가장 먼저 , 현재 프로젝트는 빈 프로젝트였으므로 AppDelegate의 루트뷰를 설정해주어야한다. 


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // 현재 스토리보드를 불러온다.
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
    // RootViewController로 설정할 PageScrollView를 불러온다.
    UIViewController *rootViewController = [storyboard instantiateViewControllerWithIdentifier:@"PageScrollView"];
    
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    self.window.rootViewController = rootViewController;
    
    [self.window makeKeyAndVisible];
    return YES;
}

#PageScrollViewController.h

 RootViewController의 설정이 끝나면 이제 본격적으로 코딩에 들어가보자 ! 먼저 PageScrollViewController부터 작업해준다. PageScrollViewController클래스의 헤더는 아래와 같이 작성한다. 헤더의 각 프로퍼티들과 요소들을 선언한후에는 스토리보드의 요소들과 매칭시켜주어야한다.

#import 

@interface PageScrollViewController : UIViewController<UIScrollViewDelegate>

// IBOUTLET
@property (weak, nonatomic)IBOutlet UIScrollView *scrollView;
@property (weak, nonatomic)IBOutlet UIPageControl *pageControl;
@property (weak, nonatomic)IBOutlet UIButton *btn_Prev;
@property (weak, nonatomic)IBOutlet UIButton *btn_Next;

// 컨트롤러들이 저장될 배열
@property (strong, nonatomic)NSMutableArray *controllers;

// 페이지 수동 전환용 IBACTION
-(IBAction)btn_ScrollMove:(id)sender;

@end

 PageScrollViewController가 가지는 변수는 위의 소스와 같은데 (화면 구성을 위한 IBOUTLET들의 설명은 생략) 이 중 주목해야될 객체는 NSMutableArray형인 controllers다. 이 배열은 페이징을 구성하는 뷰 컨트롤러들을 가지고있는 배열로, 뷰 컨트롤러들을 로드해서 이 배열에 저장해두었다가 필요할때 꺼내서 쓰는 방식으로 사용한다. 페이징을 구성하는 실질적인 DataSource라고 할 수 있다.


#PageScrollViewController.m

 이 파일에서는 PageViewController를 미리 import시켜두어야한다. 페이징 뷰를 구성할 뷰 컨트롤러들을 저장해두기 위함이다. 각 프로퍼티들을 Synthesize 해준 뒤 본격적인 코딩에 들어간다.

 먼저 스크롤 뷰 와 페이지 컨트롤, 그리고 데이터소스가될 controllers등을 초기화해줄 함수를 구성해준다. 소스는 아래와 같다. 


-(void)initScrollViewAndPageControl{
    // 스크롤뷰와 페이지 컨트롤에 관련된 변수들을 초기화합니다.
    
    // 임의로 표시할 페이지 수를 설정합니다.
    int numbersOfPage = 4;
    
    // 컨트롤러들을 저장할 배열을 초기화합니다.
    _controllers = [[NSMutableArray alloc]init];
    
    // 최초에 값을 넣어줄때는 배열에 Null을 넣어줍니다.
    for(int idx = 0 ; idx < numbersOfPage ; idx++){
        [_controllers addObject:[NSNull null]];
    }
    
    // 스크롤 뷰의 컨텐츠 사이즈를 미리 만들어둡니다.
    CGSize contentSize = _scrollView.frame.size;
    contentSize.width = _scrollView.frame.size.width * numbersOfPage;
    
    // 스크롤 뷰의 컨텐츠 사이즈를 설정합니다.
    [_scrollView setContentSize:contentSize];
    // 스크롤 뷰의 Delegate를 설정합니다. ScrollView Delegate 함수를 사용하기 위함입니다.
    [_scrollView setDelegate:self];
    // 스크롤 뷰의 페이징 기능을 ON합니다.
    [_scrollView setPagingEnabled:YES];
    // 스크롤 뷰의 Bounce를 Disabled합니다 첫 페이지와 마지막 페이지에서 애니메이션이 발생하지않습니다.
    [_scrollView setBounces:NO];
    [_scrollView setScrollsToTop:NO];
    [_scrollView setScrollEnabled:YES];
    
    // 스크롤 바들을 보이지 않습니다.
    _scrollView.showsHorizontalScrollIndicator = NO;
    _scrollView.showsVerticalScrollIndicator = NO;
    
    // 현재 페이지 컨트롤의 페이지 숫자와 현재 페이지를 설정합니다.
    [_pageControl setNumberOfPages:numbersOfPage];
    [_pageControl setCurrentPage:0];
    
    // 최초에 보여줄 페이지를 미리 2개만 로드합니다.
    [self loadScrollViewDataSourceWithPage:0];
    [self loadScrollViewDataSourceWithPage:1];
}

 각 줄의 설명은 소스의 주석으로 대체하기로하고, 위의 함수를 보면 loadScrollViewDataSourceWithPage:라는 함수가 있는데, 이 함수는 스크롤뷰를 구성할 각 페이지를 로드하는 함수로 지정한 페이지에 해당하는 뷰 컨트롤러를 미리 불러와 스크롤뷰의 SubView로 추가해주는 핵심적인 함수라고 할 수 있다. 

 위의 함수에서는 0페이지와 1페이지만 불러왔는데 이는 최초 페이지가 0일 경우 이전페이지는 존재하지않고 당장에 스크롤로 보여주여야할 뷰는 현재 페이지와 다음페이지 즉, 0과 1페이지 뿐이므로 0페이지와 1페이지에 해당하는 뷰컨트롤러만 미리 로드해두도록한 것이다. 이 loadScrollViewDataSourceWithPage:의 내용은 아래와 같다. 


- (void)loadScrollViewDataSourceWithPage:(NSInteger)page{
    // 스크롤뷰에서 표시할 뷰를 미리 로드합니다.
    
    // 페이지가 범위를 벗어나면 로드하지않습니다.
    if(page >= _controllers.count)
        return;

    // 페이지의 뷰컨트롤러를 배열에서 읽어옵니다.
    PageViewController *controller = [_controllers objectAtIndex:page];
    
    // 현재 컨트롤러가 비어있다면, 컨트롤러를 초기화해줍니다.
    // (initScrollViewAndPageControl 참조)
    if((NSNull *)controller == [NSNull null]){
        NSLog(@"Page %d Controller Init..",page);
        
        // 현재 스토리보드에서 SinglePageView라는 StoryboardIdentifier를 가진 뷰를 읽어옵니다.
        controller = [[self storyboard] instantiateViewControllerWithIdentifier:@"SinglePageView"];
        // 현재 컨트롤러의 뷰에 Frame을 초기화해줍니다.
        [controller.view setFrame:_scrollView.frame];
        // 컨트롤러에 이미지와 텍스트들을 설정합니다.
        [controller initPageViewInfo:page];
        // 현재 컨트롤러와 배열에 들어있는 객체를 교체합니다.
        [_controllers replaceObjectAtIndex:page withObject:controller];
    }
    
    // 현재 컨트롤러의 뷰가 superview를 가지지 못했을 경우(현재 스크롤뷰의 서브뷰가 아닌 경우)
    // 스크롤 뷰의 서브뷰로 추가해줍니다.
    if(controller.view.superview == nil){
        NSLog(@"Page %d Controller Add On ScrollView..",page);
        
        // 현재 컨트롤러의 뷰가 위치할 frame을 잡아줍니다.
        // Page에 따라 Origin의 x값이 달라집니다.
        CGRect curFrame = _scrollView.frame;
        curFrame.origin.x = CGRectGetWidth(curFrame) * page;
        curFrame.origin.y = 0;
        controller.view.frame = curFrame;
        
        // 컨트롤러를 현재 컨트롤러의 ChildViewController로 등록하고 컨트롤러의 뷰를 스크롤뷰에 Subview로 추가해줍니다.
        [self addChildViewController:controller];
        [_scrollView addSubview:controller.view];
        [controller didMoveToParentViewController:self];
    }
}

 이렇게 페이징을 구성할 데이터 소스들을 다루는 함수의 구현을 끝마치고나면, UIScrollViewDelegate의 Delegate함수인 scrollViewDidEndDecelerating:를 구현해주어야한다. 이 Delegate 함수는 스크롤 뷰의 스크롤링이 완전히 끝마쳤을 경우 실행되는 함수로 이 함수에서 현재 페이지를 계산해 PageControl에 넘겨준후 현재 페이지와 이전페이지, 다음 페이지에 해당하는 뷰 컨트롤러들을 미리 로드해두게된다. 

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    
    // ScrollView의 드래그가 멈춘경우
    CGFloat pageWidth = CGRectGetWidth(_scrollView.frame);
    // 현재 페이지를 구합니다. floor는 소수점 자리를 버리는 함수입니다
    NSUInteger page = floor((_scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
    // 현재 페이지를 계산된 페이지로 설정해줍니다.
    _pageControl.currentPage = page;

    // 보여줄 페이지들을 미리 로드합니다.
    [self loadScrollViewDataSourceWithPage:page - 1];
    [self loadScrollViewDataSourceWithPage:page];
    [self loadScrollViewDataSourceWithPage:page + 1];
}

 

 Delegate 함수의 구현이 끝나면 스크롤 뷰의 스크롤이 아닌 방법으로(버튼 동작,페이지 컨트롤 조작)현재 페이지를 변경하는 함수도 구현해주도록하자 gotoPage:AtPage:함수는 함수에서 전달받은 페이지로 페이지를 이동시켜주는 함수로, 애니메이션의 여부를 결정하는 bool값을 별도로 입력받게된다. 이 함수와 이것을 사용하는 IBAction함수는 아래와 같다. 

- (void)gotoPage:(BOOL)animated AtPage:(NSInteger)page{
    // 스크롤을 임의로 원하는 페이지로 이동시킵니다.
    
    // 페이지 컨트롤의 현재 페이지를 넘겨받은 페이지로 설정합니다.
    [_pageControl setCurrentPage:page];
    
    // 미리 뷰를 로드합니다.
    [self loadScrollViewDataSourceWithPage:page - 1];
    [self loadScrollViewDataSourceWithPage:page];
    [self loadScrollViewDataSourceWithPage:page + 1];
    
    // 보여줄 스크롤뷰의 ContentOffset을 설정합니다.
    // 스크롤 뷰의 Width * Page입니다.
    CGRect bounds = _scrollView.bounds;
    bounds.origin.x = CGRectGetWidth(bounds) * page;
    bounds.origin.y = 0;
    
    // 정해진 부분으로 스크롤뷰를 스크롤합니다.
    [_scrollView scrollRectToVisible:bounds animated:animated];
    
}

#pragma mark - IBACTION

-(IBAction)btn_ScrollMove:(id)sender{
    // 버튼을 누르거나, 페이지컨트롤의 값이 변경될 경우 스크롤을 이동시킵니다.
    
    // 버튼은 Tag로 구분하며, pageControl은 태그를 설정해두지않았습니다.
    if([sender tag] == 0){
        if(_pageControl.currentPage < 3){
            [self gotoPage:YES AtPage:_pageControl.currentPage + 1];
        }
        else{
            [self alertViewShowsWithMessage:@"마지막 페이지입니다"];
        }
    }
    else if([sender tag] == 1){
        if(_pageControl.currentPage > 0){
            [self gotoPage:YES AtPage:_pageControl.currentPage - 1];
        }
        else{
            [self alertViewShowsWithMessage:@"첫 페이지입니다"];
        }
    }
    else{
        [self gotoPage:YES AtPage:_pageControl.currentPage];
    }
}

 위의 함수들을 모두 구현하고 나면 최초의 View가 로드되었을때 아래와 같이 스크롤뷰와 페이지 컨트롤러등을 초기화하기 위한 initScrollViewAndPageControl함수를 실행해주고 , 각 버튼들에 Tag를 세팅해주어야한다. 

 만약 페이징을 구현할때, 최초에 보여주어야하는 페이지가 0페이지가 아니라 다른 임의의 페이지가 되어야한다면 위에서 구현한 gotoPage:AtPage함수를 아래 소스의 주석과 같이 실행해주면된다.

- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view.
    
    // 버튼을 구분하기 위해 버튼에 tag를 설정해줍니다.
    [_btn_Prev setTag:1];
    [_btn_Next setTag:0];
    
    // 스크롤 뷰와 페이지 컨트롤들을 초기화해줍니다.
    [self initScrollViewAndPageControl];
    
    // 만약 최초에 보여줄 페이지를 변경하려면 아래줄의 주석을 풀어주면 됩니다.
    // Page는 원하는 페이지로 설정할 수 있습니다.
    // [self gotoPage:FALSE AtPage:2];
}


#PageViewController.h

 PageScrollViewcController의 구현이 모두 끝났다면, 사실상 PageViewController에서 구현할 것은 그렇게 많지않다. 물론 예제이기때문이겠지만.. 해당 파일의 내용은 아래의 소스와 같다. 여기서 굳이 중요한 부분을 꼽자면 initPageViewInfo가 그나마 중요하다고 할 수있겠다

 이 함수는 현재 페이지를 입력받고 보여줄 Text와 이미지를 초기화한다. 페이지를 구성하는 내용을 초기화하는 함수인셈.

@interface PageViewController : UIViewController

@property (strong,nonatomic)IBOutlet UIImageView *imageView;
@property (strong,nonatomic)IBOutlet UILabel *titleLabel;

-(void)initPageViewInfo:(NSInteger)page;

@end


#PageViewController.m

 이 파일에서는 각 프로퍼티들을 Synthesize하는 것과 initPageViewInfo함수 밖에 구현해둔 것이 없는데, 소스는 아래와 같다. 이 소스에서 명시해둔 이미지들은 첨부해둔 예제소스 압축파일에 동봉되어있다.

-(void)initPageViewInfo:(NSInteger)page{
    NSString *imgName;

    switch (page) {
        case 0:
            imgName = @"Bicycle.jpg";
            break;
        case 1:
            imgName = @"City.jpg";
            break;
        case 2:
            imgName = @"Surf.jpg";
            break;
        case 3:
            imgName = @"Leaves.jpg";
            break;
        default:
            break;
    }
    
    // 현재 페이지의 이미지와 텍스트 적용
    [_imageView setImage:[UIImage imageNamed:imgName]];
    [_titleLabel setText:[NSString stringWithFormat:@"Page %d",page]];
}

 이렇게 길고긴 코딩이 끝나고나면 우리가 원하는 스크롤뷰를 이용한 페이징 구현이 완성되게된다. 이 방법을 사용하면서 중간중간 찾아본바로는 PageController가 현재 구현한 기능들을 모두 포함하고있는 것같던데.. 정석으로 PageController를 사용해서 구현하는 방법도 나중에 찾아봐야할 것 같다. 급하게 작업을 끝마쳐야 했어서 눈앞에 보이는 예제로 작업했는데 오히려 길을 돌아간것 같은 느낌이.. 

+ Recent posts