Breeds
A Swift UIKit View Code tutorial
Install / Use
/learn @ericklborges/BreedsREADME
Breeds
Este é um aplicativo feito para o estudo de diferentes abordagens de desenvolvimento da camada de View. São duas muito comuns, uma que lista raças de cachorros em uma collection, e a outra é uma tela de detalhe que aparece quando você seleciona uma das raças.
Configurações do projeto
- Gerar API Key em thedogapi.com
- Instalar o Bundler:
$ sudo gem install bundler
💎 O Bundler é um gerenciador de gemas (aplicações em ruby), e neste caso estamos instalando ele para usar a ferramenta cocoapods-keys, como pode ser visto no arquivo Gemfile na raiz do projeto. Esta ferramenta nos ajuda a evitar que nossas chaves privadas subam para o repositório.
- Clonar o repositório
- Instalar Gemas
$ Bundler install - Instalar dependências do projeto
$ Bundle exec pod install - Quando solicitado, entrar com API Key solicitada no terminal
Refatorando Storyboard para View Coding
O passo a passo a seguir detalha a refatoração da camada de de View deste projeto, de Storyboard + Xibs para View Coding usando UIKit.
Você pode dar um checkout para a branch viewCode/Storyboard, ou para a tag live-code-start para navegar até o momento inicial deste tutorial.
🏷 As tags marcadas ao longo do passo a passo te levam para o ponto do desenvolvimento onde estão. Navegue até a pasta do projeto, e digite o comando $ git checkout <tag-name> no terminal para usar uma tag. (e.g. o comando $ git checkout live-code-start te leva ao commit inicial do passo a passo a seguir)
Passo a passo
Inicialização
<details> <summary> 1. Trocar main interface para <b>LaunchScreen</b> nos general settings do projeto </summary> <p> <img src="Readme Images/Main interface.png"> </p> </details> <details> <summary> 2. Instanciar <b>UINavigationController</b> passando a <b>BreedCollectionViewController</b> com o root, e setar ela como <b>window?.rootViewController</b> no <b>SceneDelegate</b> </summary> <p>func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: windowScene)
let controller = BreedsCollectionViewController()
let navigation = UINavigationController(rootViewController: controller)
window?.rootViewController = navigation
window?.backgroundColor = BackgroundColor.main
window?.makeKeyAndVisible()
}
</p>
</details>
Refatorando BreedCollectionViewCell
<details> <summary> 3. Substituir <b>outlets</b> por views criadas programaticamente na <b>BreedCollectionViewCell</b> </summary> <p>// MARK: Views
let imageView: UIImageView = {
let image = UIImageView(frame: .zero)
image.clipsToBounds = true
image.contentMode = .scaleAspectFill
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
let overlayView: UIView = {
let view = UIImageView(frame: .zero)
view.backgroundColor = BackgroundColor.overlay
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let nameLabel: UILabel = {
let label = UILabel(frame: .zero)
label.numberOfLines = 1
label.textColor = TextColor.primary
label.font = UIFont.systemFont(ofSize: 13, weight: .bold)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
</p>
</details>
<details>
<summary>
4. Criar <b>extension</b> adicionando <b>views</b> e configurando <b>constraints</b>
</summary>
<p>
// MARK: Autolayout
extension BreedCollectionViewCell {
func setupViewHierarchy() {
addSubview(imageView)
addSubview(overlayView)
overlayView.addSubview(nameLabel)
}
func setupConstraints() {
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
overlayView.heightAnchor.constraint(equalToConstant: 24),
overlayView.bottomAnchor.constraint(equalTo: bottomAnchor),
overlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
overlayView.trailingAnchor.constraint(equalTo: trailingAnchor),
nameLabel.topAnchor.constraint(equalTo: overlayView.topAnchor),
nameLabel.bottomAnchor.constraint(equalTo: overlayView.bottomAnchor),
nameLabel.leadingAnchor.constraint(equalTo: overlayView.leadingAnchor, constant: 16),
nameLabel.trailingAnchor.constraint(equalTo: overlayView.trailingAnchor, constant: -16)
])
}
}
</p>
</details>
<details>
<summary>
5. Criar <b>init</b> chamando os métodos na ordem correta
</summary>
<p>
// MARK: Life Cycle
override init(frame: CGRect) {
super.init(frame: .zero)
setupViewHierarchy()
setupConstraints()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
</p>
</details>
<details>
<summary>
6. Criar protocolo <b>ViewCodable</b>, e atualiza init com método <b>setupViews()</b>
</summary>
<p>
protocol ViewCodable {
func setupViews()
func setupViewHierarchy()
func setupConstraints()
func setupAditionalConfiguration()
}
extension ViewCodable {
func setupViews() {
setupViewHierarchy()
setupConstraints()
setupAditionalConfiguration()
}
func setupAditionalConfiguration() { }
}
// MARK: Life Cycle
override init(frame: CGRect = .zero) {
super.init(frame: frame)
setupViews()
}
</p>
</details>
Testando BreedCollectionViewCell
</p> </details> <details> <summary> 7. Escrever teste para visualizar interface da classe <b>BreedCollectionViewCellSpec</b> </summary> <p>import Quick
import Nimble
import Nimble_Snapshots
@testable import Breeds
class BreedCollectionViewCellSpec: QuickSpec {
override func spec() {
describe("BreedCollectionViewCell") {
var sut: BreedCollectionViewCell!
context("when initialized") {
beforeEach {
sut = BreedCollectionViewCell()
sut.setup(image: .stub(url: AssetHelper.LocalImage.carameloDog.url))
sut.frame.size = CGSize(width: 200, height: 200)
}
it("should layout itself properly") {
// expect(sut).toEventually(haveValidSnapshot(named: "BreedCollectionViewCell_Layout"), timeout: 0.5)
expect(sut).toEventually(recordSnapshot(named: "BreedCollectionViewCell_Layout", identifier: nil, usesDrawRect: false), timeout: 0.5)
}
}
}
}
}
</p>
</details>
<details>
<summary>
8. Referência da pasta <b>ReferenceImages</b> no <b>target</b> de <b>BreedsTests</b> (opcional)
</summary>
<p>
Atenção às configurações Copy items if needed, e Create folder references
<img src="Readme Images/Reference Images.png"> </p> </details>🏷️live-code-goal
Refatorando BreedsCollectionViewController
<details> <summary> 9. Atualizar o método <b>setupNavigation()</b> da <b>BreedsCollectionViewController</b> setando o title </summary> <p>func setupNavigation() {
title = "Breeds"
navigationController?.applyCustomAppearence()
navigationController?.navigationBar.prefersLargeTitles = true
}
</p>
</details>
<details>
<summary>
10. Criar <b>FlowLayout</b> e <b>Collection</b> programaticamente
</summary>
<p>
// MARK: Views
let collectionFlowLayout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = .zero
layout.minimumInteritemSpacing = .zero
return layout
}()
lazy var collectionView: UICollectionView = {
let collection = UICollectionView(frame: .zero, collectionViewLayout: collectionFlowLayout)
collection.delegate = self
collection.dataSource = self
collection.prefetchDataSource = self
collection.backgroundColor = BackgroundColor.main
collection.register(BreedCollectionViewCell.self, forCellWithReuseIdentifier: Identifier.Cell.breedCell)
collection.translatesAutoresizingMaskIntoConstraints = false
return collection
}()
</p>
</details>
<details>
<summary>
11. Criar <b>extension</b> implementando o protocolo <b>ViewCodable</b>
</summary>
<p>
// MARK: Autolayout
extension BreedsCollectionViewController: ViewCodable {
func setupViewHierarchy() {
view.addSubview(collectionView)
}
func setupConstraints() {
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
</p>
</details>
<details>
<summary>
12. Chamar <b>setupViews()</b> no <b>viewDidLoad()</b>
</summary>
<p>
// MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
setupNavigation()
viewModel.fetchImages()
}
</p>
</details>
🏷️live-code-extra-collection
Refatorando BreedDetailView
<details> <summary> 13. Criar classe para componente <b>BreedDetailLabel</b> </summary> <p>class BreedDetailLabel: UILabel {
// MARK: Init
init() {
super.init(frame: .zero)
self.numberOfLines = 0
self.translatesAutoresizingMaskIntoConstraints = false
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("i
Related Skills
node-connect
353.3kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
111.7kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
353.3kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
353.3kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
