2

I have an App and also a Share Extension. Between them I share data via UserDefaults. But it stopped working all of a sudden. Only bools or Strings can now be retrieved inside the Share Extension but when trying to retrieve a Custom Struct it is always returning nil.

Custom Struct getter/setter in UserDefaults:

//MARK: dataSourceArray
func setDataSourceArray(data: [Wishlist]?){
    set(try? PropertyListEncoder().encode(data), forKey: Keys.dataSourceKey)
    synchronize()
}


func getDataSourceArray() -> [Wishlist]? {
    if let data = self.value(forKey: Keys.dataSourceKey) as? Data {
        do {
            _ = try PropertyListDecoder().decode(Array < Wishlist > .self, from: data) as [Wishlist]
        } catch let error {
            print(error)
        }
        if let dataSourceArray =
            try? PropertyListDecoder().decode(Array < Wishlist > .self, from: data) as[Wishlist] {
                return dataSourceArray
            } 
    }
    return nil
}

I am calling it like this inside my Extension as well as in my Main App:

   if let defaults = UserDefaults(suiteName: UserDefaults.Keys.groupKey) {
        if let data = defaults.getDataSourceArray() {
            print("working")
        } else {
            print("error getting datasourceArray")
        }
    }

This is printing "working" in the Main App but "error getting datasourceArray" in my Extension. I don't understand the issue, especially because simple Bool-Getter are working also from my Share Extension, the issue is only with the Custom Struct.

What am I missing here?

Wishlist Struct:

import UIKit

enum PublicState: String, Codable {
    case PUBLIC
    case PUBLIC_FOR_FRIENDS
    case NOT_PUBLIC
}

struct Wishlist: Codable {
    var id: String
    var name: String
    var image: UIImage
    var wishes: [Wish]
    var color: UIColor
    var textColor: UIColor
    var index: Int
    var publicSate: PublicState

    enum CodingKeys: String, CodingKey {
        case id, name, image, wishData, color, textColor, index, isPublic, isPublicForFriends, publicSate
    }

    init(id: String, name: String, image: UIImage, wishes: [Wish], color: UIColor, textColor: UIColor, index: Int, publicSate: PublicState) {
        self.id = id
        self.name = name
        self.image = image
        self.wishes = wishes
        self.color = color
        self.textColor = textColor
        self.index = index
        self.publicSate = publicSate
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(String.self, forKey: .id)
        name = try values.decode(String.self, forKey: .name)
        wishes = try values.decode([Wish].self, forKey: .wishData)
        color = try values.decode(Color.self, forKey: .color).uiColor
        textColor = try values.decode(Color.self, forKey: .textColor).uiColor
        index = try values.decode(Int.self, forKey: .index)
        publicSate = try values.decode(PublicState.self, forKey: .publicSate)

        let data = try values.decode(Data.self, forKey: .image)
        guard let image = UIImage(data: data) else {
            throw DecodingError.dataCorruptedError(forKey: .image, in: values, debugDescription: "Invalid image data")
        }
        self.image = image
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
        try container.encode(wishes, forKey: .wishData)
        try container.encode(Color(uiColor: color), forKey: .color)
        try container.encode(Color(uiColor: textColor), forKey: .textColor)
        try container.encode(index, forKey: .index)
        try container.encode(image.pngData(), forKey: .image)
        try container.encode(publicSate, forKey: .publicSate)
    }
}

Update

This is the part where it fails:

if let data = self.value(forKey: Keys.dataSourceKey) as? Data

Is there any way to catch an error?

I also found out that this feature is actually working for other users. The app is live: https://apps.apple.com/de/app/wishlists-einfach-w%C3%BCnschen/id1503912334

But it is not working for me? I deinstalled the app, downloaded it from the App Store but it is still not working.

Chris
  • 1,828
  • 6
  • 40
  • 108
  • 1
    Unrelated but why do you decode the property list twice? Make the method `throw` then this is sufficient: `func getDataSourceArray() throws -> [Wishlist] { guard let data = self.data(forKey: Keys.dataSourceKey) else {return [] } return try PropertyListDecoder().decode([Wishlist].self, from: data) }` – vadian Dec 16 '20 at 12:02
  • @vadian good point thanks, but like you said, unrelated to the main issue here :( – Chris Dec 16 '20 at 12:07

2 Answers2

0

I had the same problem but with another type of extension. Hope it works for you too.

  1. Create a file you share between the two targets and put the following code there:
//MARK: - Model
struct WishlistStruct: Codable {
//your wishlist struct, I'll assume you'll have a name and some items
  var name : String
    var items : [String]
}
typealias Wishlist = WishlistStruct

//MARK: - Defaults
let sharedUserdefaults = UserDefaults(suiteName: SharedDefault.suitName)
struct SharedDefault {
    static let suitName = "yourAppGroupHere"
    
    struct Keys{
        static let WishlistKey = "WishlistKey"
       
    }
}

var myWishlist: [Wishlist] {
   get {
    if let data = sharedUserdefaults?.data(forKey: SharedDefault.Keys.WishlistKey) {
            let array = try! PropertyListDecoder().decode([Wishlist].self, from: data)
        return array
    } else{
        //Here you should return an error but I didn't find any way to do that so I put this code which hopefully will never be executed
    return sharedUserdefaults?.array(forKey: SharedDefault.Keys.WishlistKey) as? [Wishlist] ?? [Wishlist]()
    }
   } set {
       
   }
}
  1. Now, whenever you need to retrieve the struct, both in app and extension, use the following code:
 var wishlist  : [Wishlist] = []
 var currentWishlist = myWishlist

//In your viewDidLoad call
wishlist.append(contentsOf: myWishlist)

  1. To edit the data inside of your wishlist use the following code
 wishlist.append(Wishlist(name: "wishlist", items: ["aaa","bbb","ccc"]))
        currentWishlist.append(Wishlist(name: "wishlist", items: items: ["aaa","bbb","ccc"]))
        if let data = try? PropertyListEncoder().encode(currentWishlist) {
            sharedUserdefaults?.set(data, forKey: SharedDefault.Keys.WishlistKey)
        }

Let me know if you need more clarifications

  • I will give that a try! But do you have any idea why the problem occurs??? – Chris Dec 16 '20 at 21:57
  • There's an error in the way you retrieve the data. You're using value for key and then you cast the result to Data. Try using data for key instead. The code inside the `myWishlist` variable I put in my answer should help you out – Federica Benacquista Dec 17 '20 at 10:12
  • I don't think that's it. I tried changing my `getDataSourceArray` to this: `if let data = data(forKey: Keys.dataSourceKey) { let array = try! PropertyListDecoder().decode([Wishlist].self, from: data) return array } else { print(":(") }` , but same result – Chris Dec 17 '20 at 11:17
  • not yet, but I will. Kind of busy at the moment with the project and I don't see the main difference between yours and mine? – Chris Dec 17 '20 at 21:57
  • The difference is that with my code you'll be able to retrieve the struct. – Federica Benacquista Dec 20 '20 at 12:34
  • when calling `wishlist.append(contentsOf: myWishlist)` , myWishlist is `nil` ... Do you maybe have a wrapper class for this? I tried making one but it is always failing... I really don't understand what is going on with your code and why yours should work, but mine not :( – Chris Dec 21 '20 at 20:08
  • Yep, my bad, I made a stupid edit to my answer. `var myWishlist: [Wishlist]` shouldn't be optional. I rolled back my edit, you just need to copy and paste that var and it will work just fine – Federica Benacquista Dec 21 '20 at 20:20
0

Updated code to your struct. You should change some type of properties to yours(i remove some field for test).

import UIKit

enum PublicState: String, Codable {
    case PUBLIC
    case PUBLIC_FOR_FRIENDS
    case NOT_PUBLIC
}

struct Wishlist: Codable {
    var id: String = ""
    var name: String = ""
    var image: Data = Data()//TODO: use Data type
    var color: String = ""//TODO: change it to your class
//    var wish: //TODO: add this filed, i don't have it
    var textColor: String = "" //TODO: change it to your class
    var index: Int = 0
    var publicSate: PublicState = .PUBLIC

    enum CodingKeys: String, CodingKey {
        case id, name, image, color, textColor, index, publicSate
    }

    init() {}
    
    init(id: String, name: String, image: Data, color: String, textColor: String, index: Int, publicSate: PublicState) {
        self.id = id
        self.name = name
        self.image = image
        self.color = color
        self.textColor = textColor
        self.index = index
        self.publicSate = publicSate
    }
}

struct WishlistContainer: Codable {
    var list: [Wishlist] = []

    enum CodingKeys: String, CodingKey {
        case list
    }
}


class UserDefaultsManager {

//be sure your correctly setup your app groups
private var currentDefaults: UserDefaults = UserDefaults(suiteName: "put here your app group ID")!

private func getFromLocalStorage<T: Codable>(model: T.Type, key: String) -> T? {
    
    if let decoded = currentDefaults.object(forKey: key) as? String {
        
        guard let data = decoded.data(using: .utf8) else { return nil }
        
        if let product = try? JSONDecoder().decode(model.self, from: data) {
            return product
        }
    }
    
    return nil
}

private func saveToLocalStorage(key: String, encodedData: String) {
    currentDefaults.set(encodedData, forKey: key)
}

private func removeObject(key: String) {
    currentDefaults.removeObject(forKey: key)
}

var wishList: WishlistContainer? {
    set {
        guard let value = newValue else {
            removeObject(key: "wishList")
            return
        }
        
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        
        guard let jsonData = try? encoder.encode(value) else { return }
        
        guard let jsonString = String(data: jsonData, encoding: .utf8) else { return }
        
        saveToLocalStorage(key: "wishList", encodedData: jsonString)
    }
    get {
        guard let value = getFromLocalStorage(model: WishlistContainer.self, key: "wishList") else {
            return nil
        }
        
        return value
    }
  }
}

//MARK: - Usage
let list: [Wishlist] = [Wishlist()]
let container: WishlistContainer = WishlistContainer(list: list)
UserDefaultsManager().wishList = container //set
UserDefaultsManager().wishList // get
isHidden
  • 860
  • 5
  • 16
  • are you sure? why is it working inside the main app then? I think you can store them if they are `Codable` and `Wishlists` confirms to `codable` . – Chris Dec 18 '20 at 10:19
  • If your share works correct for primitive types, but doesn't work for custom structs. This code should help. – isHidden Dec 18 '20 at 10:33
  • I will give it a try, but I don't quite get it because as I said in the quesiton, it is actually working for others – Chris Dec 18 '20 at 12:54
  • I think I have to change `standard` to my `suitName` don't I? – Chris Dec 18 '20 at 12:55
  • and could you give an example how I would use the code with my example? – Chris Dec 18 '20 at 12:58
  • yes, it should be changed to your suitName. Be sure, that you correctly set the suitName and added app groups to your project. Try to set your array to object. I mean use not a [Wishlist], but struct Container: Codable { let data: [Wishlist] }. https://stackoverflow.com/questions/45607903/sharing-userdefaults-between-extensions/45608095 – isHidden Dec 21 '20 at 13:18
  • not working. auth needs to conform to `codable` . what is `authData` anyway? – Chris Dec 21 '20 at 20:09
  • you should replace authData to Container that i described in previous comment. This code is like working example and can be changed to your task – isHidden Dec 22 '20 at 11:52
  • I am struggling to change it to my desire :( I updated my question with the `Wishlist Struct`. Can you help me out here? – Chris Dec 23 '20 at 13:37