14

@AppStorage was introduced in SwiftUI recently, it seems like an alternative to UserDefaults. I'm trying to make the @AppStorage able to store nested lists.

For simple cases, you would do

@AppStorage("selected") var selected = 0

I used this while dealing with normal UserDefaults:

@Published var list = UserDefaults.standard.array(forKey: "nestedList") as? [[String]] ?? [[String]]()

Long story short, how do I convert plain old UserDefuaults to the new property wrapper, @AppStorage?

pawello2222
  • 46,897
  • 22
  • 145
  • 209
Kelvin Jou
  • 271
  • 2
  • 12

3 Answers3

36

All you need is to conform Array to RawRepresentable using the extension from here:

extension Array: RawRepresentable where Element: Codable {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode([Element].self, from: data)
        else {
            return nil
        }
        self = result
    }

    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8)
        else {
            return "[]"
        }
        return result
    }
}

Here is a demo:

struct ContentView: View {
    @AppStorage("items") var items: [[String]] = [
        ["1", "2"],
        ["a", "b", "c"],
    ]

    var body: some View {
        VStack {
            Text("items: \(String(describing: items))")
            Button("Add item") {
                items[0].append(String(Int.random(in: 1...10)))
            }
        }
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
3

SwiftUI 2.0 (Xcode 12 - maybe will be changed in future)

The AppStorage wrapper does not support containers now, only Bool, Int, Double, String, URL, Data.

enter image description here

So the solution for your case either to continue use UserDefaults or to encode/decode your nested array into JSON Data and use AppStorage with Data.

Asperi
  • 228,894
  • 20
  • 464
  • 690
  • How do you use the standard user defaults in AppGroups? I’ve only done it in the AppStorage thing – Kelvin Jou Jul 30 '20 at 16:55
  • UserDefaults does not depend on selected Life-Cycle, or platform or language, it is part of Foundation framework and it works the same everywhere available. [Just search](https://stackoverflow.com/search?q=%5Bswiftui%5D+UserDefaults) – Asperi Jul 30 '20 at 17:08
3

You could use Data to achieve this:

class Storage: NSObject {
    
    static func archiveStringArray(object : [String]) -> Data {
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false)
            return data
        } catch {
            fatalError("Can't encode data: \(error)")
        }

    }

    static func loadStringArray(data: Data) -> [String] {
        do {
            guard let array = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [String] else {
                return []
            }
            return array
        } catch {
            fatalError("loadWStringArray - Can't encode data: \(error)")
        }
    }
}

struct TestView: View {
            
    @AppStorage("albums") var albums: Data = Data()
    
    var body: some View {
        TabView {
            VStack {
                Text("AppStorage - array String")
                List {
                    ForEach (getStrings(data: albums), id:\.self) { s in
                        Text(s)
                    }
                }
            }
                .tabItem {
                    Text("read only")
                }
            VStack {
                Text("AppStorage - array String")
                List {
                    ForEach (getStrings(data: albums), id:\.self) { s in
                        Text(s)
                    }
                }
                Button("add album") {
                    addAlbum()
                }
            }
                .tabItem {
                    Text("add album")
                }
        }
    }
    
    func getStrings(data: Data) -> [String] {
        return Storage.loadStringArray(data: data)
    }
    func addAlbum() {
        var tmpAlbums = getStrings(data: albums)
        
        tmpAlbums.append("Album # \(tmpAlbums.count)")
        
        albums = Storage.archiveStringArray(object: tmpAlbums)
    }
}

struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
    }
}
Simone Pistecchia
  • 2,746
  • 3
  • 18
  • 30