I am trying to set a variable as a UserDefault so that when a user opens this app (that displays a list of questions), they get whatever "Pack" they had last selected when they were in the app last time.
The Pack is determined by this "defaultPack" variable that sets to UserDefaults, and that pack's data shows up on the launch screen. There is a menu where a user can select another pack, and the link there has a @Binding variable that updates the pack in the home screen, which should go back to the home screen and then update the UserDefaults as well.
It appears the defaultPack variable is being set correctly, but UserDefaults must not be being set, because whenever I reopen the app, it resets to the original default "Pack" rather than the one the user last selected.
Any ideas on what I need to change to have this update UserDefaults properly?
It's a custom data type, so I am using an extension of UserDefaults that encodes/decodes that value for storage.
Here is my class with the defaultPack variable:
final class UserInformation: ObservableObject {
@Published var defaultPack: Pack {
didSet {
do {
try UserDefaults.standard.setObject(defaultPack, forKey: "defaultPack")
} catch {
print(error.localizedDescription)
}
}
}
init() {
self.defaultPack = UserDefaults.standard.object(forKey: "defaultPack") as? Pack ?? samplePack
}
}
Here is the extension of UserDefaults that has encodable/decodable instructions for UserDefaults
//extension to UserDefaults to include the encodable/decodable instructions for UserDefaults
extension UserDefaults: ObjectSavable {
func setObject<Object>(_ object: Object, forKey: String) throws where Object: Encodable {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(object)
set(data, forKey: forKey)
} catch {
throw ObjectSavableError.unableToEncode
}
}
func getObject<Object>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable {
guard let data = data(forKey: forKey) else { throw ObjectSavableError.noValue }
let decoder = JSONDecoder()
do {
let object = try decoder.decode(type, from: data)
return object
} catch {
throw ObjectSavableError.unableToDecode
}
}
}
Here is my View that grabs the default Pack:
struct PackTopics: View {
@ObservedObject var userInformation: UserInformation
@State var showPacksList: Bool = false
var packsButton: some View {
Button(action: { self.showPacksList.toggle()
}) {
Image(systemName: "tray.full")
.imageScale(.large)
.accessibility(label: Text("Show Packs List"))
.padding()
}
}
var body: some View {
NavigationView {
List {
ForEach(userInformation.defaultPack.sections, id: \.self) { section in
Section(header: SectionHeader(linkedSection: section))
{
ForEach (section.topics, id: \.self) { topic in
TopicLine(linkedTopic: topic)
}
}
}
}
.navigationBarTitle(Text(userInformation.defaultPack.name))
.navigationBarItems(trailing: packsButton)
.sheet(isPresented: $showPacksList) {
PackPage(isPresented: self.$showPacksList, newPack: self.$userInformation.defaultPack)
}
}
}
}
And here is the view that has binding to the pack variable in the previous view, and should update the defaultPack:
struct PackPage: View {
@Binding var isPresented: Bool
@Binding var newPack: Pack
let userDefaults = UserDefaults.standard
var body: some View {
VStack {
HStack {
Text("Question Packs")
.font(.largeTitle)
.fontWeight(.bold)
.padding()
Spacer()
}
Text("Available Packs")
List {
HStack(alignment: .top) {
ForEach(purchasedPacks, id: \.self) { pack in
UnlockedPackTile(tilePack: pack)
.onTapGesture {
print("Originally tapped \(pack.name)")
self.newPack = pack
do {
let userDefaultPack = try self.userDefaults.getObject(forKey: "defaultPack", castTo: Pack.self)
print("Default pack is now set to \(userDefaultPack)")
} catch {
print(error.localizedDescription)
}
self.isPresented.toggle()
}
}
}
}
Adding the output *
2020-06-30 07:04:06.608783-0500 Travel Conversation Starters[2524:131054] [Agent] Received remote injection
2020-06-30 07:04:06.609182-0500 Travel Conversation Starters[2524:131054] [Agent] Create remote injection Mach transport: 6000023ca0d0
2020-06-30 07:04:06.609622-0500 Travel Conversation Starters[2524:131000] [Agent] No global connection handler, using shared user agent
2020-06-30 07:04:06.609840-0500 Travel Conversation Starters[2524:131000] [Agent] Received connection, creating agent
2020-06-30 07:04:07.916795-0500 Travel Conversation Starters[2524:131000] [Agent] Received message: < DTXMessage 0x600002cc4c00 : i2.0e c0 object:(__NSDictionaryI*) {
"products" : <NSArray 0x600000acc200 | 1 objects>
"id" : [0]
"scaleFactorHint" : [2]
"providerName" : "28Travel_Conversation_Starters20ContentView_PreviewsV"
"updates" : <NSArray 0x7fff8062d430 | 0 objects>
} > {
"serviceCommand" : "forwardMessage"
"type" : "display"
}
2020-06-30 07:04:16.447421-0500 Travel Conversation Starters[2524:131000] [Common] _BSMachError: port 9f2f; (os/kern) invalid capability (0x14) "Unable to insert COPY_SEND"
**Originally tapped Family Topics**
**Default pack is now set to Pack(id: 3541AF19-8CDB-4D9B-B261-7194DDD8516E, name: "Family Topics")**
some time ago a ran into a similar error. I did not have any errors on my serialization to Userdefault but there were a bug in the @Published and the didSet property observer. Are you really sure that your didSet is called. In my case when I changed my variable it did not trigger the didSet property observer. I finally ended up using a custom binding as described here :
https://www.hackingwithswift.com/books/ios-swiftui/creating-custom-bindings-in-swiftui.
You could also try to copy your object into a temp variable and reset it back again to see if the didSet is called.
I am aware that it does sounds like a workaround rather than a proper solution. But it is a start to understand what it is happening here.