[SWIFT] TableView, SearchBar, 2 recherches différentes

Holà ^^

J'ai un problème un peu tordus et je vais essayé de vous expliquer ça simplement..

J'ai une interface de recherche :

Lorsque j'arrive dessus, je peux commencer à écrire une adresse, l'autocompletion m'aide si j'en ai besoin.. pas de soucis, ça fonctionne.

J'aimerai rajouter un autre mode de recherche dans une liste qui existe déjà..
Cette liste s'afficherai lorsque je clic sur l'icon "favoris" (après la barre de recherche)

En gros, quand je clic sur l'icon "favoris", ma liste actuelle se vide et se remplis par mes favoris (une liste en JSON) et j'aimerai garder le fonctionnement de la barre de recherche tant qu'a faire :D

Puis si je veux repasser en mode "recherche d'adresse", je reclic sur le bouton "favoris" qui me vide ma liste des favoris et remet le fonctionnement normal.

J'ai aucune idée de comment faire ça..

Est ce que je peux utiliser la même tableView pour faire ça ou est ce que je dois avoir 2 tablesView, en cachant celle qui ne me sert pas ?
Si je met 2 tablesView, est ce qu'une des deux (celle des favoris) ne va pas se mélanger les pinceaux et interagir avec les extensions alors qu'elle ne devrait pas ?

Voici mon code pour l'instant :

Je récupère une adresse (String) via la vue précédente pour remplir ma SearchBar..
Et stock les coordonnées et l'adresse à l'appuie sur le bouton Valider pour m'en re-servir sur l'autre vue.

class ChoixAdresseController: UIViewController {

@IBOutlet weak var searchbarAdresse: UISearchBar!
@IBOutlet weak var tableViewResultatAdresse: UITableView!
@IBOutlet weak var boutonValider: UIBarButtonItem!
@IBOutlet weak var boutonFavoris: UIButton!

var adresse : String = ""
var coordonnees : String = ""
var searchCompleter = MKLocalSearchCompleter()
var searchResults = [MKLocalSearchCompletion]()

override func viewDidLoad() {
    super.viewDidLoad()

    navigationController?.navigationBar.barTintColor = couleurBandeau
    navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: couleurTexteBandeau]
    searchbarAdresse.barTintColor = UIColor(hex: "#484D54")

    if(adresse != ""){
        searchbarAdresse.text = adresse
    }

    searchCompleter.delegate = self
}

@IBAction func ationBoutonRetour(_ sender: Any) {
    dismiss(animated: true, completion: nil) // On ferme la fenetre
}

@IBAction func actionBoutonValider(_ sender: Any) {
    if let nouvelleAdresse = searchbarAdresse.text {
        if(nouvelleAdresse != ""){
            // stock les données
            DB.store["nouvelleAdresse"] = nouvelleAdresse
            DB.store["nouvelleAdresse_coordonnees"] = coordonnees
            dismiss(animated: true, completion: nil) // On ferme la fenetre
        }
    }
}

@IBAction func actionBoutonFavoris(_ sender: Any) {

    let imageFavoris = UIImage(named: "icon_favoris")

    // Changement de bouton quand on clic dessus
    if let imageBouton = boutonFavoris.imageView?.image {
        if(imageBouton == imageFavoris){
            boutonFavoris.setImage(UIImage(named: "icon_pin_favoris"), for: .normal)
        }
        else{
            boutonFavoris.setImage(UIImage(named: "icon_favoris"), for: .normal)
        }
    }

}

}

extension ChoixAdresseController: UISearchBarDelegate {

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    searchCompleter.queryFragment = searchText
}

}

extension ChoixAdresseController: MKLocalSearchCompleterDelegate {

func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
    searchResults = completer.results
    tableViewResultatAdresse.reloadData()
}

func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
    // handle error
}
}

extension ChoixAdresseController: UITableViewDataSource {

func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return searchResults.count
}

// pour chaque cellule
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let searchResult = searchResults[indexPath.row]
    let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
    cell.textLabel?.text = searchResult.title
    cell.detailTextLabel?.text = searchResult.subtitle
    cell.imageView?.image = UIImage(named: "icon_building")
    return cell
}
}

extension ChoixAdresseController: UITableViewDelegate {

// Quand on clic sur une ligne
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)

    let completion = searchResults[indexPath.row]

    let searchRequest = MKLocalSearch.Request(completion: completion)
    let search = MKLocalSearch(request: searchRequest)
    search.start { (response, error) in
        if let coordinate = response?.mapItems[0].placemark.coordinate {
            self.searchbarAdresse.text = response?.mapItems[0].placemark.title
            self.coordonnees = String(stringInterpolationSegment: coordinate.latitude)+","+String(stringInterpolationSegment: coordinate.longitude)
            self.actionBoutonValider("")
        }
    }
}
}

Comment faire pour gérer 2 formats différents (un avec le code actuelle et l'autre avec les favoris en JSON) dans une seule tableView et garder le fonctionnement de recherche ?

Réponses

  • • Une seule UITableView.
    • - Soit un seul type d'objet pour le modèle de la TableView, qui s'initie soit avec un MKLocalSearchCompleter.resultClass, soit avec un dictionnaire de tes favoris.
    - Soit, si tu as déjà un vrai objet dans tes favoris, une vraie classe custom, tu peux utiliser un protocol, mais je te conseille d'utiliser un objet model qui sera un wrapper, car tu sembles encore débutant, et c'est plus simple à comprendre.
    • Enfin, tu as un array d'un seul type d'objets du coup, et quand tu changes ce que tu souhaites afficher, tu changes juste l'array qui remplit ta tableView.

  • Je crois comprendre la théorie mais je ne vois pas du tout comment mettre ça en pratique..

    J'aime pas demander ça mais aurais-tu un bout de code pour me débloquer ?

  • Grosso Modo:

    struct AddressTableViewModel {
        let title: String
        let subtitle: String
    }
    
    let source: [AddressTableViewModel]
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return source.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let model = source[indexPath.row]
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil) //Au fait, il ne devrait pas y avoir de nil ici
        cell.textLabel?.text = model.title
        cell.detailTextLabel?.text = model.subtitle
        cell.imageView?.image = UIImage(named: "icon_building")
        return cell
    }
    
    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        source = completer.results.map{ AddressTableViewModel(title: $0.title, subtitle: $0.subtitle) } //Ou faire à la main un for loop, peu importe, ce qui te semble le plus faisable à ton niveau, y'a un exemple sur les favoris
        tableViewResultatAdresse.reloadData()
    }
    
    

    Je suppose que là, tu changes montre tes favoris ou non :

    @IBAction func actionBoutonFavoris(_ sender: Any) {
        if showFavorites {
            let arrayOfFavorites = // À retrouver je ne sais comment
            var tempModels = [AddressTableViewModel]()
            for aFavoriDict in arrayOfFavorites {
                let aModel = AddressTableViewModel(title: aFavoriDict["title"], subtitle: aFavoriDict["subtitle"]) 
                tempModels.append(aModel)
            }
            source = tempModels
        } else {
           source = ??
        }
        tableViewResultatAdresse.reloadData()
    }
    

    Il y a sûrement des problèmes de unwrap et autres typos, et je n'ai aucune idée de à quoi ressemble ton array de favoris, mais voilà l'idée.

  • Joanna CarterJoanna Carter Membre, Modérateur

    Moi, j'ai un petit enum :

    enum DataFilterType<filterT : Equatable>
    {
      case all
      case filtered(filterT)
    }
    
    
    extension DataFilterType : Equatable
    {
      static func ==(lhs: DataFilterType, rhs: DataFilterType) -> Bool
      {
        switch (lhs, rhs)
        {
          case (.all, .all):
            return true
          case (.filtered(let lFilter), .filtered(let rFilter)):
            return lFilter == rFilter
          default:
            return false
        }
      }
    }
    

    Et, avec ça, je peut déterminer quelles données à récupérer do mon DataProvider

    public final class EventDataProvider : NSObject
    {
      var dataFilterType: DataFilterType<String> = .all
      {
        didSet
        {
          if dataFilterType != oldValue
          {
            _fetchedResultsController = nil // reset lazy var
          }
        }
      }
    
      var locationFilterType: DataFilterType<CDLocation> = .all
    
      public var events: [CDEvent]
      {
        guard let fetchedObjects = self.fetchedResultsController?.fetchedObjects else
        {
          return[]
        }
    
        return fetchedObjects
      }
    
      public subscript(indexPath: IndexPath) -> CDEvent?
      {
        return self.fetchedResultsController?.object(at: indexPath)
      }
    
      private var _fetchedResultsController: NSFetchedResultsController<CDEvent>?
    
      fileprivate var fetchedResultsController: NSFetchedResultsController<CDEvent>?
      {
        get
        {
          if _fetchedResultsController == nil
          {
            let request = CDEvent.fetchRequest() as NSFetchRequest<CDEvent>
    
            let daySort = NSSortDescriptor(key: "day.date", ascending: true)
    
            let startTimeSort = NSSortDescriptor(key: "startTime", ascending: true)
    
            let endTimeSort = NSSortDescriptor(key: "endTime", ascending: true)
    
            let nameSort = NSSortDescriptor(key: "artist.imageName", ascending: true)
    
            request.sortDescriptors = [daySort, startTimeSort, endTimeSort, nameSort]
    
            if case .filtered(let filter) = self.dataFilterType
            {
              let predicate = NSPredicate(format: "artist.imageName contains[cd] %@", filter)
    
              request.predicate = predicate
            }
            else
            {
              if case .filtered(let filter) = self.locationFilterType
              {
                let predicate = NSPredicate(format: "location.narrative = %@", filter.narrative!)
    
                request.predicate = predicate
              }
            }
    
            self._fetchedResultsController = NSFetchedResultsController(fetchRequest: request,
                                                                   managedObjectContext: DataProvider.sharedInstance.managedObjectContext,
                                                                   sectionNameKeyPath: "day.narrative",
                                                                   cacheName: nil)
          }
    
          do
          {
            try _fetchedResultsController!.performFetch()
          }
          catch
          {
            fatalError("Failed to initialize fetchedResultsController: \(error)")
          }
    
          return _fetchedResultsController
        }
      }
    
      public func commitChanges()
      {
        do
        {
          try self.fetchedResultsController?.managedObjectContext.save()
        }
        catch
        {
          fatalError("Could not save Event Context: \(error)")
        }
      }
    }
    
    
    extension EventDataProvider : UICollectionViewDataSource
    {
      public func numberOfSections(in collectionView: UICollectionView) -> Int
      {
        return self.fetchedResultsController?.sections?.count ?? 1
      }
    
      public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
      {
        guard let sections = self.fetchedResultsController?.sections else
        {
          fatalError("No sections in fetchedResultsController")
        }
    
        let sectionInfo = sections[section]
    
        return sectionInfo.numberOfObjects
      }
    
      public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
      {
        switch kind
        {
          case UICollectionElementKindSectionHeader:
            let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind,
                                                                             withReuseIdentifier: "EventSectionHeader",
                                                                             for: indexPath) as! EventSectionHeaderView
    
            let sections = self.fetchedResultsController?.sections
    
            let sectionInfo = sections?[indexPath.section]
    
            headerView.dateLabel.text = sectionInfo?.name
    
            return headerView
          default:
            assert(false, "Unexpected element kind")
        }
      }
    
      public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
      {
        let cell = collectionView.dequeueReusableCell(for: indexPath) as EventListCell
    
        cell.event = self.fetchedResultsController?.object(at: indexPath)// as? EntityType
    
        return cell
      }
    }
    

    Du coup, lorsque je veux changer le contenu de la UICollectionView, il ne faut que changer l'enum et appeler reloadData :

    extension EventsMasterViewController : UISearchBarDelegate
    {
      func searchBarCancelButtonClicked(_ searchBar: UISearchBar)
      {
        searchBar.text = nil
    
        searchBar.resignFirstResponder()
    
        EventDataProvider.sharedInstance.dataFilterType = .all
    
        collectionView.reloadData()
      }
    
      func searchBarSearchButtonClicked(_ searchBar: UISearchBar)
      {
        searchBar.resignFirstResponder()
      }
    
      func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
      {
        let searchText = searchBar.text ?? ""
    
        if (searchText.isEmpty)
        {
          DispatchQueue.main.async
          {
            [unowned self] in
    
            self.searchBar.resignFirstResponder()
          }
        }
    
        EventDataProvider.sharedInstance.dataFilterType = searchText.isEmpty ? .all : .filtered(searchText)
    
        collectionView.reloadData()
      }
    }
    
  • InsouInsou Membre
    février 2019 modifié #6

    @Larme : Woah.. merci.. j'ai adapté tout ça à mon code et tout se goupille plutôt bien ^^

    En fait à chaque fois que ça appelle source, je test si je suis dans mes favoris ou dans la recherche d'adresse et j'adapte mes valeurs selon mon cas..
    C'est le cas dans "didSelectRowAt" par exemple..

    J'ai un dernier truc où je galère..

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    
            if(!isFavoris){ // Si je ne suis pas dans mes favoris
                searchCompleter.queryFragment = searchText // On recherche l'adresse (autocompletion)
            }
            else{ // Sinon on est dans les favoris
                // Ici.. il doit chercher dans ma liste de favoris
            }
    }
    

    Comment faire pour qu'il recherche dans ma nouvelle source (favoris) ?
    Edit : Est ce qu'il n'y aurait pas une histoire de filtre sur la valeur de ma source ?
    Bon bah en effet, il y avait bien une histoire de filtre sur ma source ^^

    J'ai fais comme ça :

    Quand je clic sur mon bouton "Favoris", je sauvegarde ma source (mon JSON de favoris)

    if(imageBouton == imageFavoris){
                boutonFavoris.setImage(UIImage(named: "icon_pin_favoris"), for: .normal)
    
                var tempModels = [AddressTableViewModel]()
                for leFavori in favoris {
                    let aModel = AddressTableViewModel(title: leFavori.1["NomZone"].stringValue, subtitle: leFavori.1["Adresse"].stringValue)
                    tempModels.append(aModel)
                }
                source = tempModels
                sourceTMP = source // ici je garde la source pour éviter les modifications
    
                // Sauvegarde l'adresse (recherche) pour la remettre quand on sort des favoris
                if let adresseSearchBar = self.searchbarAdresse.text {
                    if(!adresseSearchBar.isEmpty){
                        adresseTMP = adresseSearchBar
                        fonctions().consoleLog(texte:adresseTMP,etat:0,separateur: true)
                    }
                }
                self.searchbarAdresse.text = ""
            }
    

    et ma fonction de recherche est devenue :

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    
            if(!isFavoris){ //  On est dans la recherhe d'adresse
                searchCompleter.queryFragment = searchText
            }
            else{ // Sinon on est dans les favoris
    
                if(!searchText.isEmpty){
                    source = sourceTMP.filter{$0.title.contains(searchText)}
                    tableViewResultatAdresse.reloadData()
                }
                else{
                    source = sourceTMP
                    tableViewResultatAdresse.reloadData()
                }
            }
    
    }
    

    ..et hop, ça fonctionne bien :)

    pfiouf.. merci, j'pensais pas que ça allait se passer aussi bien :D

    @Joanna Olalala ton code est beaucoup trop compliqué pour moi :lol:

  • @Larme Je reviens sur cette fonction et ton commentaire..

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let model = source[indexPath.row]
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil) //Au fait, il ne devrait pas y avoir de nil ici
        cell.textLabel?.text = model.title
        cell.detailTextLabel?.text = model.subtitle
        cell.imageView?.image = UIImage(named: "icon_building")
        return cell
    }
    

    Pourquoi je ne devrais pas avoir nil ici ?

    L'identifiant de la cellule, c'est pas juste quand je veux faire des cellules personnalisées ?
    Si je veux juste utiliser le type de cellule par défaut et remplir avec mes données, je suis obligé de mettre un identifiant à la cellule ?

  • Joanna CarterJoanna Carter Membre, Modérateur
    février 2019 modifié #8

    Il faut, au moins, ajouter un cellule par défaut comme prototype dans le UITableView, remplir l'identifiant a avec n'importe quoi (e.g. "mon identifiant") et utiliser le code suivant :

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
      let model = source[indexPath.row]
    
      let cell = tableView.dequeueReusableCell(withIdentifier: "mon identifiant")
    
      cell.textLabel?.text = model.title
    
      cell.detailTextLabel?.text = model.subtitle
    
      cell.imageView?.image = UIImage(named: "icon_building")
    
      return cell
    }
    

    https://developer.apple.com/documentation/uikit/uitableview/1614891-dequeuereusablecell

  • Bah c'est ça que je ne comprends pas...
    Pourquoi il faut ajouter une cellule par défaut ?

    Pour moi, il le faut uniquement si tu fais des cellules personnalisés nan ?
    Si je veux changer l'affichage par défaut des cellules.. mais sinon, aucun intérêt, j'ai juste à remplir les cellules par défauts avec mes données et il s'occupe du reste.

    Je me dis qu'Apple met à disposition une tableView avec des cellules de base, qu'on peut utiliser juste en remplissant avec nos données et que si on veut changer l'affichage, là on utilise l'identifiant et une cellule personnalisable.

    Quel intérêt d'ajouter une cellule par défaut pour refaire la même que celle qui m'est proposée de base ?

    J'essaie juste de comprendre en quoi c'est mieux

  • Joanna CarterJoanna Carter Membre, Modérateur

    Il s'agit la réutilisation des cellules pour ne pas avaler la mémoire.

    Et, en plus, il y a au moins trois ou quatre variantes de la cellule "par défaut". Donc, on peut choisir l'agencement en IB, la donner un identifiant et, hop ! pas de code nécessaire.

  • LarmeLarme Membre
    février 2019 modifié #11

    Je viens de tilter ce qui m'avait échappé.

    Au départ, je pensais que tu utilisaisdequeueReusableCell(withIdentifier:), car je m'attendais à ce que tu le fasses. Or le paramètre reuseIdentifier n'est pas optionnel. Cela n'aurait pas dû compiler, et si jamais cela fonctionnait, cela aurait dû crasher. Donc cela m'étonnait grandement tout ça, d'où mon commentaire.

    La subtilité qui m'avait échappée, c'est que tu appelles en réalité UITableViewCell.init(style:, reuseIdentifier:) et non pas dequeueReusableCell(withIdentifier:).
    Donc en réalité, tu TE DOIS d'utiliser dequeueReusableCell(withIdentifier:) à moins que tu sois un « grand expert », que tu bidouilles et encore.

    Cela n'a rien avoir à "ajouter une cellule par défaut", c'est tout le concept du REUSE qui te manque et qui est très important pour les UITableView/UICollectionView. Tu bypasses la totalité de l'optimization mémoire mise en place par UIKit (note que ce concept existe sur Android avec le RecycleView).
    Or pour cela, tu te dois d'utiliser un identifier, que la cellule soit basique ou non.

    Tu dois faire normalement au début:

    maTableView.register(UITableViewCell.self, forCellReuseIdentifier: "myIdentifier")
    

    Et dans tableView(_:cellForRowAt:):

    let cell = dequeueReusableCell(withIdentifier: ""myIdentifier", for: indexPath)

  • Donc en réalité, tu TE DOIS d'utiliser dequeueReusableCell(withIdentifier:) à moins que tu sois un « grand expert », que tu bidouilles et encore.

    Première réaction : Cool, j'suis un grand expert >:)

    Deuxième réaction : Hé merde, j'vais devoir revenir sur ce code :neutral:

    Mais ceci dit, c'est plus clair maintenant ^^

    Du coup, je n'utilise pas le storyboard pour mettre un identifiant à ma cellule, je peux le faire via le code si j'ai bien compris ?

    maTableView.register(UITableViewCell.self, forCellReuseIdentifier: "myIdentifier")
    

    Je vais faire quelques tests et remettre tout ça à jour parce que c'est pas le seul endroit où j'utilise des cellules..

  • Après quelques tests :

    Cas 1 :

    Dans viewDidLoad :

    tableViewResultatAdresse.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifierCellule)
    

    Dans cellForRowAt :

    let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifierCellule, for: indexPath)
    

    Résultat : J'ai perdu le style : .subtitle :/


    Cas 2 :

    Dans viewDidLoad :

    J'ai enlevé ma ligne précédente :

    //tableViewResultatAdresse.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifierCellule)
    

    Dans cellForRowAt :

    J'ai gardé la ligne rajoutée :

    let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifierCellule, for: indexPath)
    

    Dans le storyboard :

    J'ai rajouté un prototype de cellule avec le style "subtitle" (Ok Joanna, je viens de comprendre où se situait les plusieurs style par défaut ^^) et je lui ai donné l'identifiant que je réutilise dans mon code (reuseIdentifierCellule)

    Résultat :

    Je retrouve bien mon style "subtitle", tout fonctionne bien.
    Le fait de passer faire ça par le storyboard, ça remplace donc la ligne de code que j'ai enlevé si j'ai bien suivi ?

    //tableViewResultatAdresse.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifierCellule)
    

    Donc là normalement, j'utilise bien le concept de REUSE ? ^^

  • Joanna CarterJoanna Carter Membre, Modérateur

    @Insou a dit :
    Donc là normalement, j'utilise bien le concept de REUSE ? ^^

    Voilà ! tu l'as bien pigé ;)

  • pfiouf, une bonne chose de réglée :D

    Merci à vous 2 ^^
    Je vais finir par vous créditer dans mon app à ce rythme là lol

    (et à bientôt pour la suite de mes soucis avec swift :blush: )

Connectez-vous ou Inscrivez-vous pour répondre.