How to use @AppStorage for a Dictionary of Strings [String: String]

2.6k views Asked by At

Hey in my app I am trying to store whether the User wants a location to be its favourite. In order to make the change in this UserDefaults update the view I need to use @AppStorage. However, at the moment I only get the ERROR "No exact matches in call to initializer" Do i need to use Data instead of the [String: String], if yes how? Does anyone know how to fix this? Thanks in advance

import SwiftUI

struct SpotDetailView: View {
    @State var spot: WindApp //knows which spot it's at
    var spotname:String
    //@State var favourites: [String:String] = UserDefaults.standard.object(forKey: "heart") as? [String:String] ?? [:]
    @AppStorage("heart") var favourites: [String: String] = [:]

        
    var body: some View {
        
        ZStack {
            
            List {
                SpotMapView(coordinate: spot.locationCoordinate)
                    .cornerRadius(8)
                ForEach(0..<9) { i in
                    SingleDayView(spot: spot, counter: (i * 8))
                    }
            }
            .navigationTitle("\(spot.spot)")
            VStack {
                Spacer()
                HStack {
                    Spacer()
                    Button(action: {
                        if favourites[spot.spot] == "heart" {
                            favourites[spot.spot] = "heart.fill"
                            UserDefaults.standard.set(favourites, forKey: "heart")
                        }
                        else if favourites[spot.spot] == "heart.fill" {
                            favourites[spot.spot] = "heart"
                            UserDefaults.standard.set(favourites, forKey: "heart")
                        }
                        else {
                            favourites[spot.spot] = "heart.fill"
                            UserDefaults.standard.set(favourites, forKey: "heart")
                        }
                
                    }, label: {
                        Image(systemName: favourites[spot.spot] ?? "heart")
                    })
                    .frame(width: 20, height: 20)
                }
            }
        }
    }
}
3

There are 3 answers

0
Andrew Stoddart On

You will have to use Data. @AppStorage currently only supports:

  • Int
  • Double
  • String
  • Bool
  • URL
  • Data

See this answer for sample implementation and further details

2
awct On

You can make Dictionary [String:String] conform to RawRepresentable.

extension Dictionary: RawRepresentable where Key == String, Value == String {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),  // convert from String to Data
            let result = try? JSONDecoder().decode([String:String].self, from: data)
        else {
            return nil
        }
        self = result
    }

    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),   // data is  Data type
              let result = String(data: data, encoding: .utf8) // coerce NSData to String
        else {
            return "{}"  // empty Dictionary resprenseted as String
        }
        return result
    }

}
0
LemonandlIme On

I would recomend storing your [String: String] object as Data.

Since [String: String] conforms to Codable out of the box this is an easy task.

This is an example of how you could implement it and also get Combine support. Notice that MyUserDefaultsStorage is not only for this value but you can add additional values you would like to store in AppStorage.

struct MyUserDefaultsStorage {

    var favorites: CurrentValueSubject<[String: String], Never> = .init([:])

    @AppStorage("stars")
    private var favoritesData = Data()

    private let decoder = JSONDecoder()
    private let encoder = JSONEncoder()

    private var cancellables = Set<AnyCancellable>()

    init() {
        do {
            favorites.value = try decoder.decode([String: String].self, from: favoritesData)
        } catch {
            print("No saved data")
        }

        favorites
            .receive(on: RunLoop.main)
            .removeDuplicates()
            .encode(encoder: encoder)
            .replaceError(with: Data())
            .assign(to: \.favoritesData, on: self)
            .store(in: &cancellables)
    }
}