Swift + Magical record + select distinct

LeChatNoirLeChatNoir Membre, Modérateur

Salut à  tous,


 


Ca fait 1 heure que je m'énerve pour faire un select distinct via swift+MR....


 


Voilà  le code que j'ai réussi à  pondre mais le compilo m'envoie paitre avec une erreur que je ne comprends pas.


 


J'ai des entités Topo qui ont un attribut "region". Je veux donc une liste de toutes les régions distinctes.



let paidFilter = NSPredicate(format:"free=%d",segment)
let req = Topo.mr_requestAll(with:paidFilter)
req.propertiesToFetch = ["region"]
req.returnsDistinctResults = true
region = Topo.mr_executeFetchRequest(req)

Et Xcode me dit : 


 


Cannot invoke 'mr_executeFetchRequest' with an argument list of type '(NSFetchRequest<NSFetchRequestResult>)'


 


J'ai fait quoi de mal ?


Merci :)


«1

Réponses

  • Je crois que c'est due au fait qu'il ne sait pas de quel type il s'agit. J'avais eu ce problème lors d'une migration sur Swift 3 et en gros il fallait spécifier le type.


  • LeChatNoirLeChatNoir Membre, Modérateur

    Et concrètement, tu fais comment ? J'ai essayé un as! [Topo] mais pas mieux...


  • C'est ça. Après le fetch un as [NSManagedObject]. 


    Mais dans mon cas c'était sans MagicalRecord donc je sais pas si la librairie joue un rôle dans ton erreur.


  • LeChatNoirLeChatNoir Membre, Modérateur

    Bon j'avance mais je coince toujours...


    Ce code là  me fait une requête CoreData qui me ramène 9 résultats.



    var region:[Topo]!
    let req = Topo.mr_requestAll(with:paidFilter)
    req.propertiesToFetch = ["region"]
    req.returnsDistinctResults = true
    req.resultType = NSFetchRequestResultType.dictionaryResultType
    region = Topo.mr_executeFetchRequest(req) as! [Topo]


    Dans le debugger, un print region me montre 



    (lldb) print region
    ([Topo]?) $R1 = 9 values {
    [0] = 0x0000600001a2ab40
    [1] = 0x0000600001a2ab20
    [2] = 0x0000600001a2acc0
    [3] = 0x0000600001a2ab60
    [4] = 0x0000600001a2aa80
    [5] = 0x0000600001a2aaa0
    [6] = 0x0000600001a2ac00
    [7] = 0x0000600001a2ab80
    [8] = 0x0000600001a2abe0

    Seulement le type retourné ne semble pas lui convenir car dès que je veux accéder à  un membre, j'ai un beau 



    fatal error: NSArray element failed to match the Swift Array Element type
    expression produced error: error: /var/folders/gr/f6m2njfj18j4rrycv0ycm_700000gp/T/./lldb/92795/expr15.swift:1:81: error: use of undeclared type '__ObjC'


    Alors comme je demande un retour sous forme de NSDicitonary, j'essaye de modifier mon tableau en [NSDictionary] mais la ligne du executeFetch est alors en erreur :



    NSDictionary is not a subtype of NSManagedObject

    Pas étonnant puisque executeFetch est censée ramené des NSManagedObject...


     


    Bref, je suis perdu  B)

  • LeChatNoirLeChatNoir Membre, Modérateur

    Petite précision qui a son importance, si je ne mets pas req.resultType = NSFetchRequestResultType.dictionaryResultType, ca part en cacahuète....



    2017-01-11 22:23:20.222 ClimbingAway[14026:13197190] Error: The database appears corrupt. (invalid primary key)
    2017-01-11 22:23:20.225 ClimbingAway[14026:13197190] Error Message: Impossible d'ouvrir le fichier " ClimbingAway.sqlite " car son format n'est pas correct.



     


  • LeChatNoirLeChatNoir Membre, Modérateur

    Ca cause à  personne visiblement...


     


    Ici, c'est clairement un pb de typage ... Malgré mes recherches, je n'ai rien trouvé sur "comment déterminer quels types contient une variable". J'ai chercher dans le débuger. Un print m'affiche bien le tableau avec un type mais quand je fais un po, bim. Erreur...

  • Je connais mal Swift, mais pourquoi tu ne tenterais pas un NSLog de ton objet. ça te donne pas la classe ?


     


    Ne serait-pas (par hasard) un problème de "faulted objects" ?


  • LeChatNoirLeChatNoir Membre, Modérateur

    Le print marche bien et il m'indique que c'est un array de Topo (ou de NSManagedObject).


    C'est au moment d'acccéder à  ses éléments que ca se complique et qu'il plante sauvagement. Un print de region[0] fait planter.


  • Joanna CarterJoanna Carter Membre, Modérateur

    Est-ce que region est valide ?


     


    Est-ce que region.count renvoie plus que 0 ?


     


    region[0] renvoie un optionnel, du coup, il faut vérifier que le résultat n'est pas nil avant de le manipuler


  • LeChatNoirLeChatNoir Membre, Modérateur

    oui, région est valide :



    (lldb) print region
    ([Topo]?) $R1 = 9 values {
    [0] = 0x0000600001a2ab40
    [1] = 0x0000600001a2ab20
    [2] = 0x0000600001a2acc0
    [3] = 0x0000600001a2ab60
    [4] = 0x0000600001a2aa80
    [5] = 0x0000600001a2aaa0
    [6] = 0x0000600001a2ac00
    [7] = 0x0000600001a2ab80
    [8] = 0x0000600001a2abe0
  • Joanna CarterJoanna Carter Membre, Modérateur
    janvier 2017 modifié #12

    Tu as fait un forced unwrap avec le var region [Topo]!  >:(


     


    Le mieux c'est d'utiliser quelque chose comme :



    let req = Topo.mr_requestAll(with:paidFilter)

    req.propertiesToFetch = ["region"]

    req.returnsDistinctResults = true

    guard let region = Topo.mr_executeFetchRequest(req) as? [Topo] else
    {
    // tu as un erreur ...
    }

    // utilises region qui est garanti d'être valide

  • LeChatNoirLeChatNoir Membre, Modérateur
    janvier 2017 modifié #13

    J'ai fait 



    guard let reg = Topo.mr_executeFetchRequest(req) as? [Topo] else
    {
    // tu as un erreur ...
    print("ERRRORORORORRO")
    return
    }


    Et à  l'exécution  >:(



    fatal error: NSArray element failed to match the Swift Array Element type

  • As-tu essayé de mettre des points d'arrêt dans la méthode de MR pour voir un peu ce qui se passe ?


  • Joanna CarterJoanna Carter Membre, Modérateur
    janvier 2017 modifié #15

    Dans le cas des requêtes distinct, le résultat d'une requête est un array de dictionnaires, pas un array d'objets.

     



    let req = Topo.mr_requestAll(with:paidFilter)

    req.propertiesToFetch = ["region"]

    req.returnsDistinctResults = true

    req.resultType = .dictionaryResultType

    guard let region = Topo.mr_executeFetchRequest(req) as? [[String : Any]] else
    {
    // tu as un erreur ...
    }

    if let obj = region[0]
    {
    let regionValue = obj["region"]
    }

    Je n'ai pas l'essayé et tu devrais peut-être vérifier le type des éléments de l'array


  • LeChatNoirLeChatNoir Membre, Modérateur

    Malheureusement, quand je mets ça, il me dit :


     


    'NSDictionary' is not a subtype of 'NSManagedObject'


     


    B)


     


    J'aimerai vérifier le type des éléments de l'array mais dès que je cherche à  y accéder, ça plante  :'(


  • Joanna CarterJoanna Carter Membre, Modérateur
    janvier 2017 modifié #17
    Je crois que c'est car toutes les méthodes de Topo sont traduites en Swift pour qu'elles renvoient un array du type (Topo). De mon examination du code source sur Github, il n'y a pas le moyen de faire les requêtes distinct qui renvoient les dictionnaires :(
  • LeChatNoirLeChatNoir Membre, Modérateur

    Ah :( En fait, Topo est en Objective-C mais le viwController dans lequel je suis est en swift...


     


    Bon ben c'est pas grave, je fais ça du coup :



    let paidFilter = NSPredicate(format:"free=%d",segment)
    region=Topo.mr_findAllSorted(by: "region", ascending: true, with: paidFilter) as! [Topo]!
    filters=Array(Set(region.map{$0.region}))

    Ca marche pareil. C'est moins élégant :( Merci de votre aide !


  • Joanna CarterJoanna Carter Membre, Modérateur
    Bon mais arrêtes de utiliser les ! ;)
  • LeChatNoirLeChatNoir Membre, Modérateur

    ok. Je vais utiliser guard. Promis :)


  • Joanna CarterJoanna Carter Membre, Modérateur
    janvier 2017 modifié #21

    :D


     


    Quelques petites astuces :


     


    1. Ne declares pas region avant de l'utiliser ; c'est pas nécessaire.


     


    2. Tu mets une liste de Topo dans region ; il vaut mieux de le nommer topos, non ???


     


    3. Si tu anticipes avoir besoin de trouver les valeurs uniques d'autres arrays, voici une extension sur Array qui le simplifierait



    extension Array where Element : Equatable
    {
    func unique() -> [Element]
    {
    var result = [Element]()

    for element in self
    {
    if !result.contains(element)
    {
    result.append(element)
    }
    }

    return result
    }
    }

    Du coup, tu pourrais faire



    let paidFilter = NSPredicate(format:"free = %d",segment)

    guard let topos = Topo.mr_findAllSorted(by: "region", ascending: true, with: paidFilter),
    let regions = trucs.map { $0.region }.unique() else
    {
    // pas de regions
    }

    // utiliser regions, qui est un [String]

    4. Mets les espaces entres les mots et les opérateurs et entre les lignes ; c'est plus facile à  trouver les problèmes plus tard  ::)


     


    5. Si le membre "free" dans Topo ne pourrait être que "oui" ou "non", pourquoi pas utiliser Bool à  la place de Int ?


  • LeChatNoirLeChatNoir Membre, Modérateur
    janvier 2017 modifié #22

    Little question about guard :) Je parle d'un autre cas de figure, une String qui peut être nulle et que je veux affecter à  un Label.


     


    Contexte : je suis dans le cellForRow d'une UITableView. Dedans, je fais un switch indexPath.section.


     


    Quand j'utilise guard, dans le else, il me demande de mettre un return ou un break. Si je mets un break, c'est embêtant car le reste ne s'exécute pas alors que je le souhaite quand meme.


     


    C'est peut être pas le bon cas d'utilisation de guard ?


     


    [edit] je crois que dans ce cas, c'est plus ça que je dois utiliser



    let cellPrice = topo.formatedPrice ?? ""

    non ?


  • Joanna CarterJoanna Carter Membre, Modérateur

    Tu mets le guard ou exactement ? Tu peux montrer le code ?


  • Pourquoi pas un 



    iflet

    ?


  • Joanna CarterJoanna Carter Membre, Modérateur

    Mais si, c'est possible, selon le contexte


  • LeChatNoirLeChatNoir Membre, Modérateur

    Voilà  ce que j'ai fait. Dans ce cas de figure, j'ai utilisé ?? pour mettre une valeur par défaut.


    En gros, formattedPrice peut être nil.


    Et pareil pour les UserDefault et le langage.


     


    J'utilise ?? pour mettre "" dans ces cas là . Pas guard. 



    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    switch indexPath.section {
    case 0:
    // Header Photo + buy + desc
    let cell=tableView.dequeueReusableCell(withIdentifier: "packHeader", for: indexPath) as! packDetailHeaderCell

    // Photo
    let photoUrl=String(format:urlToposThumbs,topo.id)
    cell.packImage!.sd_setImage (with: URL(string:photoUrl))

    // Price zone
    let cellPrice = topo.formatedPrice ?? ""
    cell.packBuyButton.setTitle(String(format:"%@ (%@)",cellPrice,"Acheter".localized), for: UIControlState.normal)
    cell.packBuyLabel.text="Prix du pack complet".localized

    // Desc
    let languagePref = UserDefaults.standard.value(forKey: kUserPreferedLanguage) as? String ?? ""
    if (languagePref == "fr") {
    cell.packDesc.text=topo.descFr
    } else {
    cell.packDesc.text=topo.descEn
    }

    return cell
    case 1:
    ....

    }

  • Joanna CarterJoanna Carter Membre, Modérateur

    OK. On commence où ?


     


    1. Tu as beaucoup de logique dans le mauvais endroit.


     


    2. Tous les types, comme packDetailHeaderCell devrait commencer avec une majuscule (PackDetailHeaderCell)


     


    J'ai fait un struct Topo pour illustrer l'example



    struct Topo
    {
    var id = "anId"

    var formattedPrice = "123,45"

    var frenchDescription = "Café"

    var englishDescription = "Coffee"
    }

    En le faisant, j'ai vu quelques soucis :


     


    1. Le formatting des chiffres et dates devrait être fait seulement dans le ViewController, en utilisant NumberFormatter et DateFormatter. Ne mets que les chiffres ou les dates dans les entités.


     


    2. Tu as limité la choix de langues à  l'anglais ou le français ; il vaut mieux d'avoir une deuxième entité, comme LocalizedTopo, qui tiennent les attributs comme : language (qui tient "en", "fr", etc), description, etc. La gestion des langues est plus compliqué que l'on imagine. J'ai rédige un struct pour déterminer la langue préférée et, en cas échéant parmi ceux que l'on a fourni, ça renvoie la langue de développement (ici "fr")



    struct Language
    {
    static var current: String
    {
    get
    {
    guard Locale.autoupdatingCurrent.languageCode != nil else
    {
    return "fr"
    }

    let projectLanguages = Bundle.main.localizations.flatMap
    {
    (localization) -> String? in

    if localization == "Base"
    {
    return nil
    }

    let endIndex = localization.index(localization.startIndex, offsetBy: 2)

    return localization.substring(to: endIndex)
    }

    let preferredLanguages = Locale.preferredLanguages.map
    {
    (language) -> String in

    let endIndex = language.index(language.startIndex, offsetBy: 2)

    return language.substring(to: endIndex)
    }

    if let preferredLanguage = preferredLanguages.first(where:
    {
    (language) -> Bool in

    projectLanguages.contains(language)
    })
    {
    return preferredLanguage
    }

    return "fr"
    }
    }
    }

    Du coup, on peut ajouter plus facilement n'importe quelle langue dans l'avenir.


     


    Bon. maintenant aux cellules.


     


    Il vaut mieux de séparer le code de présentation dans la cellule :



    class PackDetailHeaderCell : UITableViewCell
    {
    @IBOutlet weak var packImage: UIImageView!

    @IBOutlet weak var buyButton: UIButton!

    @IBOutlet weak var buyLabel: UILabel!

    @IBOutlet weak var descriptionLabel: UILabel!

    var topo: Topo?
    {
    didSet
    {
    guard let topo = self.topo else
    {
    return
    }

    Date

    self.packImage.image = UIImage(named: topo.id)

    self.buyButton.titleLabel?.text = "\("Acheter".localizedCapitalized) (\(topo.formattedPrice))"

    if let languagePref = UserDefaults.standard.string(forKey: kUserPreferedLanguage),
    languagePref == "fr"
    {
    self.descriptionLabel.text = topo.frenchDescription
    }
    else
    {
    self.descriptionLabel.text = topo.englishDescription
    }

    }
    }

    override func awakeFromNib()
    {
    self.buyLabel.text = "Prix du pack complet".localizedCapitalized
    }

    override func prepareForReuse()
    {
    super.prepareForReuse()

    self.packImage.image = nil

    self.buyButton.titleLabel?.text = "Acheter".localizedCapitalized

    self.descriptionLabel.text = nil
    }
    }

    Du coup, on peut simplifier le code dans la méthode tableView(_:, cellForRowAt:)



    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
    switch indexPath.section
    {
    case 0:

    guard let cell = tableView.dequeueReusableCell(withIdentifier: "packHeader", for: indexPath) as? PackDetailHeaderCell else
    {
    fatalError("Could not dequeue PackDetailHeaderCell")
    }

    cell.topo = self.topo

    return cell

    default:
    fatalError("Invalid Section")
    }
    }
  • LeChatNoirLeChatNoir Membre, Modérateur

    :)


     


    Pour le formattedPrice, je préfère le stocker dans mon entité car les prix proviennent de l'appStore avec une struct "locale" et je préfère ne pas interroger l'appStore à  chaque fois...


     


    Pour le reste, merci bcp, je vais étudier ton code grande pécheresse du swift  o:)

  • Joanna CarterJoanna Carter Membre, Modérateur

    Pour le formattedPrice, je préfère le stocker dans mon entité car les prix proviennent de l'appStore avec une struct "locale" et je préfère ne pas interroger l'appStore à  chaque fois...




    Lorsque tu trouves les prix, ils sont en quel format ?
  • LeChatNoirLeChatNoir Membre, Modérateur

    C'est dans un SKProduct.


     


    Donc le prix :



    var price: NSDecimalNumber { get }

    Et après (doc Apple)  :



    Discussion

    Your application can format the price using a number formatter, as shown in the following sample code:

    Code Listing 1
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
    [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
    [numberFormatter setLocale:product.priceLocale];
    NSString *formattedString = [numberFormatter stringFromNumber:product.price];
  • Joanna CarterJoanna Carter Membre, Modérateur
    janvier 2017 modifié #31

    Et voilà  !


     


    Tu mets Topo.price en NSDecimalNumber et tu mets quelque chose comme le code d'Apple dans la classe PackDetailHeaderCell, juste avant de l'assigner au bouton.


     


    Mais, à  la place d'utiliser product.priceLocale, tu utiliserais Locale.autoupdatingCurrent.


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