В данной статье возьмем курс на проектировку и реализацию переключателя страниц (view pager)  —  элемента UI, применяемого во многих приложениях. По итогам обучения получим вот такой результат: 

Мы также сделаем ViewPager переиспользуемым и компонуемым, что даст возможность задействовать его как с верхними вкладками, так и без них. 

Исходный код проекта доступен в конце статьи. 

Начало работы 

Если разложить компонент ViewPager на составляющие, то станет ясно, что он образован из двух частей: представления со вкладками (tabbed view) в верхней области и представления со страницами (paged view) в нижней. В идеале лучше одновременно использовать оба элемента или ограничиться только вторым. В связи с этим у нас будут 3 основных класса:

  • ViewPager объединяет TabbedView и PagedView, обеспечивая их совместную работу;
  • TabbedView —  верхний элемент, допускающий выбор одной вкладки за раз; 
  • PagedView —  нижний элемент, содержащий “страницы” с возможностью их прокрутки. 

PagedView

Начнем с создания простого подкласса UIView и его протокола делегата: 

import UIKit

protocol PagedViewDelegate: AnyObject {
func didMoveToPage(index: Int)
}

class PagedView: UIView {

// MARK: - Инициализация
init() {
super.init(frame: .zero)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Свойства
public weak var delegate: PagedViewDelegate?
}

Позднее воспользуемся PagedViewDelegate для отправки уведомлений другим классам при прокрутке пользователем страниц. 

За кадром PagedView привлечет UICollectionView для реализации разбивки на страницы. Поэтому создаем свойство collectionView и приводим в соответствие с протоколами UICollectionViewDelegateFlowLayout и UICollectionViewDataSource:

import UIKit

protocol PagedViewDelegate: AnyObject {
func didMoveToPage(index: Int)
}

class PagedView: UIView, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {

// MARK: - Инициализация
init() {
super.init(frame: .zero)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Свойства
public weak var delegate: PagedViewDelegate?

// MARK: - Источник данных
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// 1
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// 2
}

// MARK: - Делегат макета
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

// 3
return CGSize(width: self.collectionView.frame.width,
height: self.collectionView.frame.height)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {

// 4
return 0
}
}

Данный код подразумевает выполнение следующих действий: 

  1. В методе numbersOfItemsInSection возвращаем общее количество страниц в PagedView
  2. В методе cellForItemAt возвращается UICollectionViewCell, представляющий определенную страницу. 
  3. Метод sizeForItemAt позволяет установить размер для каждой страницы. Они занимают все свободное пространство. 
  4. Поскольку интервалы между страницами не требуются, то внутри метода делегата макета minimumLineSpacingForSectionAt возвращается 0.

Теперь реализуем фактический UICollectionView и добавляем его в иерархию представления: 

import UIKit

protocol PagedViewDelegate: AnyObject {
func didMoveToPage(index: Int)
}

class PagedView: UIView, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {

// MARK: - Инициализация
init() {
super.init(frame: .zero)

self.setupUI()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Свойства
public weak var delegate: PagedViewDelegate?

private lazy var collectionView: UICollectionView = {
// 1
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: layout
)
// 2
collectionView.showsHorizontalScrollIndicator = false
collectionView.isPagingEnabled = true
// 3
collectionView.delegate = self
collectionView.dataSource = self
// 4
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}()

// MARK: - Настройка UI UI Setup
func setupUI() {

// 5
self.translatesAutoresizingMaskIntoConstraints = false

self.addSubview(collectionView)
collectionView.backgroundColor = .white

NSLayoutConstraint.activate([
collectionView.widthAnchor
.constraint(equalTo: self.widthAnchor),
collectionView.heightAnchor
.constraint(equalTo: self.heightAnchor),
collectionView.centerXAnchor
.constraint(equalTo: self.centerXAnchor),
collectionView.centerYAnchor
.constraint(equalTo: self.centerYAnchor)
])
}

// MARK: - Источник данных
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

}

// MARK: - Делегат макета
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

return CGSize(width: self.collectionView.frame.width,
height: self.collectionView.frame.height)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {

return 0
}
}

Последовательно добиваемся следующих результатов: 

  1. Инициализируем UICollectionViewFlowLayout с горизонтальным направлением прокрутки.
  2. Скрываем индикатор горизонтальной прокрутки представления коллекции и разрешаем разбивку на страницы. 
  3. Связываем делегата и источник данных представления коллекции.  
  4. Устанавливаем translatesAutoresizingMaskIntoConstraints значение false, так как UI создается программным способом. 
  5. Внутри метода setupUI() добавляем СоllectionView на экран и добиваемся, чтобы он занял все свободное пространство. Кроме того, не забываем вызвать этот метод внутри инициализатора.

Теперь приступаем к реализации PageCollectionViewCell, представляющего каждую страницу: 

import UIKit

class PageCollectionViewCell: UICollectionViewCell {

// MARK: - Инициализация
override init(frame: CGRect) {
super.init(frame: frame)

self.setupUI()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// 1
public var view: UIView? {
didSet {
self.setupUI()
}
}

private func setupUI() {
guard let view = view else { return }

self.contentView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false

// 2
NSLayoutConstraint.activate([
view.leftAnchor
.constraint(equalTo: self.contentView.leftAnchor),
view.topAnchor
.constraint(equalTo: self.contentView.topAnchor),
view.rightAnchor
.constraint(equalTo: self.contentView.rightAnchor),
view.bottomAnchor
.constraint(equalTo: self.contentView.bottomAnchor)
])
}
}
  1. Создаем свойство view, которое представляет содержимое страницы. 
  2. Добиваемся заполнения view всей ячейки. 

Обновляем файл PagedView.swift:

import UIKit

protocol PagedViewDelegate: AnyObject {
func didMoveToPage(index: Int)
}

class PagedView: UIView, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {

// MARK: - Инициалиация

// 1
init(pages: [UIView] = []) {

self.pages = pages
super.init(frame: .zero)

self.setupUI()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Свойства
public weak var delegate: PagedViewDelegate?

// 2
public var pages: [UIView] {
didSet {
self.collectionView.reloadData()
}
}

private lazy var collectionView: UICollectionView = {
...
// 3
collectionView.register(PageCollectionViewCell.self, forCellWithReuseIdentifier: "PageCollectionViewCell")
...
return collectionView
}()

// MARK: - Настройка UI
...

// MARK: - Источник данных

// 4
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return pages.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PageCollectionViewCell", for: indexPath) as! PageCollectionViewCell
let page = self.pages[indexPath.item]
cell.view = page
return cell
}

// MARK: - Делегат макета
...
}

Вносим следующие корректировки: 

  1. Добавляем параметр pages в инициализатор. 
  2. Создаем свойство pages, после чего перезагружаем представление коллекции. 
  3. Регистрируем ранее созданный PageCollectionViewCell в представлении коллекции. 
  4. Возвращаем количество pages в методе источника данных numberOfItemsInSection. Помимо этого, инициализируем PageCollectionViewCell, снабжаем его страницей и возвращаем внутри метода источника данных cellForItemAt

С компонентом PagedView работа почти закончена. Осталось лишь внести действие для перемещения страницы программным образом и способ уведомить о нем делегата. С этой целью добавляем методы moveToPage(at index:) и scrollViewDidEndDecelerating:

import UIKit

protocol PagedViewDelegate: AnyObject {
func didMoveToPage(index: Int)
}

class PagedView: UIView, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {

// MARK: - Инициализация
...

// MARK: - Свойства
...

// MARK: - Настройка UI
...

// MARK: - Источник данных
...

// MARK: - Действия

// 1
public func moveToPage(at index: Int) {
self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
}

// MARK: - Делегат

// 2
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let page = Int(self.collectionView.contentOffset.x / self.collectionView.frame.size.width)

self.delegate?.didMoveToPage(index: page)
}

// MARK: - Делегат макета
...
}
  1. При вызове moveToPage(at index:) представление коллекции прокручивается до заданной страницы. 
  2. Как только пользователь закончил прокрутку страницы, получаем ее индекс и уведомляем об этом делегата PagedView. Как упоминалось ранее, делегатом может быть другое представление или его контроллер. При намерении использовать только PagedView делегатом обычно становится контроллер представления. В случае применения ViewPager, содержащего и PagedView, и TabbedView поверх него, он послужит делегатом PagedView.

Закончив с PagedView переходим к созданию компонента TabbedView со вкладками. За раз может быть выбрана только одна из них:

TabbedView

Начнем с создания подкласса UIView, представленного ниже:

import UIKit

// 1
protocol TabbedViewDelegate: AnyObject {
func didMoveToTab(at index: Int)
}

class TabbedView: UIView {

// 2
enum SizeConfiguration {
case fillEqually(height: CGFloat, spacing: CGFloat = 0)
case fixed(width: CGFloat, height: CGFloat, spacing: CGFloat = 0)

var height: CGFloat {
switch self {
case let .fillEqually(height, _):
return height
case let .fixed(_, height, _):
return height
}
}
}

// MARK: - Жизненный цикл

// 3
init(sizeConfiguration: SizeConfiguration) {

self.sizeConfiguration = sizeConfiguration
super.init(frame: .zero)

self.setupUI()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Свойства
weak var delegate: TabbedViewDelegate?

public let sizeConfiguration: SizeConfiguration

// 4
private var currentlySelectedIndex: Int = 0

// 5
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.estimatedItemSize = .zero
let collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: layout
)
collectionView.backgroundColor = .white
collectionView.dataSource = self
collectionView.delegate = self
collectionView.showsHorizontalScrollIndicator = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
return collectionView
}()

// MARK: Настройка UI

// 6
private func setupUI() {
self.translatesAutoresizingMaskIntoConstraints = false

self.addSubview(collectionView)

NSLayoutConstraint.activate([
collectionView.leftAnchor
.constraint(equalTo: self.leftAnchor),
collectionView.topAnchor
.constraint(equalTo: self.topAnchor),
collectionView.rightAnchor
.constraint(equalTo: self.rightAnchor),
collectionView.bottomAnchor
.constraint(equalTo: self.bottomAnchor)
])
}
}

Здесь мы выполняем следующие действия: 

  1. Добавляем TabbedViewDelegate для отправки уведомлений по факту выбора пользователем определенной вкладки. 
  2. Перечисление SizeConfiguration позволяет либо задать размер для вкладок, либо заполнить ими свободное пространство. Благодаря .fillEqually SizeConfiguration UI выглядит так:

3. В инициализаторе устанавливаем свойство sizeConfiguration и запускаем метод setupUI.

4. Свойство currentlySelectedIndex помогает отслеживать выбранную вкладку. 

5. Также как и в PagedView создаем горизонтальный UICollectionView

6. Метод setupUI размещает представление коллекции на экране и заполняет им свободное пространство. 

Следующая задача  —  реализовать UICollectionViewCell, представляющий вкладку. Создаем новый класс TabCollectionViewCell

import UIKit

// 1
protocol TabItemProtocol: UIView {
func onSelected()
func onNotSelected()
}

class TabCollectionViewCell: UICollectionViewCell {

// MARK: - Инициализация
override init(frame: CGRect) {
super.init(frame: frame)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Свойства

// 2
public var view: TabItemProtocol? {
didSet {
self.setupUI()
}
}

// 3
var leftConstraint = NSLayoutConstraint()
var topConstraint = NSLayoutConstraint()
var rightConstraint = NSLayoutConstraint()
var bottomConstraint = NSLayoutConstraint()

public var contentInsets: UIEdgeInsets = UIEdgeInsets(
top: 0,
left: 0,
bottom: 0,
right: 0
) {
didSet {
leftConstraint.constant = contentInsets.left
topConstraint.constant = contentInsets.top
rightConstraint.constant = -contentInsets.right
bottomConstraint.constant = -contentInsets.bottom
self.contentView.layoutIfNeeded()
}
}

// MARK: - Настройка UI

// 4
private func setupUI() {
guard let view = view else { return }

self.contentView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false

leftConstraint = view.leftAnchor
.constraint(equalTo: self.contentView.leftAnchor,
constant: contentInsets.left)
topConstraint = view.topAnchor
.constraint(equalTo: self.contentView.topAnchor,
constant: contentInsets.top)
rightConstraint = view.rightAnchor
.constraint(equalTo: self.contentView.rightAnchor,
constant: -contentInsets.right)
bottomConstraint = view.bottomAnchor
.constraint(equalTo: self.contentView.bottomAnchor,
constant: -contentInsets.bottom)

NSLayoutConstraint.activate([
leftConstraint,
topConstraint,
rightConstraint,
bottomConstraint
])
}
}

Последовательность действий:

  1. Создаем протокол TabItemProtocol, которому должна соответствовать каждая вкладка. При использовании TabbedView необходимо обеспечить пользовательское представление для каждой вкладки. Оно требует реализации методов onSelected() и onNotSelected() протокола TabItemProtocol

2. После установки свойства представления запускаем метод setupUI().

3. Добавляем способ установки необходимых отступов (insets) для каждой вкладки. После установки свойства contentInsets все ограничения обновляются соответствующим образом.  

4. Метод setupUI() добавляет ранее определенное свойство view на экран и активирует его ограничения. 

Отлично! Теперь необходимо обновить файл TabbedView.swift путем реализации протоколов UICollectionViewDataSource, UICollectionViewDelegate и UICollectionViewDelegateFlowLayout:

import UIKit

protocol TabbedViewDelegate: AnyObject {
func didMoveToTab(at index: Int)
}

class TabbedView: UIView {

...

// MARK: - Жизненный цикл

// 1
init(sizeConfiguration: SizeConfiguration,
tabs: [TabItemProtocol] = []) {

self.sizeConfiguration = sizeConfiguration
self.tabs = tabs
super.init(frame: .zero)

self.setupUI()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Свойства
weak var delegate: TabbedViewDelegate?

public let sizeConfiguration: SizeConfiguration

// 2
public var tabs: [TabItemProtocol] {
didSet {
self.collectionView.reloadData()
self.tabs[currentlySelectedIndex].onSelected()
}
}
private var currentlySelectedIndex: Int = 0

private lazy var collectionView: UICollectionView = {
...

// 3
collectionView.register(TabCollectionViewCell.self, forCellWithReuseIdentifier: "TabCollectionViewCell")
...
return collectionView
}()

// MARK: - Действие

// 4
public func moveToTab(at index: Int) {
self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)

self.tabs[currentlySelectedIndex].onNotSelected()
self.tabs[index].onSelected()

self.currentlySelectedIndex = index
}

// MARK: Настройка UI
private func setupUI() {
...
}
}

extension TabbedView: UICollectionViewDelegateFlowLayout {

// 5
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

switch sizeConfiguration {
case let .fillEqually(height, spacing):
let totalWidth = self.frame.width
let widthPerItem = (
totalWidth - (
spacing * CGFloat((self.tabs.count + 1))
)
) / CGFloat(self.tabs.count)

return CGSize(width: widthPerItem,
height: height)

case let .fixed(width, height, spacing):
return CGSize(width: width - (spacing * 2),
height: height)
}
}

// 6
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
switch sizeConfiguration {
case let .fillEqually(_, spacing),
let .fixed(_, _, spacing):

return spacing
}
}

}

extension TabbedView: UICollectionViewDataSource {
// 7
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return tabs.count
}

// 8
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TabCollectionViewCell", for: indexPath) as! TabCollectionViewCell
cell.view = tabs[indexPath.row]
return cell
}
}

extension TabbedView: UICollectionViewDelegate {

// 9
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.moveToTab(at: indexPath.item)
self.delegate?.didMoveToTab(at: indexPath.item)
}
}

Перечень изменений:

  1. В инициализатор добавляем параметр [TabItemProtocol] для установки вкладок. 
  2. Как только вкладки установлены, перезагружаем данные представления коллекции и выбираем вкладку в соответствии со свойством curentlySelectedIndex. По умолчанию оно равно 0, поэтому выбирается первая вкладка.
  3. Регистрируем TabCollectionViewCell в представлении коллекции. 
  4. moveToTab(at index:) позволяет программно выбрать нужную вкладку и соответствующим образом обновить состояние. 
  5. В методе sizeForItemAt делегата макета изучаем предоставленное свойство sizeConfiguration и устанавливаем размер для каждой вкладки. 
  6. Метод minimumLineSpacingForSectionAt задает интервалы между вкладками, если в этом есть необходимость. Как в предыдущем случае, изучаем свойство sizeConfiguration и возвращаем требуемое значение. По умолчанию spacing равняется 0. 
  7. В методе numberOfItemsInSection просто возвращаем количество вкладок. 
  8. Снабжаем каждую ячейку TabCollectionViewCell представлением, являющимся вкладкой. 
  9. Наконец, реагируем на нажатие вкладок, корректируя переменные состояния посредством метода moveToTab(at index:). Кроме того, уведомляем делегата о переходе пользователя на определенную вкладку. 

Реализация TabbedView завершена, и осталось лишь наладить совместную работу PagedView и TabbedView. Для этого создадим новый класс ViewPager.

ViewPager

Этот класс выглядит просто и отвечает за совместную работу PagedView и TabbedView:

import UIKit

class ViewPager: UIView {

// MARK: - Инициализация

// 1
init(tabSizeConfiguration: TabbedView.SizeConfiguration) {
self.sizeConfiguration = tabSizeConfiguration
super.init(frame: .zero)

self.setupUI()

tabbedView.delegate = self
pagedView.delegate = self
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

public let sizeConfiguration: TabbedView.SizeConfiguration

// 2
public lazy var tabbedView: TabbedView = {
let tabbedView = TabbedView(
sizeConfiguration: sizeConfiguration
)
return tabbedView
}()

public let pagedView = PagedView()

// MARK: - Настройка UI

// 3
private func setupUI() {
self.translatesAutoresizingMaskIntoConstraints = false

self.addSubview(tabbedView)
self.addSubview(pagedView)

NSLayoutConstraint.activate([
tabbedView.leftAnchor
.constraint(equalTo: self.leftAnchor),
tabbedView.topAnchor
.constraint(equalTo: self.topAnchor),
tabbedView.rightAnchor
.constraint(equalTo: self.rightAnchor),
tabbedView.heightAnchor
.constraint(equalToConstant: sizeConfiguration.height)
])

NSLayoutConstraint.activate([
pagedView.leftAnchor
.constraint(equalTo: self.leftAnchor),
pagedView.topAnchor
.constraint(equalTo: self.tabbedView.bottomAnchor),
pagedView.rightAnchor
.constraint(equalTo: self.rightAnchor),
pagedView.bottomAnchor
.constraint(equalTo: self.bottomAnchor)
])
}
}

// 4
extension ViewPager: TabbedViewDelegate {
func didMoveToTab(at index: Int) {
self.pagedView.moveToPage(at: index)
}
}

extension ViewPager: PagedViewDelegate {
func didMoveToPage(index: Int) {
self.tabbedView.moveToTab(at: index)
}
}
  1. Предоставляем параметр TabbedView.SizeConfiguration, чтобы внести конфигурацию размера для представления с вкладками. 
  2. В свойстве tabbedView передаем внесенную конфигурацию размера. 
  3. Метод setupUI() размещает TabbedView сверху, а PagedView снизу. 
  4. Мы также следуем TabbedViewDelegate и PagedViewDelegate для получения уведомления о прокрутке любого из компонентов. Когда пользователь выбирает вкладку, PagedView перемещается на нужную страницу программным способом. Таким же образом при прокрутке пользователем страницы выбирается соответствующая вкладка. 

Отлично! Наконец-то, переиспользуемый ViewPager готов. Теперь применим его в контроллере представления. 

Практический пример

Начнем с пустого UIViewController, который отображает сверху заголовок “ViewPager”:

import UIKit

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

self.overrideUserInterfaceStyle = .light
self.view.backgroundColor = .white
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

self.navigationItem.title = "ViewPager"

self.navigationController?.navigationBar.standardAppearance.backgroundColor = .systemBlue
self.navigationController?.navigationBar.standardAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
}
}

Затем создаем ViewPager и добавляем его на экран следующим образом:

import UIKit

class ViewController: UIViewController {

lazy var viewPager: ViewPager = {
// 1
let viewPager = ViewPager(
tabSizeConfiguration: .fillEqually(height: 60, spacing: 0)
)

// 2
let view1 = UIView()
view1.backgroundColor = .red

let view2 = UIView()
view2.backgroundColor = .blue

let view3 = UIView()
view3.backgroundColor = .orange

// 3
viewPager.tabbedView.tabs = [
AppTabItemView(title: "First"),
AppTabItemView(title: "Second"),
AppTabItemView(title: "Third")
]
viewPager.pagedView.pages = [
view1,
view2,
view3
]
viewPager.translatesAutoresizingMaskIntoConstraints = false
return viewPager
}()

override func viewDidLoad() {
...

// 4
self.view.addSubview(viewPager)

NSLayoutConstraint.activate([
viewPager.widthAnchor.constraint(equalTo: self.view.widthAnchor),
viewPager.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.7),
viewPager.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
viewPager.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor)
])
}

override func viewWillAppear(_ animated: Bool) {
...
}
}
  1. Инициализируем ViewPager с конфигурацией размера .fillEqually
  2. Далее создаем три представления разных цветов. Они воплощают страницы. 
  3. Добавляем 3 вкладки с названиями “First,” “Second” и “Third”, а также снабжаем ViewPager ранее созданными страницами. 
  4. Размещаем ViewPager на экране так, чтобы он занимал 70% доступной высоты. 

Закончив сборку и запустив приложение, получаем желаемый результат: 

Дополнительные ресурсы

Исходный код доступен на GitHub.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Zafar Ivaev: How To Create a View Pager in Swift 5

Предыдущая статьяСоздание платформы обработки и анализа данных Bazaar
Следующая статьяЧто делать, когда ваши сотрудники конфликтуют?