NSKeyedArchiver et CLLocationCoordinate2D

Bonjour à  tous


 


Je tente de sauvegarder des CLLocationCoordinate2D avec userdefaults (XCOde 9 et Swift 4)


 


J'ai une classe parkingSpot qui contient des infos dont le CLLocationCoordinate2D, un required init(coder aDecoder: NSCoder) et func encode(with aCoder: NSCoder)


 


Je sauvegarde mes données avec :



let postsData = NSKeyedArchiver.archivedData(withRootObject: parkedCarAnnotation!)
UserDefaults.standard.set(postsData, forKey: "posts")
UserDefaults.standard.synchronize()

Pas d'erreur à  la compilation mais quand je veux tester l'app, paf le chien :


'NSInvalidArgumentException', reason: '*** -[NSKeyedArchiver encodeValueOfObjCType:at:]: this archiver cannot encode structs'


 


J'ai trouvé des choses sur le net mais rien n'a pu m'aider à  résoudre mon problème.


 


Auriez-vous une idée ?


 


Merci


Réponses

  • Bonjour,


     


    this archiver cannot encode structs


     


    Le message semble assez explicite. Il faudrait voir ta méthode encode mais tu dois essayer "serializer" une valeur qui ne peut pas l'être. Il faut dans ce cas transformer cette valeur en autre chose qui sera accepté par NSKeyedArchiver. Si c'est la structure CLLocationCoordinate2D il doit être possible de faire une transformation en String ou bien d'encoder lat et long distinctement. Lors de l'init il suffit de transformer à  nouveau la chaine ou 'lat' et 'long' en CLLocationCoordinate2D.


  • Tu veux sauvegarder dans (NS)UserDefault une class custom et utiliser NSKeyedArchiver (et son pendant de désarchivage) ce qui transforme ta classe en (NS)Data.


     


    Tu as bien implémenté le initWithCoder: et decodeWithCoder: (en bref respecter le NSCoding protocol), et tous les "sous-objets" doivent l'implémenter aussi.


    La plupart des objets/types basiques le supportent, mais CLLocationCoordinate2D est une struct et ne le supporte pas, et il faut le faire à  la main.


     


    Comme il s'agit d'une simple structure avec deux double (qui est caché derrière un CLLocationDegrees), plusieurs choix s'offrent à  toi, donc le plus simple, enregistrer la myLocation.latitude et myLocation.longitude lors de l'archive, et lors de la désarchive (dans initWithCoder:), les récupérer et d'appeler CLLocationCoordinate2DMake() avec.


  • Joanna CarterJoanna Carter Membre, Modérateur
    juillet 2017 modifié #4

    Ou tu peux créer une extension pour Data qui marchera avec n'importe quelle struct :



    extension Data
    {
    init<T>(from value: T)
    {
    var value = value

    self.init(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }

    func to<T>(type: T.Type) -> T
    {
    return self.withUnsafeBytes { $0.pointee }
    }
    }

    Et, pour l'utiliser :



    let location = CLLocationCoordinate2DMake(48.654755, -3.631023)

    let data = Data(from: location)

    UserDefaults.standard.set(data, forKey: "location")

    if let defaultsData = UserDefaults.standard.value(forKey: "location") as? Data
    {
    let defaultsLocation = defaultsData.to(type: CLLocationCoordinate2D.self)

    print(defaultsLocation.latitude)

    print(defaultsLocation.longitude)
    }

  • Ou tu peux créer une extension pour Data qui marchera avec n'importe quelle struct :

    ...


    Bonne suggestion, il faut cependant faire très attention à  l'évolution du contenu des structures.
  • Merci beaucoup pour vos réponses, je suis en train de les digérer...


    ::)


  • macphimacphi Membre
    juillet 2017 modifié #7

    Bon ben la digestion est mauvaise et en plus je n'avais pas tout dit...


    La petite app doit sauvegarder un emplacement avec une description et c'est un test pour incruster dans mon app principale plus quand j'aurais pigé comment faire.


     


    Je tente de sauvegarder une custom class qui contient entre autre un CLLocationCoordinate2D et ça semble fonctionner.


    ​En revanche pas moyen de récupérer les données après kill de l'app...


     


    Voici le code de Joanna que j'ai adapté sans tout comprendre c'est certain :


     


    La custom classe :



    import UIKit
    import MapKit
    import Contacts

    class ParkingSpot: NSObject, MKAnnotation {
    var title: String?
    var locationName: String?
    var coordinate: CLLocationCoordinate2D

    init(title: String, locationName: String, coordinate: CLLocationCoordinate2D) {
    self.title = title
    self.locationName = locationName
    self.coordinate = coordinate
    }

    init(coder aDecoder: NSCoder!) {
    self.title = aDecoder.decodeObject(forKey: "title") as? String
    self.locationName = aDecoder.decodeObject(forKey: "locationName") as! String
    self.coordinate = aDecoder.decodeObject(forKey: "coordinate") as! CLLocationCoordinate2D
    }

    func initWithCoder(aDecoder: NSCoder) -> ParkingSpot {
    self.title = aDecoder.decodeObject(forKey: "title") as! String
    self.locationName = aDecoder.decodeObject(forKey: "locationName") as! String
    self.coordinate = aDecoder.decodeObject(forKey: "coordinate") as! CLLocationCoordinate2D
    return self
    }

    func encodeWithCoder(aCoder: NSCoder!) {
    aCoder.encode(title, forKey: "title")
    aCoder.encode(locationName, forKey: "locationName")
    aCoder.encode(coordinate, forKey: "coordinate")
    }

    }

    Le code du button qui sauvegarde :



    @IBAction func parkBtnWasPressed(_ sender: Any) {
    if mapView.annotations.count == 1 {
    mapView.addAnnotation(parkedCarAnnotation!)
    parkBtn.setImage(UIImage(named: "foundCar.png"), for: .normal)

    let DataTitle = Data(from: parkedCarAnnotation?.title)
    let encodedTitle = NSKeyedArchiver.archivedData(withRootObject: DataTitle)
    let DataLocationName = Data(from: parkedCarAnnotation?.locationName)
    let encodedLocationName = NSKeyedArchiver.archivedData(withRootObject: DataLocationName)
    let DataCoordinate = Data(from: parkedCarAnnotation?.coordinate)
    let encodedCoordinate = NSKeyedArchiver.archivedData(withRootObject: DataCoordinate)

    let encodedArray: [Data] = [encodedTitle, encodedLocationName, encodedCoordinate]

    UserDefaults.standard.set(encodedArray, forKey: "parkingSpot")


    } else {
    mapView.removeAnnotations(mapView.annotations)
    parkBtn.setImage(UIImage(named: "parkCar.png"), for: .normal)
    }
    centerMapOnLocation(location: LocationService.instance.locationManager.location!)
    }


    Et le code qui est censé recharger les données après un kill par exemple :



    override func viewDidLoad() {
    super.viewDidLoad()
    mapView.delegate = self
    checkLocationAuthorizationStatus()

    if let defaultsData = UserDefaults.standard.value(forKey: "parkingSpot") as? Data
    {
    let defaultsParkingSpot = defaultsData.to(type: ParkingSpot.self)
    parkedCarAnnotation? = defaultsParkingSpot
    mapView.addAnnotation(parkedCarAnnotation!)
    parkBtn.setImage(UIImage(named: "foundCar.png"), for: .normal)

    } else {
    mapView.removeAnnotations(mapView.annotations)
    parkBtn.setImage(UIImage(named: "parkCar.png"), for: .normal)

    }


    }

  • Joanna CarterJoanna Carter Membre, Modérateur

    Pourquoi pas utiliser CoreData pour sauvegarder les données ?


  • Si tu penses que c'est mieux, je regarder mais ça semble encore plus compliqué pour un débutant que UserDefaults...


     


    Donc ce que je fais ne peux pas fonctionner ?


  • LarmeLarme Membre
    juillet 2017 modifié #10

    let encodedArray: [Data] = [encodedTitle, encodedLocationName, encodedCoordinate]
    UserDefaults.standard.set(encodedArray, forKey: "parkingSpot")

    Tu sauvegardes un ARRAY d'objets de type Data serializant chacune leur propre classe.



    if let defaultsData = UserDefaults.standard.value(forKey: "parkingSpot") as? Data
    {
        let defaultsParkingSpot = defaultsData.to(type: ParkingSpot.self)

    Tu lis ça comme un objet de type Data qui serializait un ParkingSpot.


     


    C'est plus que confus.


     


    Tu as un init(coder aDecoder: NSCoder!) et un initWithCoder(aDecoder: NSCoder) -> ParkingSpot (différence réelle ? à  part peut-être du Swift 3 et du Swift inférieur ?)


     


     


    Tout ceci :



    ​let DataTitle = Data(from: parkedCarAnnotation?.title)
    let encodedTitle = NSKeyedArchiver.archivedData(withRootObject: DataTitle)
    let DataLocationName = Data(from: parkedCarAnnotation?.locationName)
    let encodedLocationName = NSKeyedArchiver.archivedData(withRootObject: DataLocationName)
    let DataCoordinate = Data(from: parkedCarAnnotation?.coordinate)
    let encodedCoordinate = NSKeyedArchiver.archivedData(withRootObject: DataCoordinate)
               
    let encodedArray: [Data] = [encodedTitle, encodedLocationName, encodedCoordinate]
               
    UserDefaults.standard.set(encodedArray, forKey: "parkingSpot")

    devrait être :


    Si parkedCarAnnotation est un objet de type ParkingSpot :



    let encodedCoordinate = NSKeyedArchiver.archivedData(withRootObject: parkedCarAnnotation)

    Sinon



    let parkingSpot = ParkingSpot.init(title: parkedCarAnnotation.title, locationName: parkedCarAnnotation.locationName, coordinate: parkedCarAnnotation.coordinate)
    let encodedCoordinate = NSKeyedArchiver.archivedData(withRootObject: parkingSpot)

    PS : Il y a peut-être des ? et autres ! propres à  Swift qui manque, mais je m'attache plus à  la logique/concept.


  • Joanna CarterJoanna Carter Membre, Modérateur
    juillet 2017 modifié #11

    Voici mon code :



    extension Data
    {
    init<T>(from value: T)
    {
    var value = value

    self.init(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }

    func to<T>(type: T.Type) -> T
    {
    return self.withUnsafeBytes { $0.pointee }
    }
    }


    class ParkingSpot : NSObject, NSCoding, MKAnnotation
    {
    var title: String?

    var locationName: String?

    var coordinate: CLLocationCoordinate2D

    init(title: String, locationName: String, coordinate: CLLocationCoordinate2D)
    {
    self.title = title

    self.locationName = locationName

    self.coordinate = coordinate
    }

    required init?(coder aDecoder: NSCoder)
    {
    title = aDecoder.decodeObject(forKey: "title") as? String

    locationName = aDecoder.decodeObject(forKey: "locationName") as? String

    let coordinateData = aDecoder.decodeObject(forKey: "coordinate") as? Data

    coordinate = coordinateData?.to(type: CLLocationCoordinate2D.self)
    }

    func encode(with aCoder: NSCoder)
    {
    aCoder.encode(title, forKey: "title")

    aCoder.encode(locationName, forKey: "locationName")

    let coordinateData = Data(from: coordinate)

    aCoder.encode(coordinateData, forKey: "coordinate")
    }
    }


    {
    let spot = ParkingSpot(title: "here", locationName: "there", coordinate: CLLocationCoordinate2D(latitude: 34, longitude: 3))

    let data = NSKeyedArchiver.archivedData(withRootObject: spot)

    UserDefaults.standard.set(data, forKey: "parkingSpot")

    ...

    if let retrievedData = UserDefaults.standard.object(forKey: "parkingSpot") as? Data,
    let parkedCarAnnotation = NSKeyedUnarchiver.unarchiveObject(with: retrievedData) as? ParkingSpot
    {
    mapView.addAnnotation(parkedCarAnnotation)
    }
    }

  • macphimacphi Membre
    juillet 2017 modifié #12

    Alors oui avec ton code ça fonctionne parfaitement ! C'est super !


    Merci ! 


    Je ne pensais pas ramer autant pour sauvegarder un "simple" emplacement...


     


    Toute l'intelligence du code réside dans l'extension Data mais que hélas j'ai du mal à  comprendre.


    La fonction .to est appelée depuis la classe ParkingSport alors que l'extension est sur la classe Viewcontroller, par exemple.


     


    Pourrais-tu me dire en deux mots ce que fait cette extension ?


    La notation avec des T et des <T> ne me dit rien.


     


    Merci encore


  • @Joanna Carter


    Même si l'utilisation de Generics est bon, je préconiserais de ne pas les utiliser étant donné apparemment le niveau de @macphi. C'est un usage avancé dont il peut se passer pour l'instant tant qu'il maà®trise le reste basique du initWithCoder/decodeWithADecoder, NSKeyedArchive, NSKeyedUnarchive qui est le sujet actuellement.


    Je sais que cela a d'autres utilisations, mais il a tout le temps d'apprendre d'autres concepts avant, car j'ai peur que cela reste un peu de la magie/boà®te noire, et les exemples donnés trop vagues/abstraits ou trop précis, risquant de créer des erreurs/confusions par la suite.


  • Joanna CarterJoanna Carter Membre, Modérateur

    @macphi


     


    Bon. Comme dit Larme, peut-être il y a les choses dans mon code qu'il faut laisser pout le moment ; l'extension sur Data utilise un technique qui s'appelle "generics" - ce que tu puisse oublier à  ton niveau  :-*


     


    Du coup, j'ai refait le code pour la classe ParkingSpot avec un technique plus simple :



    class ParkingSpot : NSObject, NSCoding, MKAnnotation
    {
    var title: String?

    var locationName: String?

    var coordinate: CLLocationCoordinate2D

    init(title: String, locationName: String, coordinate: CLLocationCoordinate2D)
    {
    self.title = title

    self.locationName = locationName

    self.coordinate = coordinate
    }

    required init?(coder aDecoder: NSCoder)
    {
    title = aDecoder.decodeObject(forKey: "title") as? String

    locationName = aDecoder.decodeObject(forKey: "locationName") as? String

    let latitude = aDecoder.decodeDouble(forKey: "latitude")

    let longitude = aDecoder.decodeDouble(forKey: "longitude")

    coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    }

    func encode(with aCoder: NSCoder)
    {
    aCoder.encode(title, forKey: "title")

    aCoder.encode(locationName, forKey: "locationName")

    aCoder.encode(coordinate.latitude, forKey: "latitude")

    aCoder.encode(coordinate.longitude, forKey: "longitude")
    }
    }
  • Merci beaucoup pour vos réponses !


     


    Du coup effectivement les generics ça me dit quelque chose dans le bouquin Swift d'Apple vers la fin il me semble...


     


    Il faudra donc que je révise !


     


    :'(




  •  


    @macphi


     


    Bon. Comme dit Larme, peut-être il y a les choses dans mon code qu'il faut laisser pout le moment ; l'extension sur Data utilise un technique qui s'appelle "generics" - ce que tu puisse oublier à  ton niveau  :-*


     


    Du coup, j'ai refait le code pour la classe ParkingSpot avec un technique plus simple :




     


    Attention avec la classe Parkinson, c'est dangereux  ce truc ..

  • Joanna CarterJoanna Carter Membre, Modérateur
    juillet 2017 modifié #17

    Je constate que tu utilises Xcode 9 et Swift 4.


     


    Dans ce cas là , il y a les nouveaux APIs qui sont plus simples et n'utilises plus les Strings pour les noms des propriétés que l'on encode/decode.


     


    CLLocationCoordinate2D n'est pas encore automatiquement encodé et décodé mais, ce code ci-dessous, montre comment encoder/decoder les autres types qui ne sont pas encore pris en charge.



    extension CLLocationCoordinate2D : Codable
    {
    enum CodingKeys: String, CodingKey
    {
    case latitude

    case longitude
    }

    public init(from decoder: Decoder) throws
    {
    let values = try decoder.container(keyedBy: CodingKeys.self)

    let latitude = try values.decode(Double.self, forKey: .latitude)

    let longitude = try values.decode(Double.self, forKey: .longitude)

    self.init(latitude: latitude, longitude: longitude)
    }

    public func encode(to encoder: Encoder) throws
    {
    var container = encoder.container(keyedBy: CodingKeys.self)

    try container.encode(latitude, forKey: .latitude)

    try container.encode(longitude, forKey: .longitude)
    }
    }

    De mon avis, il est préférable de séparer l'idée d'un ParkingSpot de l'idée d'un annotation. Du coup, avec l'extension pour CLLocationCoordinate2D, nous pouvons créer une struct qui sera automatiquement encodé/décodé :



    public struct ParkingSpot : Codable
    {
    public var title: String?

    public var locationName: String?

    public var coordinate: CLLocationCoordinate2D
    }

    Puis, nous pouvons définir une class pour l'annotation :



    class ParkingSpotAnnotation : NSObject, MKAnnotation
    {
    var title: String?

    var subtitle: String?

    var coordinate: CLLocationCoordinate2D

    init(parkingSpot: ParkingSpot)
    {
    title = parkingSpot.title

    subtitle = parkingSpot.locationName

    coordinate = parkingSpot.coordinate
    }

    var asParkingSpot: ParkingSpot
    {
    return ParkingSpot(title: title, locationName: subtitle, coordinate: coordinate)
    }
    }

    Et, enfin, du code pour le tester :



    {
    ...

    let spot = ParkingSpot(title: "here", locationName: "there", coordinate: CLLocationCoordinate2D(latitude: 34, longitude: 3))

    do
    {
    let encoder = JSONEncoder()

    let data = try encoder.encode(spot)

    UserDefaults.standard.set(data, forKey: "parkingSpot")
    }
    catch
    {
    print("coding failed \(error)")
    }

    ...

    if let retrievedData = UserDefaults.standard.object(forKey: "parkingSpot") as? Data
    {
    do
    {
    let decoder = JSONDecoder()

    let retrievedSpot = try decoder.decode(ParkingSpot.self, from: retrievedData)

    let parkingSpotAnnotation = ParkingSpotAnnotation(parkingSpot: retrievedSpot)

    mapView.addAnnotation(parkingSpotAnnotation)
    }
    catch
    {
    print("decoding failed \(error)")
    }
    }

    ...
    }

  • Joanna CarterJoanna Carter Membre, Modérateur
    juillet 2017 modifié #18

    Si tu préfères de mélanger le ParkingSpot avec l'annotation, c'est possible comme ci :



    class ParkingSpot : NSObject, Codable, MKAnnotation
    {
    public var title: String?

    public var locationName: String?

    public var subtitle: String?
    {
    return locationName
    }

    public var coordinate: CLLocationCoordinate2D

    init(title: String?, locationName: String?, coordinate: CLLocationCoordinate2D)
    {
    self.title = title

    self.locationName = locationName

    self.coordinate = coordinate
    }
    }

    Et tu peux utiliser le même code de test qu'auparavant mais en oubliant le code qui crée l'annotation du ParkingSpot


  • C'est parfait, j'ai des devoirs à  faire en rentrant ce soir !


    :D


     


    J'ai rarement vu sur des forums des réponses aussi exhaustives.


     


    Merci beaucoup !


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