개인작업을 하다보니 여러 앱에 빠지지않은 페이징기능을 구현해야할 경우가 생겨, 여러 방법을 찾아봤는데 어떻게된것이 페이지 컨트롤러를 분명 XCode에서 지원하는데도, 직접 구현하는 방법을 먼저 찾게되어버렸다.
그런고로 이번 게시물에서 다룰것은 그 페이징기능을 ScrollView와 PageControl을 이용해 구현하는 방법이다. 사용해보지는 않았지만, 페이지 컨트롤러로 페이징을 구현하는 것보다 귀찮은 짓일것이라고 생각된다.
샘플소스는 아래의 링크로 다운 !
스크롤뷰를 이용해 페이징을 구현하기에 앞서 스크롤뷰의 속성에 대해서 하나 알아둘 것이있다. 바로 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를 사용해서 구현하는 방법도 나중에 찾아봐야할 것 같다. 급하게 작업을 끝마쳐야 했어서 눈앞에 보이는 예제로 작업했는데 오히려 길을 돌아간것 같은 느낌이..
'DEV > iOS' 카테고리의 다른 글
하이브리드 앱 개발 시 UIWebView / WKWebView에서의 처리 (0) | 2017.10.18 |
---|---|
NSThread를 통한 데이터 처리와 화면 제어 팁 (0) | 2014.03.09 |
NSURLConnection와 NSURLRequest를 이용해 POST데이터를 동기적으로 통신 (0) | 2014.03.09 |
UIAlertView 간단한 함수로 만들어 두기 (0) | 2014.03.09 |
NSString을 파일로 읽고 쓰기 (0) | 2014.03.09 |