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"
                    }
                }
            }
        }
    }