Aller au contenu


Photo

NSKeyedArchiver et CLLocationCoordinate2D


  • Please log in to reply
18 réponses à ce sujet

#1 macphi

macphi

    Eleveur de cacaoyers

  • Membre
  • PipPip
  • 43 messages

Posté 07 juillet 2017 - 13:14

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



#2 Lexxis

Lexxis

    Ecabosseur en fèves

  • Membre
  • PipPipPipPip
  • 404 messages

Posté 07 juillet 2017 - 15:02

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.



#3 Larme

Larme

    Broyeur de fèves

  • Artisan chocolatier
  • PipPipPipPipPipPip
  • 1 949 messages
  • LocationParis

Posté 07 juillet 2017 - 16:11

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.


Tant que vous avez des dents, mangez des pommes. Tant que vous avez de l'argent, croquez la Pomme.

#4 Joanna Carter

Joanna Carter

    Broyeur de fèves

  • Contrôleur d'arômes
  • 1 887 messages
  • LocationPlestin-les-Grèves (22)

Posté 07 juillet 2017 - 16:24

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


#5 Lexxis

Lexxis

    Ecabosseur en fèves

  • Membre
  • PipPipPipPip
  • 404 messages

Posté 07 juillet 2017 - 18:05

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.

#6 macphi

macphi

    Eleveur de cacaoyers

  • Membre
  • PipPip
  • 43 messages

Posté 08 juillet 2017 - 18:45

Merci beaucoup pour vos réponses, je suis en train de les digérer...

::)



#7 macphi

macphi

    Eleveur de cacaoyers

  • Membre
  • PipPip
  • 43 messages

Posté 11 juillet 2017 - 16:08

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)

        }
        
        
    }


#8 Joanna Carter

Joanna Carter

    Broyeur de fèves

  • Contrôleur d'arômes
  • 1 887 messages
  • LocationPlestin-les-Grèves (22)

Posté 11 juillet 2017 - 16:13

Pourquoi pas utiliser CoreData pour sauvegarder les données ?



#9 macphi

macphi

    Eleveur de cacaoyers

  • Membre
  • PipPip
  • 43 messages

Posté 11 juillet 2017 - 16:17

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 ?



#10 Larme

Larme

    Broyeur de fèves

  • Artisan chocolatier
  • PipPipPipPipPipPip
  • 1 949 messages
  • LocationParis

Posté 11 juillet 2017 - 16:25

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.


Tant que vous avez des dents, mangez des pommes. Tant que vous avez de l'argent, croquez la Pomme.

#11 Joanna Carter

Joanna Carter

    Broyeur de fèves

  • Contrôleur d'arômes
  • 1 887 messages
  • LocationPlestin-les-Grèves (22)

Posté 11 juillet 2017 - 17:28

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


#12 macphi

macphi

    Eleveur de cacaoyers

  • Membre
  • PipPip
  • 43 messages

Posté 11 juillet 2017 - 19:05

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

Merci !  :bravo!:

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



#13 Larme

Larme

    Broyeur de fèves

  • Artisan chocolatier
  • PipPipPipPipPipPip
  • 1 949 messages
  • LocationParis

Posté 11 juillet 2017 - 19:48

@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.


Tant que vous avez des dents, mangez des pommes. Tant que vous avez de l'argent, croquez la Pomme.

#14 Joanna Carter

Joanna Carter

    Broyeur de fèves

  • Contrôleur d'arômes
  • 1 887 messages
  • LocationPlestin-les-Grèves (22)

Posté 11 juillet 2017 - 20:16

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


#15 macphi

macphi

    Eleveur de cacaoyers

  • Membre
  • PipPip
  • 43 messages

Posté 11 juillet 2017 - 21:02

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 !

 

:snif:



#16 Draken

Draken

    Mouleur de chocolats

  • Artisan chocolatier
  • PipPipPipPipPipPipPipPip
  • 8 599 messages
  • LocationParis

Posté 11 juillet 2017 - 21:38

 

@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 Carter aime ceci

Garçon, servez-moi un Covfefe avec du lait de soja, sans OGM ..

Et faites régler la climatisation, il fait bien chaud, ici !

 

 

Éternel Novice !  :baby:

Tueur de poneys !  :(

 

Faire simple .. c'est compliqué !

Faire compliqué .. c'est simple !

 

Un MOOC (cours en ligne - dont je ne suis pas l'auteur) gratuit sur la programmation en Obj-C et en Swift 3, démarrant le 14 Mars 2017 :

https://www.edx.org/...onnex-progios1x

 

Des dizaines d'heures de tutoriels vidéo en français (je ne suis pas l'auteur) pour apprendre à développer en Obj-C et Swift : http://pagesperso-sy...don/5I452-2014/

 

 


#17 Joanna Carter

Joanna Carter

    Broyeur de fèves

  • Contrôleur d'arômes
  • 1 887 messages
  • LocationPlestin-les-Grèves (22)

Posté 11 juillet 2017 - 21:56

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


#18 Joanna Carter

Joanna Carter

    Broyeur de fèves

  • Contrôleur d'arômes
  • 1 887 messages
  • LocationPlestin-les-Grèves (22)

Posté 11 juillet 2017 - 22:08

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



#19 macphi

macphi

    Eleveur de cacaoyers

  • Membre
  • PipPip
  • 43 messages

Posté 12 juillet 2017 - 06:49

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

:clap:

 

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

 

Merci beaucoup !






0 utilisateur(s) li(sen)t ce sujet

0 membre(s), 0 invité(s), 0 utilisateur(s) anonyme(s)