PokeFind
Whatever your age is, don’t tell me you don’t know what POKEMON is. Based on a research, more than 8 in 10 americans know what Pokemon is. To many, it is nostalgic, and it is still popular generation after generation.
A long friend / mentor of mine have requested a challenge to me to finish the app within 5 days and he have selected a Pokemon API to show list of Pokemons and when you click on one of them, it shows list of abilities and moves that a specific Pokemon can perform.
I went a little overboard with the layout and the UI design because I just love em and gotta capture ‘em all 😎. It was a little bit frustrating because JSON file was tricky due to nesting and continuous network calling within the nesting, but I got to practice more of them and it was very rewarding after the app was finished.
Search List.
func configureSearchController() {
let searchController = UISearchController()
searchController.searchResultsUpdater = self
searchController.searchBar.placeholder = "Search for Pokemon..."
searchController.searchBar.tintColor = .black
searchController.obscuresBackgroundDuringPresentation = false
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
}
func displayPokemons() {
showLoadingView()
NetworkManager.shared.getPokemons() { [weak self] result in
guard let self = self else { return }
switch result {
case.success(let pokemons):
for result in pokemons.results {
let pokemonInfoURL = result.url
NetworkManager.shared.getPokeInfo(for: pokemonInfoURL) { pokeInfoResult in
switch pokeInfoResult {
case .success(let pokeinfo):
self.updateUI(with: [pokeinfo])
case .failure(let error):
self.presentAPAlertOnMainThread(title: "Bad Stuff Happened", message: error.rawValue, buttonTitle: "OK")
}
}
}
case .failure(let error):
self.presentAPAlertOnMainThread(title: "Bad Stuff Happened", message: error.rawValue, buttonTitle: "OK")
}
}
}
func updateUI(with pokemons: [PokeInfo]) {
self.pokemons.append(contentsOf: pokemons)
self.updateData(on: self.pokemons)
}
func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, pokemon in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PokemonCell.reuseID, for: indexPath) as! PokemonCell
cell.set(pokemonThumbnail: pokemon)
return cell
})
self.dismissLoadingView()
}
func updateData(on pokemons: [PokeInfo]) {
var snapshot = NSDiffableDataSourceSnapshot()
snapshot.appendSections([.main])
snapshot.appendItems(pokemons)
DispatchQueue.main.async { self.dataSource.apply(snapshot, animatingDifferences: true) }
}
extension PokemonListVC: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let activeArray = isSearching ? filteredPokemons : pokemons
let selectedPokemon = activeArray[indexPath.item]
let destVC = PokemonDetailVC(pokemonURL: "https://pokeapi.co/api/v2/pokemon/\(selectedPokemon.species.name)")
navigationController?.pushViewController(destVC, animated: true)
}
}
extension PokemonListVC: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let filter = searchController.searchBar.text, !filter.isEmpty else {
filteredPokemons.removeAll()
updateData(on: pokemons)
isSearching = false
return
}
isSearching = true
filteredPokemons = pokemons.filter { $0.species.name.lowercased().contains(filter.lowercased())}
updateData(on: filteredPokemons)
}
}
Main Image.
let mainImageViewContainer = UIView()
let mainImageView = APListImageView(frame: .zero)
let nameLabel = APTitleLabel(textAlignment: .center, fontSize: 40)
let typeLabel = APBodyLabel(textAlignment: .center, fontSize: 25)
let pbLogoBig = UIImageView()
func updateUI() {
guard let pokemonInfo = pokemonInfo else { return }
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.nameLabel.text = pokemonInfo.species.name.capitalized
let types = pokemonInfo.types.map { $0.type.name.capitalized }.joined(separator: " / ")
if let firstType = pokemonInfo.types.first?.type.name.capitalized {
switch firstType {
case "Grass":
mainImageViewContainer.backgroundColor = Colors.grass
case "Fire":
mainImageViewContainer.backgroundColor = Colors.fire
case "Water":
mainImageViewContainer.backgroundColor = Colors.water
case "Electric":
mainImageViewContainer.backgroundColor = Colors.electric
case "Psychic":
mainImageViewContainer.backgroundColor = Colors.psychic
case "Fighting":
mainImageViewContainer.backgroundColor = Colors.fighting
case "Steel":
mainImageViewContainer.backgroundColor = Colors.steel
case "Ice":
mainImageViewContainer.backgroundColor = Colors.ice
case "Poison":
mainImageViewContainer.backgroundColor = Colors.poison
case "Normal":
mainImageViewContainer.backgroundColor = Colors.normal
case "Ground":
mainImageViewContainer.backgroundColor = Colors.ground
case "Flying":
mainImageViewContainer.backgroundColor = Colors.flying
case "Bug":
mainImageViewContainer.backgroundColor = Colors.bug
case "Rock":
mainImageViewContainer.backgroundColor = Colors.rock
case "Ghost":
mainImageViewContainer.backgroundColor = Colors.ghost
case "Dragon":
mainImageViewContainer.backgroundColor = Colors.dragon
case "Dark":
mainImageViewContainer.backgroundColor = Colors.dark
case "Fairy":
mainImageViewContainer.backgroundColor = Colors.fairy
default:
mainImageViewContainer.backgroundColor = .black
}
}
self.typeLabel.text = types.isEmpty ? "No types found" : types
if let imageUrl = pokemonInfo.sprites.other?.officialArtwork.frontDefault {
self.mainImageView.downloadImage(fromURL: imageUrl)
} else {
self.mainImageView.image = UIImage(named: "placeholderImage") // Use a placeholder image
}
self.abilityTitleLabel.text = "Abilities"
self.movesTitleLabel.text = "Moves learnt by growth"
self.scrollView.contentSize = CGSize(width: self.view.frame.width, height: self.contentView.frame.height)
}
}
Abilities Content.
func updateAbilityAndDescriptions() {
guard let pokemonInfo = pokemonInfo else { return }
DispatchQueue.main.async {
self.abilityStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
}
let abilityURLs = pokemonInfo.abilities.map { $0.ability.url }
for (index, ability) in pokemonInfo.abilities.enumerated() {
let abilityNameLabel = APGeneralTitleLabel(textAlignment: .left, fontSize: 18)
abilityNameLabel.text = ability.ability.name.capitalized
let abilityDescriptionLabel = APGeneralDescriptionLabel(textAlignment: .left, fontSize: 13)
abilityDescriptionLabel.text = "Loading description..."
let abilityPairContainer = UIView()
abilityPairContainer.addSubviews(abilityNameLabel, abilityDescriptionLabel)
abilityNameLabel.translatesAutoresizingMaskIntoConstraints = false
abilityDescriptionLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
abilityNameLabel.topAnchor.constraint(equalTo: abilityPairContainer.topAnchor),
abilityNameLabel.leadingAnchor.constraint(equalTo: abilityPairContainer.leadingAnchor),
abilityNameLabel.trailingAnchor.constraint(equalTo: abilityPairContainer.trailingAnchor),
abilityDescriptionLabel.topAnchor.constraint(equalTo: abilityNameLabel.bottomAnchor, constant: 5),
abilityDescriptionLabel.leadingAnchor.constraint(equalTo: abilityPairContainer.leadingAnchor),
abilityDescriptionLabel.trailingAnchor.constraint(equalTo: abilityPairContainer.trailingAnchor),
abilityDescriptionLabel.bottomAnchor.constraint(equalTo: abilityPairContainer.bottomAnchor)
])
DispatchQueue.main.async {
self.abilityStackView.addArrangedSubview(abilityPairContainer)
}
NetworkManager.shared.getAbilityDescription(for: abilityURLs[index]) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let abilityDesc):
DispatchQueue.main.async {
abilityDescriptionLabel.text = abilityDesc.englishEffectEntry?.effect ?? "Description not available"
}
case .failure:
DispatchQueue.main.async {
abilityDescriptionLabel.text = "Description not available"
}
}
}
}
}
Moves and Descriptions.
func updateMovesAndDescriptions() {
guard let pokemonInfo = pokemonInfo else { return }
DispatchQueue.main.async {
self.moveStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
}
var uniqueMoves: Set = []
var moveDetails: [(name: String, level: Int, url: String)] = []
for move in pokemonInfo.moves {
for detail in move.versionGroupDetails {
if detail.moveLearnMethod.name == "level-up" || detail.moveLearnMethod.name == "egg" {
let moveName = move.move.name.capitalized
if !uniqueMoves.contains(moveName) {
uniqueMoves.insert(moveName)
moveDetails.append((name: moveName, level: detail.levelLearnedAt, url: move.move.url))
}
}
}
}
moveDetails.sort { $0.level < $1.level }
for moveDetail in moveDetails {
let moveNameLabel = APGeneralTitleLabel(textAlignment: .left, fontSize: 18)
moveNameLabel.text = "\(moveDetail.name) (lvl: \(moveDetail.level))"
let moveDescriptionLabel = APGeneralDescriptionLabel(textAlignment: .left, fontSize: 13)
moveDescriptionLabel.text = "Loading description..."
let movePairContainer = UIView()
movePairContainer.addSubviews(moveNameLabel, moveDescriptionLabel)
moveNameLabel.translatesAutoresizingMaskIntoConstraints = false
moveDescriptionLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
moveNameLabel.topAnchor.constraint(equalTo: movePairContainer.topAnchor),
moveNameLabel.leadingAnchor.constraint(equalTo: movePairContainer.leadingAnchor),
moveNameLabel.trailingAnchor.constraint(equalTo: movePairContainer.trailingAnchor),
moveDescriptionLabel.topAnchor.constraint(equalTo: moveNameLabel.bottomAnchor, constant: 5), // Padding between labels
moveDescriptionLabel.leadingAnchor.constraint(equalTo: movePairContainer.leadingAnchor),
moveDescriptionLabel.trailingAnchor.constraint(equalTo: movePairContainer.trailingAnchor),
moveDescriptionLabel.bottomAnchor.constraint(equalTo: movePairContainer.bottomAnchor)
])
DispatchQueue.main.async {
self.moveStackView.addArrangedSubview(movePairContainer)
}
NetworkManager.shared.getMoveDescription(for: moveDetail.url) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let moveDesc):
DispatchQueue.main.async {
moveDescriptionLabel.text = moveDesc.englishEffectEntry?.effect ?? "Description not available"
}
case .failure:
DispatchQueue.main.async {
moveDescriptionLabel.text = "Description not available"
}
}
}
}
}