처음부터 스와이프할 수 있는 UITabBar를 만드는 방법

게시 됨: 2022-03-11

아시다시피 Apple의 iOS SDK에는 무수히 많은 내장 UI 구성 요소가 포함되어 있습니다. 버튼, 컨테이너, 탐색, 탭 레이아웃, 이름만 지정하면 필요한 거의 모든 것이 있습니다. 아니면?

이러한 모든 기본 구성 요소를 사용하면 기본 구조화된 UI를 만들 수 있지만 상자를 벗어나야 하는 경우 어떻게 됩니까? iOS 개발자가 기본적으로 SDK에서 지원되지 않는 일종의 동작을 빌드해야 할 때?

이러한 경우 중 하나가 UITabBar 입니다. 여기서 탭 사이를 스와이프할 수 없고 탭 간 전환을 위한 애니메이션도 없습니다.

쉬운 UITabBar 수정 찾기

상당한 양의 검색 끝에 Github에서 유용한 라이브러리를 하나만 찾았습니다. 불행히도 라이브러리는 애플리케이션을 실행하는 동안 많은 문제를 발생시켰지만 언뜻 보기에는 우아한 솔루션처럼 보였습니다.

즉, 라이브러리는 사용하기가 매우 쉽지만 버그가 있어서 사용 편의성보다 훨씬 더 중요하고 문제를 일으키는 경향이 있습니다. 여전히 관심이 있는 경우 이 링크에서 lib를 찾을 수 있습니다.

그래서 약간의 생각과 많은 검색 후에 저는 제 솔루션을 구현하기 시작했고 제 자신에게 이렇게 말했습니다 . 이 두 가지를 함께 그룹화하여 탭바를 스와이프하거나 탭하면서 페이지 인덱스를 처리하면 어떻게 될까요?”

나중에 설명할 것처럼 다소 까다로웠지만 궁극적으로 해결책을 찾았습니다.

정교한 UITabBar 솔루션

세 개의 탭 표시줄 항목을 작성해야 한다고 상상해 보십시오. 이는 자동으로 각 탭 항목당 세 개의 페이지/컨트롤러가 표시된다는 것을 의미합니다.

이 경우 세 개의 뷰 컨트롤러를 인스턴스화해야 하며 탭 모음에 대한 두 개의 자리 표시자/빈 뷰 컨트롤러도 필요합니다. 탭 모음 항목을 만들고 탭을 눌렀을 때 상태를 변경하거나 사용자가 변경하려는 경우 탭 인덱스를 프로그래밍 방식으로.

이를 위해 Xcode를 살펴보고 몇 가지 클래스를 작성하여 이러한 작업이 어떻게 작동하는지 살펴보겠습니다.

탭 간 스와이프의 예

iOS에서 스와이프할 수 있는 탭의 예

이 스크린샷에서 첫 번째 탭 표시줄 항목이 파란색이고 사용자가 오른쪽 탭으로 스와이프한 다음(노란색) 마지막 화면에서 세 번째 항목이 선택되어 전체 페이지가 노란색으로 표시되는 것을 볼 수 있습니다.

스와이프할 수 있는 탭 표시줄의 프로그래밍 방식 사용

이제 이 기능을 자세히 살펴보고 iOS용 스와이프할 수 있는 탭바의 쉬운 예를 작성해 보겠습니다. 우선 새 프로젝트를 만들어야 합니다.

프로젝트에 필요한 전제 조건은 매우 기본적입니다. Mac에 설치된 Xcode 및 Xcode 빌드 도구입니다.

새 프로젝트를 생성하려면 Mac에서 Xcode 애플리케이션을 열고 "새 Xcode 프로젝트 생성" 을 선택한 다음 프로젝트 이름을 지정하고 마지막으로 생성할 애플리케이션 유형을 선택합니다. "Single View App" 을 선택하고 다음을 누르기만 하면 됩니다.

Xcode 스크린샷

보시다시피 다음 화면에서는 몇 가지 기본 정보를 제공해야 합니다.

  • 제품 이름: SwipeableTabbar라고 이름을 지정했습니다.
  • 팀: 실제 기기에서 이 애플리케이션을 실행하려면 개발자 계정이 있어야 합니다. 제 경우에는 제 계정을 사용하겠습니다.

참고: 개발자 계정이 없는 경우 시뮬레이터에서도 실행할 수 있습니다.

  • 조직 이름: Toptal 이라고 이름을 지정했습니다.
  • 조직 식별자: 이름을 com.toptal 로 지정했습니다.
  • 언어: Swift를 선택합니다.
  • 선택 취소: "핵심 데이터 사용", "단위 테스트 포함""UI 테스트 포함".

다음 버튼을 누르면 스와이프할 수 있는 탭 표시줄을 만들 준비가 된 것입니다.

단순한 아키텍처

지금까지 이미 알고 있듯이 새 앱을 만들 때 이미 Main ViewController 클래스와 Main.Storyboard 있습니다.

디자인을 시작하기 전에 먼저 필요한 모든 클래스와 파일을 만들어 작업의 UI 부분으로 진행하기 전에 모든 것을 설정하고 실행하도록 합시다.

프로젝트 내부 어딘가에 몇 개의 새 파일 -> TabbarController.swift , NavigationController.swift , PageViewController.swift 를 생성하기만 하면 됩니다.

제 경우에는 이렇게 보입니다.

스크린샷: Xcode 컨트롤러

다른 모든 메서드를 제거할 수 있으므로 AppDelegate 파일에서 didFinishLaunchingWithOptions 만 남겨둡니다.

didFinishLaunchingWithOptions 내부에서 아래 줄을 복사하여 붙여넣기만 하면 됩니다.

 window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = NavigationController(rootViewController: TabbarController()) window?.makeKeyAndVisible() return true

ViewController.swift 라는 파일에서 모든 것을 제거하십시오. 나중에 이 파일로 돌아오겠습니다.

먼저 NavigationController.swift 용 코드를 작성해 보겠습니다.

 import Foundation import UIKit class NavigationController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() navigationBar.isTranslucent = true navigationBar.tintColor = .gray } }

이를 통해 회색 TintColor가 있는 반투명 막대가 있는 간단한 UINavigationController 를 만들었습니다. 그게 다야

이제 PageViewController 를 계속 진행할 수 있습니다.

여기에는 이전에 논의한 파일보다 약간 더 많은 코딩이 필요합니다.

이 파일에는 하나의 클래스, 하나의 프로토콜, 일부 UIPageViewController 데이터 소스 및 대리자 메서드가 포함되어 있습니다.

결과 파일은 다음과 같아야 합니다.

Xcode 스크린샷: 메서드

보시다시피 PageViewControllerDelegate 라는 자체 프로토콜을 선언했습니다. 이 프로토콜은 탭 표시줄 컨트롤러에 스와이프가 처리된 후 페이지 인덱스가 변경되었음을 알려야 합니다.

 import Foundation import UIKit protocol PageViewControllerDelegate: class { func pageDidSwipe(to index: Int) }

그런 다음 뷰 컨트롤러를 보유하고 특정 인덱스에서 페이지를 선택하며 스와이프를 처리하는 PageViewController 라는 새 클래스를 만들어야 합니다.

첫 번째 실행에서 선택한 첫 번째 컨트롤러가 중앙 보기 컨트롤러여야 한다고 가정해 보겠습니다. 이 경우 기본 인덱스 값을 1로 지정합니다.

 class PageViewController: UIPageViewController { weak var swipeDelegate: PageViewControllerDelegate? var pages = [UIViewController]() var prevIndex: Int = 1 override func viewDidLoad() { super.viewDidLoad() self.dataSource = self self.delegate = self } func selectPage(at index: Int) { self.setViewControllers( [self.pages[index]], direction: self.direction(for: index), animated: true, completion: nil ) self.prevIndex = index } private func direction(for index: Int) -> UIPageViewController.NavigationDirection { return index > self.prevIndex ? .forward : .reverse } }

여기에서 볼 수 있듯이 모든 뷰 컨트롤러에 대한 참조를 포함하는 가변 pages 가 있습니다.

변수 prevIndex 는 마지막으로 선택한 인덱스를 저장하는 데 사용됩니다.

선택한 인덱스를 설정하기 위해 단순히 selectPage 메소드를 호출할 수 있습니다.

페이지 인덱스 변경 사항을 수신하려면 swipeDelegate 를 구독해야 하며 페이지를 스와이프할 때마다 페이지 인덱스가 변경되었다는 알림을 받고 현재 인덱스도 수신합니다.

메서드 방향은 UIPageViewController 의 스와이프 방향을 반환합니다. 이 클래스의 퍼즐의 마지막 조각은 대리자/데이터 소스 구현입니다.

다행히 이러한 구현은 매우 간단합니다.

 extension PageViewController: UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let viewControllerIndex = pages.firstIndex(of: viewController) else { return nil } let previousIndex = viewControllerIndex - 1 guard previousIndex >= 0 else { return nil } guard pages.count > previousIndex else { return nil } return pages[previousIndex] } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let viewControllerIndex = pages.firstIndex(of: viewController) else { return nil } let nextIndex = viewControllerIndex + 1 guard nextIndex < pages.count else { return nil } guard pages.count > nextIndex else { return nil } return pages[nextIndex] } } extension PageViewController: UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { if completed { guard let currentPageIndex = self.viewControllers?.first?.view.tag else { return } self.prevIndex = currentPageIndex self.swipeDelegate?.pageDidSwipe(to: currentPageIndex) } } }

위에서 볼 수 있듯이 세 가지 방법이 있습니다.

  • 첫 번째 것은 인덱스를 찾고 이전 뷰 컨트롤러를 반환합니다.
  • 두 번째 것은 인덱스를 찾고 다음 뷰 컨트롤러를 반환합니다.
  • 마지막 것은 스와이프가 종료되었는지 확인하고 현재 인덱스를 로컬 속성 prevIndex 로 설정한 다음 대리자 메서드를 호출하여 스와이프가 성공적으로 종료되었음을 상위 뷰 컨트롤러에 알립니다.

이제 마침내 UITabBarController 구현을 작성할 수 있습니다.

 import UIKit class TabbarController: UITabBarController { let selectedColor = UIColor.blue let deselectedColor = UIColor.gray let tabBarImages = [ UIImage(named: "ic_music")!, UIImage(named: "ic_play")!, UIImage(named: "ic_star")! ] override func viewDidLoad() { view.backgroundColor = .gray self.delegate = self tabBar.isTranslucent = true tabBar.tintColor = deselectedColor tabBar.unselectedItemTintColor = deselectedColor tabBar.barTintColor = UIColor.white.withAlphaComponent(0.92) tabBar.itemSpacing = 10.0 tabBar.itemWidth = 76.0 tabBar.itemPositioning = .centered setUp() self.selectPage(at: 1) } }

보시다시피 기본 속성과 스타일을 사용하여 TabbarController 를 만듭니다. 선택 및 선택 해제된 막대 항목에 대해 두 가지 색상을 정의해야 합니다. 또한 탭바 항목에 대한 세 가지 이미지를 소개했습니다.

viewDidLoad 에서 탭바의 기본 구성을 설정하고 페이지 #1을 선택합니다. 이것이 의미하는 바는 시작 페이지가 페이지 번호 1이 된다는 것입니다.

 private func setUp() { guard let centerPageViewController = createCenterPageViewController() else { return } var controllers: [UIViewController] = [] controllers.append(createPlaceholderViewController(forIndex: 0)) controllers.append(centerPageViewController) controllers.append(createPlaceholderViewController(forIndex: 2)) setViewControllers(controllers, animated: false) selectedViewController = centerPageViewController } private func selectPage(at index: Int) { guard let viewController = self.viewControllers?[index] else { return } self.handleTabbarItemChange(viewController: viewController) guard let PageViewController = (self.viewControllers?[1] as? PageViewController) else { return } PageViewController.selectPage(at: index) }

setUp 메서드 내에서 두 개의 자리 표시자 뷰 컨트롤러를 만든 것을 볼 수 있습니다. 이러한 자리 표시자 컨트롤러는 UITabBar 에 필요합니다. 탭 표시줄 항목 의 수는 보유하고 있는 뷰 컨트롤러의 수와 같아야 하기 때문입니다.

기억할 수 있다면 컨트롤러를 표시하기 위해 UIPageViewController 를 사용하지만 UITabBar 의 경우 완전히 작동 가능하게 만들려면 모든 뷰 컨트롤러를 인스턴스화해야 바 항목이 탭할 때 작동합니다. 따라서 이 예에서 placeholderviewcontroller #0#2 는 빈 보기 컨트롤러입니다.

중앙 보기 컨트롤러로서 3개의 보기 컨트롤러가 있는 PageViewController 를 만듭니다.

 private func createPlaceholderViewController(forIndex index: Int) -> UIViewController { let emptyViewController = UIViewController() emptyViewController.tabBarItem = tabbarItem(at: index) emptyViewController.view.tag = index return emptyViewController } private func createCenterPageViewController() -> UIPageViewController? { let leftController = ViewController() let centerController = ViewController2() let rightController = ViewController3() leftController.view.tag = 0 centerController.view.tag = 1 rightController.view.tag = 2 leftController.view.backgroundColor = .red centerController.view.backgroundColor = .blue rightController.view.backgroundColor = .yellow let storyBoard = UIStoryboard.init(name: "Main", bundle: nil) guard let pageViewController = storyBoard.instantiateViewController(withIdentifier: "PageViewController") as? PageViewController else { return nil } pageViewController.pages = [leftController, centerController, rightController] pageViewController.tabBarItem = tabbarItem(at: 1) pageViewController.view.tag = 1 pageViewController.swipeDelegate = self return pageViewController } private func tabbarItem(at index: Int) -> UITabBarItem { return UITabBarItem(title: nil, image: self.tabBarImages[index], selectedImage: nil) }

위에서 설명한 첫 번째 및 두 번째 방법은 페이지뷰 컨트롤러의 초기화 방법입니다.

메서드 tabbar 표시 tabbar 표시줄 항목을 반환합니다.

보시다시피 createCenterPageViewController() 내에서 각 뷰 컨트롤러에 태그를 사용하고 있습니다. 이것은 화면에 나타난 컨트롤러를 이해하는 데 도움이 됩니다.

다음으로 가장 중요한 메서드인 handleTabbarItemChange 에 대해 알아보겠습니다.

 private func handleTabbarItemChange(viewController: UIViewController) { guard let viewControllers = self.viewControllers else { return } let selectedIndex = viewController.view.tag self.tabBar.tintColor = selectedColor self.tabBar.unselectedItemTintColor = selectedColor for i in 0..<viewControllers.count { let tabbarItem = viewControllers[i].tabBarItem let tabbarImage = self.tabBarImages[i] tabbarItem?.selectedImage = tabbarImage.withRenderingMode(.alwaysTemplate) tabbarItem?.image = tabbarImage.withRenderingMode( i == selectedIndex ? .alwaysOriginal : .alwaysTemplate ) } if selectedIndex == 1 { viewControllers[selectedIndex].tabBarItem.selectedImage = self.tabBarImages[1].withRenderingMode(.alwaysOriginal) } }

이 방법에서는 뷰 컨트롤러를 매개변수로 사용하고 있습니다. 이 뷰 컨트롤러에서 태그를 선택한 인덱스로 가져옵니다. 탭 바의 경우 선택 색상과 선택되지 않은 색상을 설정해야 합니다.

이제 모든 컨트롤러를 반복하고 i == selectedIndex 인지 확인해야 합니다.

그런 다음 이미지를 원본 렌더링 모드 로 렌더링해야 합니다. 그렇지 않으면 이미지를 템플릿 모드 로 렌더링해야 합니다.

템플릿 모드 를 사용하여 이미지를 렌더링하면 항목의 색조 색상에서 색상이 상속됩니다.

거의 다 끝났습니다. UITabBarControllerDelegatePageViewControllerDelegate 에서 두 가지 중요한 메서드를 도입하면 됩니다.

 func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { self.selectPage(at: viewController.view.tag) return false } func pageDidSwipe(to index: Int) { guard let viewController = self.viewControllers?[index] else { return } self.handleTabbarItemChange(viewController: viewController) }

첫 번째 항목은 탭 항목을 누를 때 호출되고 두 번째 항목은 탭 사이를 스와이프할 때 호출됩니다.

마무리

모든 코드를 함께 넣으면 제스처 핸들러 구현을 직접 작성할 필요가 없고 탭 표시줄 항목 간에 부드러운 스크롤/스와이프를 제공하기 위해 많은 코드를 작성할 필요가 없다는 것을 알게 될 것입니다.

여기에서 설명하는 구현은 모든 시나리오에 이상적이지는 않지만 약간의 코드로 이러한 기능을 만들 수 있는 기발하고 빠르고 비교적 쉬운 솔루션입니다.

마지막으로 내 접근 방식을 시도하고 싶다면 내 GitHub 리포지토리를 사용할 수 있습니다. 즐거운 코딩!