スワイプ可能なUITabBarをゼロから作成する方法
公開: 2022-03-11ご存知のように、AppleのiOS SDKには、無数の組み込みUIコンポーネントが含まれています。 ボタン、コンテナ、ナビゲーション、タブ付きレイアウトなど、名前を付けることができます。必要なものはほとんどすべて揃っています。 またはそれは?
これらの基本的なコンポーネントはすべて、基本的な構造化されたUIを作成することを可能にしますが、ボックスの外に出る必要がある場合はどうなりますか。 iOS開発者がデフォルトでSDKでサポートされていないある種の動作を構築する必要がある場合はどうなりますか?
これらのケースの1つはUITabBarで、タブ間をスワイプする機能がなく、タブを切り替えるためのアニメーションもありません。
簡単なUITabBar修正の検索
かなりの量の検索を行った後、Githubで有用なライブラリを1つだけ見つけることができました。 残念ながら、ライブラリはアプリケーションの実行中に多くの問題を引き起こしましたが、一見したところエレガントなソリューションのように見えました。
言い換えれば、ライブラリは非常に使いやすいと思いましたが、バギーは明らかにその使いやすさを上回り、問題を引き起こす傾向がありました。 それでも興味がある場合は、このリンクの下にライブラリがあります。
そこで、考えて多くの検索を行った後、独自のソリューションの実装を開始し、次のように自分に言い聞かせました。「スワイプにページビューコントローラーとネイティブUITabBarを使用するとどうなるでしょうか。 これら2つをグループ化して、タブバーをスワイプまたはタップしながらページインデックスを処理するとどうなりますか?」
最終的に、私は解決策を思いつきましたが、後で説明するように、それはややトリッキーであることがわかりました。
精巧なUITabBarソリューション
構築するタブバーアイテムが3つあるとします。これは、タブアイテムごとに3つのページ/コントローラーが自動的に表示されることを意味します。
この場合、これら3つのビューコントローラーをインスタンス化する必要があります。また、タブバーアイテムを作成したり、タブが押されたとき、またはユーザーが変更したいときに、タブバー用に2つのプレースホルダー/空のビューコントローラーが必要になります。プログラムでタブインデックス。
このために、Xcodeを掘り下げて、いくつかのクラスを作成して、これらがどのように機能するかを確認しましょう。
タブ間のスワイプの例
これらのスクリーンショットでは、最初のタブバーの項目が青色で、次にユーザーが右のタブ(黄色)にスワイプし、最後の画面に3番目の項目が選択されていることを示しているため、ページ全体が黄色で表示されます。
スワイプ可能なタブバーのプログラムによる使用
それでは、この機能を詳しく調べて、iOS用のスワイプ可能なタブバーの簡単な例を書いてみましょう。 まず、新しいプロジェクトを作成する必要があります。
私たちのプロジェクトに必要な前提条件は非常に基本的です:MacにインストールされたXcodeおよびXcodeビルドツール。
新しいプロジェクトを作成するには、MacでXcodeアプリケーションを開き、[新しいXcodeプロジェクトを作成する]を選択し、プロジェクトに名前を付けて、最後に作成するアプリケーションの種類を選択します。 「シングルビューアプリ」を選択し、「次へ」を押すだけです。
ご覧のとおり、次の画面では、いくつかの基本情報を入力する必要があります。
- 製品名: SwipeableTabbarという名前を付けました。
- チーム:このアプリケーションを実際のデバイスで実行する場合は、開発者アカウントが必要です。 私の場合、これには自分のアカウントを使用します。
注:開発者アカウントをお持ちでない場合は、シミュレーターでも実行できます。
- 組織名: Toptalと名付けました。
- 組織識別子: com.toptalという名前を付けました。
- 言語: Swiftを選択します。
- [コアデータを使用する]、[単体テストを含める]、 [ UIテストを含める]のチェックを外します。
[次へ]ボタンを押すと、スワイプ可能なタブバーの作成を開始する準備が整います。
シンプルなアーキテクチャ
すでにご存知のように、新しいアプリを作成すると、 ViewController
クラスとMain.Storyboard
が既に存在します。
設計を開始する前に、まず必要なすべてのクラスとファイルを作成して、ジョブのUI部分に進む前に、すべてがセットアップされて実行されていることを確認しましょう。
プロジェクト内のどこかで、いくつかの新しいファイルを作成するだけです-> TabbarController.swift 、 NavigationController.swift 、 PageViewController.swift 。
私の場合はこんな感じです。
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 } }
これで、単純なUINavigationController
を作成しました。ここでは、灰色のTintColorを備えた半透明のバーがあります。 これですべてです。
これで、 PageViewController
の取得に進むことができます。
その中で、前に説明したファイルよりも少し多くコーディングする必要があります。
このファイルには、1つのクラス、1つのプロトコル、いくつかのUIPageViewController
データソース、およびデリゲートメソッドが含まれています。
結果のファイルは次のようになる必要があります。
ご覧のとおり、 PageViewControllerDelegate
という独自のプロトコルを宣言しました。これにより、スワイプが処理された後にページインデックスが変更されたことをタブバーコントローラーに通知する必要があります。
import Foundation import UIKit protocol PageViewControllerDelegate: class { func pageDidSwipe(to index: Int) }
次に、 PageViewController
という新しいクラスを作成する必要があります。このクラスは、View Controllerを保持し、特定のインデックスでページを選択し、スワイプも処理します。
最初の実行で最初に選択されたコントローラーがセンタービューコントローラーであると想像してみましょう。 この場合、デフォルトのインデックス値を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 } }
ここでわかるように、すべてのViewControllerの参照を含む可変pages
があります。
変数prevIndex
は、最後に選択されたインデックスを格納するために使用されます。
選択したインデックスを設定するには、 selectPage
メソッドを呼び出すだけです。
ページインデックスの変更をリッスンする場合は、 swipeDelegate
にサブスクライブする必要があります。ページをスワイプするたびに、ページインデックスが変更されたことが通知され、さらに現在のインデックスも受信されます。
メソッドdirectionは、 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) } } }
上記のように、3つの方法があります。

- 最初のものはインデックスを見つけて、前のViewControllerを返します。
- 2つ目はインデックスを見つけて、次のViewControllerを返します。
- 最後の1つは、スワイプが終了したかどうかを確認し、現在のインデックスをローカルプロパティ
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
を作成します。 選択されたバーアイテムと選択解除されたバーアイテムの2つの色を定義する必要があります。 また、タブバーアイテムの画像を3つ紹介しました。
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メソッド内で、2つのプレースホルダービューコントローラーを作成したことがわかります。 これらのプレースホルダーコントローラーはUITabBar
に必要です。これは、タブバーアイテムの数が、使用しているビューコントローラーの数と同じである必要があるためです。
思い出せる場合は、コントローラーを表示するためにUIPageViewController
を使用しますが、 UITabBar
の場合、完全に機能させるには、すべてのビューコントローラーをインスタンス化して、バーアイテムをタップしたときに機能するようにする必要があります。 したがって、この例では、 placeholderviewcontroller#0と#2は空のViewControllerです。
中央に配置されたViewControllerとして、3つのViewControllerを備えた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) }
上記の1番目と2番目のメソッドは、ページビューコントローラーのinitメソッドです。
メソッド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) } }
この方法では、ViewControllerをパラメーターとして使用しています。 このViewControllerから、選択したインデックスとしてタグを取得します。 タブバーには、選択した色と選択していない色を設定する必要があります。
次に、すべてのコントローラーをループして、 i == selectedIndex
かどうかを確認する必要があります。
次に、画像を元のレンダリングモードとしてレンダリングする必要があります。そうでない場合は、画像をテンプレートモードとしてレンダリングする必要があります。
テンプレートモードを使用して画像をレンダリングすると、アイテムの色合いの色から色が継承されます。
ほぼ完了です。 UITabBarControllerDelegate
とPageViewControllerDelegate
から2つの重要なメソッドを導入する必要があります。
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) }
1つ目は、タブアイテムを押すと呼び出され、2つ目は、タブ間をスワイプすると呼び出されます。
まとめ
すべてのコードをまとめると、ジェスチャーハンドラーの独自の実装を作成する必要がなく、タブバーアイテム間でスムーズなスクロール/スワイプを実現するために多くのコードを作成する必要がないことに気付くでしょう。
ここで説明する実装は、すべてのシナリオに理想的なものではありませんが、少しのコードでこれらの機能を作成できる、風変わりで迅速かつ比較的簡単なソリューションです。
最後に、私のアプローチを試してみたい場合は、私のGitHubリポジトリを使用できます。 ハッピーコーディング!