Sabe aquelas telas de boas-vindas que vemos quando abrimos um app pela primeira vez? Este processo é feito para familiarizar o usuário com o app e suas funções. É uma oportunidade para apresentar as principais funcionalidades ou diferenciais do app, novidades de uma versão, ou tentar convencer usuários a se registrarem no serviço.
Neste tutorial irei ensinar como criar um onboarding no iOS usando UICollectionView e Storyboard. Os controllers das views serão escritos em Swift 4. Se quiser baixar o projeto completo, siga este link para o GitHub.
Storyboard
Acho legal usar o storyboard pra desenhar as views porque economiza muito tempo e linhas de código, fora a vantagem de poder ver todo o fluxo de telas da aplicação de uma vez!
Por isso, vamos criar um novo projeto, abrir o storyboard, remover a View Controller padrão e colocar em seu lugar uma Collection View Controller. Com a ajuda do Document Outline, selecione a Collection View que está dentro da Collection View Controller. Queremos alterar algumas propriedades para que ela se comporte como um onboarding com scroll horizontal:

Além disso, vamos desmarcar as propriedades Show Horizontal Indicator e Show Vertical Indicator para que as barras de rolagem não apareçam. Desmarque também Bounce On Scroll e Bounce On Zoom, para que não ocorra o efeito de bounce ao fazer o scroll. E também marque a opção Paging Enabled, para que o scroll não pare entre uma página e outra.
Por fim, ainda no painel Utilities, na aba Size inspector, vamos alterar as propriedades Min Spacing For Cells e Min Spacing For Lines para 0. Isto fará qua não haja espaço entre as células, para que cada uma ocupe de fato a tela inteira (este tamanho vamos definir em código mais adiante).
A propriedade Items da Collection View controla quantas células ela possui. Neste onboarding faremos com que a célula ocupe a tela inteira. Assim, Items dirá quantas páginas vamos ter no onboarding.
Vamos agora desenhar o protótipo da célula da Collection View. Todas as células seguirão este protótipo, mudaremos apenas as informações de cada uma. O pequeno quadrado no canto superior esquerdo da Collection View (?) é o protótipo da célula. Dentro dele ficarão os componentes visuais que compõem o protótipo. Para conseguirmos posicioná-los, aumente o tamanho da célula (não se preocupe com o tamanho real, pois definiremos isso programaticamente para preencher a tela inteira).
O layout será uma Image View no topo, com dois Labels em baixo e um Button. Para diminuir o número de Constraints, empilharemos tudo em uma Stack View vertical:

Vamos configurar a Stack View para ter Distribution: Equal Centering e Spacing: Standard. Depois, centralizá-la e definir a largura de 260px usando constraints.


Para os Labels dentro da Stack View, defina a propriedade Lines: 0 para que textos maiores quebrem a linha automaticamente. O primeiro label será o título, então usaremos a fonte com Style: Bold e Size: 24px.
Para a Image View, vamos adicionar uma constraint de altura fixa 120px. Além disso, o Content Mode será Aspect Fit.
E o Button terá a constraint de altura e largura fixos. A largura será >= 260px, e a altura será 40px. Assim ele ocupa a largura inteira da Stack View.
Código
Vamos agora escrever as classes por trás das views que desenhamos no storyboard. Precisaremos das seguintes:
OnboardingCollectionViewController: responsável pela Collection View – data source e delegate.OnboardingCollectionViewCell: responsável por preencher os dados de cada célula da Collection View.OnboardingModel: struct que define a estrutura dos dados apresentados no onboarding.OnboardingModelFactory: para facilitar a instanciação dos modelos que serão usados no onboarding.
Seguem abaixo:
| import UIKit | |
| class OnboardingCollectionViewController: UICollectionViewController { | |
| } | |
| extension OnboardingCollectionViewController: UICollectionViewDelegateFlowLayout { | |
| } |
| import UIKit | |
| class OnboardingCollectionViewCell: UICollectionViewCell { | |
| static let reuseIdentifier = “OnboardingCell“ | |
| } |
| import UIKit | |
| struct OnboardingModel { | |
| let title: String | |
| let content: String | |
| let iconImage: String | |
| let backgroundColor: UIColor | |
| let hideButton: Bool | |
| } |
| import UIKit | |
| class OnboardingModelFactory { | |
| static func getPages() -> [OnboardingModel] { | |
| return [ | |
| OnboardingModel(title: “Feature 1“, | |
| content: “Esta feature faz com que o aplicativo faça coisas incríveis!“, | |
| iconImage: “iconOnboarding1“, | |
| backgroundColor: Color.lightPurple, | |
| hideButton: true), | |
| OnboardingModel(title: “Feature 2“, | |
| content: “Com esta feature, o aplicativo se diferencia dos concorrentes, pois faz X, Y e Z.“, | |
| iconImage: “iconOnboarding2“, | |
| backgroundColor: Color.lightGreen, | |
| hideButton: true), | |
| OnboardingModel(title: “Feature 3“, | |
| content: “Concluindo o porque este aplicativo é demais, e convidando o usuário a começar.“, | |
| iconImage: “iconOnboarding3“, | |
| backgroundColor: Color.lightOrange, | |
| hideButton: false) | |
| ] | |
| } | |
| } |
UICollectionViewController
Depois de criar os arquivos, volte para o storyboard e defina as classes da Collection View Controller e da Collection View Cell. Além disso, defina o Reuse Identifier da célula como “OnboardingCell” (painel Utilities, na aba Attributes inspector).
Vamos agora implementar os métodos da classe OnboardingCollectionViewController. Nela definiremos a quantidade de páginas do onboarding, as informações de cada página, o tamanho (tela inteira), e também colocaremos um UIPageControl para indicar ao usuário qual página ele está atualmente.
| import UIKit | |
| class OnboardingCollectionViewController: UICollectionViewController { | |
| private let pages = OnboardingModelFactory.getPages() | |
| private lazy var pageControl: UIPageControl = { | |
| let pageControl = UIPageControl() | |
| pageControl.numberOfPages = pages.count | |
| return pageControl | |
| }() | |
| override func viewDidLoad() { | |
| super.viewDidLoad() | |
| addPageControlToView() | |
| } | |
| private func addPageControlToView() { | |
| view.addSubview(pageControl) | |
| pageControl.translatesAutoresizingMaskIntoConstraints = false | |
| pageControl.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8).isActive = true | |
| pageControl.heightAnchor.constraint(equalToConstant: 40).isActive = true | |
| pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true | |
| } | |
| } | |
| extension OnboardingCollectionViewController { | |
| // MARK: UICollectionViewDataSource | |
| override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { | |
| return pages.count | |
| } | |
| override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { | |
| guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OnboardingCollectionViewCell.reuseIdentifier, for: indexPath) | |
| as? OnboardingCollectionViewCell else { fatalError(“Could not cast dequeued cell“) } | |
| cell.fill(with: pages[indexPath.row]) | |
| return cell | |
| } | |
| } | |
| extension OnboardingCollectionViewController: UICollectionViewDelegateFlowLayout { | |
| // MARK: UICollectionViewDelegateFlowLayout | |
| func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { | |
| return CGSize(width: collectionView.bounds.size.width, height: collectionView.bounds.size.height) | |
| } | |
| override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { | |
| let pageNumber = Int(targetContentOffset.pointee.x / view.frame.width) | |
| pageControl.currentPage = pageNumber | |
| } | |
| } |
Usando a factory criada anteriormente, instanciamos os conteúdos das três páginas de onboarding e colocamos na constante pages. Ela é a fonte de dados que alimenta a Collection View. Criamos também um UIPageControl e adicionamos programaticamente na view, definindo as constraints no método ddPageControlToView().
O tamanho de cada célula da Collection View é definido no método collectionView(_:layout:sizeForItemAt:). O tamanho será igual ao tamanho da Collection View, ou seja, a tela inteira.
O UIPageControl é atualizado no método scrollViewWillEndDragging(_:withVelocity:targetContentOffset:).
UICollectionViewCell
A OnboardingCollectionViewCell tem referência para os componentes de cada célula, e um método responsável por preencher essas referências com os dados:

Definimos também uma constante estática para o reuse identifier da célula (o mesmo definido no storyboard). Vamos usar essa constante no view controller, para referenciar a célula que será reaproveitada. A classe final ficará assim:
| import UIKit | |
| class OnboardingCollectionViewCell: UICollectionViewCell { | |
| static let reuseIdentifier = “OnboardingCell“ | |
| @IBOutlet weak var pageImage: UIImageView! | |
| @IBOutlet weak var pageTitle: UILabel! | |
| @IBOutlet weak var pageDescription: UILabel! | |
| @IBOutlet weak var startButton: UIButton! | |
| func fill(with data: OnboardingModel) { | |
| // Se quiser imagem no background, descomentar as linhas abaixo | |
| // e criar atributo backgroundImage no OnboardingModel | |
| // let backgroundImage = UIImageView(image: UIImage(named: data.backgroundImage)) | |
| // backgroundImage.contentMode = .scaleAspectFill | |
| // backgroundView = backgroundImage | |
| backgroundColor = data.backgroundColor | |
| pageImage.image = UIImage(named: data.iconImage) | |
| pageTitle.text = data.title | |
| pageDescription.text = data.content | |
| startButton.alpha = data.hideButton ? 0 : 1 | |
| } | |
| } |
Perceba também que as primeiras instruções do método fill definem a cor de background da célula com base no OnboardingModel recebido.
Resources
O último passo é definir os resources usados em cada página. Basta editar o arquivo Assets.xcassets, dar um nome para cada recurso e arrastar as imagens para os respectivos slots:

Obs.: em um projeto real você vai querer definir as imagens para as resoluções 1x, 2x e 3x, para que a correta seja utilizada conforme a densidade de pixels do aparelho. Para facilitar este exemplo, estou usando uma imagem única para as três densidades.
Alguns detalhes
Fora o que está descrito aqui, melhorei alguns outros detalhes como o espaçamento entre a imagem e os textos, alguns detalhes do botão, e a organização dos arquivos do projeto. A ideia é que ele leve à view inicial do app, ou para o login. Recomendo baixar o projeto do GitHub para ver como ficou o resultado final.
