1

I sent my data from my API call to my InfoController viewDidLoad. There, I was able to safely store it in a skillName constant, and also printed it, receiving all the information by console.

The problem comes when I try to assign this variable to my skillLabel.

override func viewDidLoad() {
    super.viewDidLoad()
    configureViewComponents()
    fetchPokemons { (names) in
        guard var skillName = names as? String else { return }
        self.pokemon?.skillName = skillName
        
        self.allNames = skillName
        print(self.allNames)
    }
}

There, when I print allNames, the console shows all the data I need. This is how the data looks like: Data Example

And the computed property where I wanna use this data looks is:

var pokemon: Pokemon? {
    didSet {
        guard let id        = pokemon?.id else { return }
        guard let data      = pokemon?.image else { return }

        
        navigationItem.title = pokemon?.name?.capitalized
        infoLabel.text = pokemon?.description
        infoView.pokemon = pokemon
        
        if id == pokemon?.id {
            imageView.image = UIImage(data: data)
            infoView.configureLabel(label: infoView.skillLabel, title: "Skills", details: "\(allNames)")
        }
    }
}

PD: allNames is a String variable I have at InfoController class-level.

This is how my app looks when run: PokeApp

My goal is to get that details param to show the skillName data, but it returns nil, idk why. Any advice?

EDIT1: My func that fetches the Pokemon data from my service class is this one:

func fetchPokemons(handler: @escaping (String) -> Void) {
    controller.service.fetchPokes { (poke) in
        DispatchQueue.main.async {
            self.pokemon? = poke
            
            guard let skills = poke.abilities else { return }
            
            for skill in skills {
                
                guard let ability = skill.ability else { return }
                
                guard var names = ability.name!.capitalized as? String else { return }
                
                self.pokemon?.skillName = names
                handler(names)
            }
        }
    }
}

EDIT2: InfoView class looks like:

class InfoView: UIView {

// MARK: - Properties
var delegate: InfoViewDelegate?

//  This whole block assigns the attributes that will be shown at the InfoView pop-up
//  It makes the positioning of every element possible
var pokemon: Pokemon? {
    didSet {
        guard let pokemon   = self.pokemon else { return }
        guard let type      = pokemon.type else { return }
        guard let defense   = pokemon.defense else { return }
        guard let attack    = pokemon.attack else { return }
        guard let id        = pokemon.id else { return }
        guard let height    = pokemon.height else { return }
        guard let weight    = pokemon.weight else { return }
        guard let data      = pokemon.image else { return }
        
        if id == pokemon.id {
            imageView.image = UIImage(data: data)
        }
        nameLabel.text = pokemon.name?.capitalized
        
        configureLabel(label: typeLabel, title: "Type", details: type)
        configureLabel(label: pokedexIdLabel, title: "Pokedex Id", details: "\(id)")
        configureLabel(label: heightLabel, title: "Height", details: "\(height)")
        configureLabel(label: defenseLabel, title: "Defense", details: "\(defense)")
        configureLabel(label: weightLabel, title: "Weight", details: "\(weight)")
        configureLabel(label: attackLabel, title: "Base Attack", details: "\(attack)")
    }
}

let skillLabel: UILabel = {
    let label = UILabel()
    return label
}()

let imageView: UIImageView = {
    let iv = UIImageView()
    iv.contentMode = .scaleAspectFill
    return iv
}()
. . .
}

infoView.configureLabel is this:

func configureLabel(label: UILabel, title: String, details: String) {
    let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: "\(title):  ", attributes: [NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: Colors.softRed!]))
    attributedText.append(NSAttributedString(string: "\(details)", attributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.gray]))
    label.attributedText = attributedText
}

EDIT 3: Structures design

struct Pokemon: Codable {
    var results: [Species]?
    var abilities: [Ability]?
    var id, attack, defense: Int?
    var name, type: String?
...
}

struct Ability: Codable {
    let ability: Species?
}

struct Species: Codable {
    let name: String?
    let url: String?
}
3rnestocs
  • 23
  • 7

1 Answers1

1

Jump to the Edit2 paragraph for the final answer!

Initial Answer:

I looks like you UI does not get updated after the controller fetches all the data.

Since all of you UI configuration code is inside the var pokemon / didSet, it's a good idea to extract it to a separate method.

private func updateView(with pokemon: Pokemon?, details: String?) {
    guard let id = pokemon?.id, let data = pokemon?.image else { return }

    navigationItem.title = pokemon?.name?.capitalized
    infoLabel.text = pokemon?.description
    infoView.pokemon = pokemon

    if id == pokemon?.id {
        imageView.image = UIImage(data: data)
        infoView.configureLabel(label: infoView.skillLabel, title: "Skills", details: details ?? "")
    }
}

and now you can easily call in the the didSet

var pokemon: Pokemon? {
    didSet { updateView(with: pokemon, details: allNames) }
}

and fetchPokemons completion aswell

override func viewDidLoad() {
    super.viewDidLoad()
    configureViewComponents()
    fetchPokemons { (names) in
        guard var skillName = names as? String else { return }
        self.pokemon?.skillName = skillName

        self.allNames = skillName
        print(self.allNames)
        DispatchQueue.main.async {
            self.updateView(with: self.pokemon, details: self.allNames)
        }
    }
}

It's super important to do any UI setup on the main queue.

Edit:

The fetch function may be causing the problems! you are calling handler multiple times:

func fetchPokemons(handler: @escaping (String) -> Void) {
    controller.service.fetchPokes { (poke) in
        DispatchQueue.main.async {
            self.pokemon? = poke
            guard let skills = poke.abilities else { return }
            let names = skills.compactMap { $0.ability?.name?.capitalized }.joined(separator: ", ")
            handler(names)
        }
    }
}

Edit2:

After looking at your codebase there are a couple of things you need to change:

1. fetchPokemons implementation

the handler of controller.service.fetchPokes gets called for every pokemon so we need to check if the fetched one is the current (self.pokemon) and then call the handler with properly formated skills.

func fetchPokemons(handler: @escaping (String) -> Void) {
    controller.service.fetchPokes { (poke) in
        guard poke.id == self.pokemon?.id else { return }
        self.pokemon? = poke
        let names = poke.abilities?.compactMap { $0.ability?.name?.capitalized }.joined(separator: ", ")
        handler(names ?? "-")
    }
}

2. update viewDidLoad()

now simply pass the names value to the label.

override func viewDidLoad() {
    super.viewDidLoad()
    configureViewComponents()
    fetchPokemons { (names) in
        self.pokemon?.skillName = names
        self.infoView.configureLabel(label: self.infoView.skillLabel, title: "Skills", details: names)
    }
}

3. Refactor var pokemon: Pokemon? didSet observer

var pokemon: Pokemon? {
    didSet {
        guard let pokemon = pokemon, let data = pokemon.image else { return }
        navigationItem.title = pokemon.name?.capitalized
        infoLabel.text = pokemon.description!
        infoView.pokemon = pokemon
        imageView.image = UIImage(data: data)
    }
}
Witek Bobrowski
  • 3,749
  • 1
  • 20
  • 34
  • and if you pass `self.pokemon?.skillName` instead of `self.allNames`? – Witek Bobrowski Nov 11 '20 at 21:10
  • same result, and also my CollectionView is loading slower than before the refactoring – 3rnestocs Nov 11 '20 at 21:19
  • Could you include some more code in your question? More context would be great. One thing to check is if the `infoView.skillLabel` is the same that is currently displayed. You mention a collectionView, maybe try to call `reloadData()` once fetching finishes? – Witek Bobrowski Nov 11 '20 at 21:23
  • well, my app works this way: A CollectionViewController that shows an image and a name for every item. If u click one item, it pushes to my InfoViewController, where I actually wanna show the Skills. I think the `infoView.skillLabel` is definitely the same that is currently displayed, otherwise it wouldn't show the empty "" instead of the value I want – 3rnestocs Nov 11 '20 at 21:29
  • ok so the `infoView.skillLabel ` itself is not in some kind of UICollectionViewCell but rather its a static label right? I would love to see the implementation of the `infoView.configureLabel` and maybe a blueprint of the `infoView` – Witek Bobrowski Nov 11 '20 at 21:34
  • There you have! The . . . at InfoView is because I declare all the labels and stuff that display on my InfoController, and I didn't think it was worth to show it. – 3rnestocs Nov 11 '20 at 21:44
  • you are calling `handler(names)` multiple times, for every skill in the array. so this way the `allNames` will always have 1 skill name at max! – Witek Bobrowski Nov 11 '20 at 21:44
  • if you want to concatenate all of the skill names try this instead of the for loop: `let names = skills.compactMap { $0.ability?.name?.capitalized }.joined(separator: ", ")` – Witek Bobrowski Nov 11 '20 at 21:47
  • checkout my updated answer with new fetch implementation – Witek Bobrowski Nov 11 '20 at 21:50
  • Every time you update UI from an async call, wrap it in `DispatchQueue.main.async`. You might have missed this point from this anser. – Son Nguyen Nov 12 '20 at 06:14
  • I think the issue now is on this line `skills.compactMap { $0.ability?.name?.capitalized }.joined(separator: ", ")` – 3rnestocs Nov 12 '20 at 12:33
  • The thing is that my ability?.name isn't an array, is just a String. That's why I had the for loop, because I need to show the corresponding string for all the pokemons of my CollectionView – 3rnestocs Nov 12 '20 at 12:34
  • but isn't the skills an array of the string values? how do you want to display them? separated by commas? if you now print the `names` value what do you get? – Witek Bobrowski Nov 12 '20 at 12:41
  • I get a string for every pokemon I have, separated by commas. Check it out: https://i.gyazo.com/d9fb8a482b14a022919a2ec1ea66d719.png – 3rnestocs Nov 12 '20 at 13:53
  • and what is the type of the `poke.abilities`? – Witek Bobrowski Nov 12 '20 at 14:05
  • [Ability], and that one has an ability property of type [Species], and that's the struct that holds every name and url var for my fetching code. I'll edit my post with those. – 3rnestocs Nov 12 '20 at 14:10
  • would you want to email me the xcode project? it would make stuff much easier that trying to figure it out from these small sample of code. tap on my profile and get my email from my website of github. – Witek Bobrowski Nov 12 '20 at 14:15
  • @3rnestocs I have updated my answer with working solution – Witek Bobrowski Nov 12 '20 at 15:24