Explicitly Set Core Data Objects Still Carry Nil Value - ios

Working through Paul Hudson's "100 Days of SwiftUI" and I'm having an issue where data from a FetchRequest features nil values for some properties even after they've been set explicity.
The idea is fire off a request for a .json file, download it, and then parse it into some CoreData objects.
Here's the output:
And clicking on the top "Unknown" yields:
Now for some code:
ContentView
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var cachedUsers: FetchedResults<CachedUser>
#State private var users = [User]()
var body: some View {
NavigationView {
List(cachedUsers) { cachedUser in
NavigationLink {
VStack(alignment: .leading){
Text("Name: \(cachedUser.unwrappedName)")
.font(.largeTitle)
Spacer()
Section {
Text("Email:")
Text(cachedUser.uEmail)
.font(.body)
.foregroundColor(.secondary)
Text("Company")
Text(cachedUser.uCompany)
.font(.body)
.foregroundColor(.secondary)
Text("Address:")
Text(cachedUser.uAddress)
.font(.body)
.foregroundColor(.secondary)
} header: {
Text("Contact")
.font(.title)
}
Spacer()
Spacer()
}
} label: {
VStack(alignment: .leading){
Text("\(cachedUser.unwrappedName)")
.font(.headline)
.foregroundColor(cachedUser.isActive ? .white : .red)
}
}
}
.task {
if cachedUsers.isEmpty {
do {
if let retrievedUsers = try await loadData() {
users = retrievedUsers
}
} catch {
print(error)
}
await MainActor.run {
saveToCoreData(users: users)
}
}
printCache()
}
.navigationTitle("Users")
.foregroundColor(.white)
}
.navigationViewStyle(StackNavigationViewStyle())
}
func loadData() async throws -> [User]? {
if users.isEmpty {
let targetURL = "https://www.hackingwithswift.com/samples/friendface.json"
guard let url = URL(string: targetURL) else {
print("Bad URL. No biscuit.")
return users
}
let (data, _) = try await URLSession.shared.data(from: url)
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
users = try decoder.decode([User].self, from: data)
} catch {
print(error)
}
}
return users
}
func saveToCoreData(users: [User]){
for user in users {
let cd = CachedUser(context: moc)
cd.about = user.about
cd.address = user.address
cd.age = Int16(user.age)
cd.company = user.company
cd.email = user.email
cd.id = user.id
cd.isActive = user.isActive
cd.name = user.name
cd.registered = user.registered
for friend in user.friends {
let cf = CachedFriend(context: moc)
cf.id = friend.id
cf.name = friend.name
cd.addToFriend(cf)
}
do {
if moc.hasChanges {
try moc.save()
}
} catch {
print(error)
}
}
}
func printCache() {
for cachedUser in cachedUsers {
print("Name: \(cachedUser.unwrappedName) | \(cachedUser.uUUID) ")
}
}
}
User
import Foundation
struct User: Codable, Identifiable {
enum CodingKeys: CodingKey {
case id, isActive, name, age, company, email, address, about, registered, tags, friends
}
var id = UUID()
var isActive: Bool = false
var name: String = ""
var age: Int16 = 20
var company: String = ""
var email: String = ""
var address: String = ""
var about: String = ""
var registered: Date = Date.now
var tags: [String] = []
var friends: [Friend] = []
init() { }
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(isActive, forKey: .isActive)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
try container.encode(company, forKey: .company)
try container.encode(email, forKey: .email)
try container.encode(address, forKey: .address)
try container.encode(about,forKey: .about)
try container.encode(registered,forKey: .registered)
try container.encode(tags, forKey: .tags)
try container.encode(friends, forKey: .friends)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
isActive = try container.decode(Bool.self, forKey: .isActive)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int16.self, forKey: .age)
company = try container.decode(String.self, forKey: .company)
email = try container.decode(String.self, forKey: .email)
address = try container.decode(String.self, forKey: .address)
about = try container.decode(String.self, forKey: .about)
registered = try container.decode(Date.self, forKey: .registered)
tags = try container.decode([String].self, forKey: .tags)
friends = try container.decode([Friend].self, forKey: .friends)
}
}
class UserData: ObservableObject, Codable {
#Published var user = User()
enum CodingKeys: CodingKey {
case user
}
init() { }
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(user, forKey: .user)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
user = try container.decode(User.self, forKey: .user)
}
}
NOTE: The CachedUser extension handles name and email (for example) identically.
Core Data Entity and printCache() Result Screenshot
It doesn't make any sense at all how printCache (which is going through the FetchResults cachedUsers objects) can print names, but then the minute I go through that exact same fetch object only the names don't carry through ("Unknown" displays as a result of nil coelescing) , but all the other data (About section, email, company, address) somehow does. The CachedFriend names don't, but I suspect that's the same issue I'm having with the CachedUser object just downstream.

So I discovered that the users are getting added after all. I realized that when I went to scroll on all my 'Unknowns' if I scrolled down far enough (1000+ rows) I'd start to see names. I have no idea what I did, but at one point I was adding to CoreData without regard to replacement.
In the end I had to tweak loadData() a little to get this working and then delete a bunch of garbage data that had gone in during the process of development. The ContentView was the only place I was doing it wrong. All the other code was fine.
**UPDATED ContentView (Working) **
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: []) var cachedUsers: FetchedResults<CachedUser>
#State private var users = [User]()
var body: some View {
NavigationView {
List {
ForEach(cachedUsers) { cachedUser in
NavigationLink {
VStack(alignment: .leading){
//... same as above
} header: {
Text("Contact")
.font(.title)
}
Spacer()
Spacer()
}
} label: {
VStack(alignment: .leading){
Text("\(cachedUser.uName)")
.font(.headline)
.foregroundColor(cachedUser.isActive ? .white : .red)
}
}
}
.onDelete(perform: removeUser)
}
.task {
await loadData()
}
.toolbar {
EditButton()
}
.navigationTitle("Users")
.foregroundColor(.white)
}
.navigationViewStyle(StackNavigationViewStyle())
}
func removeUser(at offsets: IndexSet) {
for index in offsets {
//this hack helped me tremendously in cleaning up the CoreData object. I was adding 200 to the index at one point.
//for i in index...index + 10{
let user = cachedUsers[index]
moc.delete(user)
do {
try moc.save()
} catch {
print(error)
}
//}
}
}
func loadData() async {
if cachedUsers.isEmpty {
let targetURL = "https://www.hackingwithswift.com/samples/friendface.json"
guard let url = URL(string: targetURL) else {
print("Bad URL. No biscuit.")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
users = try decoder.decode([User].self, from: data)
await MainActor.run {
saveToCoreData(users: users)
}
} catch {
print(error)
}
}
}
func saveToCoreData(users: [User]){
for user in users {
let cd = CachedUser(context: moc)
cd.about = user.about
cd.address = user.address
cd.age = Int16(user.age)
cd.company = user.company
cd.email = user.email
cd.id = user.id
cd.isActive = user.isActive
cd.name = user.name
cd.registered = user.registered
for friend in user.friends {
let cf = CachedFriend(context: moc)
cf.id = friend.id
cf.name = friend.name
cd.addToFriend(cf)
}
do {
if moc.hasChanges {
try moc.save()
}
} catch {
print(error)
}
}
}
}

Related

SwiftUI Navigation - List loading multiple time after navigating from details

I am creating a SwiftUI List with Details.
This list is fetching JSON data from Firebase Realtime. The data consist of 5 birds with an ID, a name and an image URL.
My problem is the following:
Each time I click on the back button after I navigate to details, the data get doubled every single time, what am I doing wrong? (see screenshots).
I am using MVVM design pattern, I am listening and removing that listener every time the View appears and disappears.
Please, find the code below:
Main View:
var body: some View {
NavigationStack {
List(viewModel.birds) { bird in
NavigationLink(destination: DetailsView(bird: bird)) {
HStack {
VStack(alignment: .leading) {
Text(bird.name).font(.title3).bold()
}
Spacer()
AsyncImage(url: URL(string: bird.imageURL)) { phase in
switch phase {
// downloading image here
}
}
}
}
}.onAppear {
viewModel.listentoRealtimeDatabase()
}
.onDisappear {
viewModel.stopListening()
}.navigationTitle("Birds")
}
}
DetailsView:
struct DetailsView: View {
var bird: Bird
var body: some View {
Text("\(bird.name)")
}
}
Model:
struct Bird: Identifiable, Codable {
var id: String
var name: String
var imageURL: String
}
View Model:
final class BirdViewModel: ObservableObject {
#Published var birds: [Bird] = []
private lazy var databasePath: DatabaseReference? = {
let ref = Database.database().reference().child("birds")
return ref
}()
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
func listentoRealtimeDatabase() {
guard let databasePath = databasePath else {
return
}
databasePath
.observe(.childAdded) { [weak self] snapshot in
guard
let self = self,
var json = snapshot.value as? [String: Any]
else {
return
}
json["id"] = snapshot.key
do {
let birdData = try JSONSerialization.data(withJSONObject: json)
let bird = try self.decoder.decode(Bird.self, from: birdData)
self.birds.append(bird)
} catch {
print("an error occurred", error)
}
}
}
func stopListening() {
databasePath?.removeAllObservers()
}
}
screenshot how it should be

How to highlight text of offline JSON data that are shown in SwiftUI's Disclosure group/ or Detail view?

What we have:
We are currently passing the data of book(json file saved in document directory) shown in swiftUI's Disclosure group. old book data is stored in core-data and shown in UIKit's WKWebView.
Scenario:
Goal:
option 1: We want to implement the Search function that can search specific words and highlight it and then show in the small scroll view which can navigate the user to specific place in the shown data.
Below is the Search option that has been implemented in core-data that can search the specific word or term that lets user navigate to specific page. But, unfortunately it is in UIKit and is for old books(core-data) only. While, I want to implement such feature for SwiftUI.
Option 2: We want to implement the search function in the final detail view of the SwiftUI chapters where the stories(data)(attributed/html text) of the book can be read. Here the user should be able to search specific words in various languages. Now the words should be highlighted and can user is able to navigate to the exact location of the highlighted word in the page along with the total words that match the search and page count.
Below is the detail view where the search can be implemented.
What i have tried:
I have implemented the demo SwiftUI search
** Code:**
Below is the code where the Book structure:
import Foundation
import SwiftUI
import UIKit
struct ContentView: View {
#EnvironmentObject var booksList: BooksList
#State var books: [BookModel] = []
#State var selection: BookModel?
var body: some View {
// NavigationView {
VStack(alignment: .trailing, spacing: 40) {
ScrollView(.vertical, showsIndicators: false) {
ForEach(booksList.books){ book in
//Alternative way , each book on seperate view
//NavigationLink(destination: lvl4(books: [book], selection: nil)){
// Text(book.bukTitle!)
//
if #available(iOS 15.0, *) {
if #available(iOS 15, *){
// label: do {
//
// }
// label: do {
// AsyncImage(url: URL(string: "\(book.coverImage!)")) .scaledToFit() .fixedSize(horizontal:true, vertical: true) .frame(minWidth: 10, maxWidth: 20)
// }
}
DisclosureGroup( "\(Text(book.bukTitle!) .fontWeight(.medium) .font(.system(size: 30)))"
) {
ForEach(book.bookContent ?? []) { bookContent in
DisclosureGroup(
"\(Text(bookContent.title).fontWeight(.medium) .font(.system(size: 25)))"
) {
OutlineGroup(bookContent.child, children: \.child) { item in
NavigationLink {
if #available(iOS 15, *) {
ScrollView {
Text(attributedString(from: item.title, font: Font.system(size: 25)))
.padding(30).lineSpacing(20).navigationTitle(Text(bookContent.title))
.navigationBarTitleDisplayMode(.inline)
}.frame(minWidth: 0, maxWidth: .infinity)
.background(Color(UIColor.hexStringToUIColor(hexStr: ("efe8d2"))))
}
} label: {
if #available(iOS 15, *) {
Text(
"\(Text(attributedString(from: item.title, font: Font.system(size: 23) )))"
)
.lineLimit(2).lineSpacing(15) .padding(15)
} else {
// Fallback on earlier versions
}
}
.lineSpacing(15) .disabled(item.child != nil)
}
.lineSpacing(20) .padding() }
}.lineSpacing(20) .padding()
}.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(lineWidth: 2)
}
}
}
}
}.padding(10)
}
VBBookManager.Swift including Book Model code::
//
// VBBooksManager.swift
// VadtaldhamBooks
//
import Foundation
enum BookParseError: Error {
case bookParsingFailed
}
struct BookModelForJSONConversion: Codable {
var id:Int
var title: String?
var content: [BookContent]?
var bookCoverImage:String?
func convertToJsonString()->String?{
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
var encodedString:String?
do {
let encodePerson = try jsonEncoder.encode(self)
let endcodeStringPerson = String(data: encodePerson, encoding: .utf8)!
//print(endcodeStringPerson)
encodedString = endcodeStringPerson
} catch {
print(error.localizedDescription)
return nil
}
return encodedString
}
}
struct BookModel: Identifiable, Codable {
var id:Int
var bukTitle: String?
var isLive: Bool?
var userCanCopy: Bool?
var bookContent: [BookContent]?
var coverImage:String?
enum CodingKeys: String, CodingKey {
case id = "id"
case bukTitle = "title"
case isLive = "is_live"
case userCanCopy = "user_can_copy"
case bookContent = "content"
case coverImage = "bookCoverImage"
}
}
struct BookContent: Identifiable, Codable {
let id = UUID()
var title, type: String
var child: [Child]
}
struct Child: Identifiable, Codable {
let id = UUID()
var title, type: String
var child: [Child]?
}
enum BooksDirectory {
/// Default, system Documents directory, for persisting media files for upload.
case downloads
/// Returns the directory URL for the directory type.
///
fileprivate var url: URL {
let fileManager = FileManager.default
// Get a parent directory, based on the type.
let parentDirectory: URL
switch self {
case .downloads:
parentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
}
return parentDirectory.appendingPathComponent(VBBooksManager.booksDirectoryName, isDirectory: true)
}
}
class VBBooksManager:NSObject {
fileprivate static let booksDirectoryName = "books"
let directory: BooksDirectory
#objc (defaultManager)
static let `default`: VBBooksManager = {
return VBBooksManager()
}()
// MARK: - Init
/// Init with default directory of .uploads.
///
/// - Note: This is particularly because the original Media directory was in the NSFileManager's documents directory.
/// We shouldn't change this default directory lightly as older versions of the app may rely on Media files being in
/// the documents directory for upload.
///
init(directory: BooksDirectory = .downloads) {
self.directory = directory
}
// MARK: - Instance methods
/// Returns filesystem URL for the local Media directory.
///
#objc func directoryURL() throws -> URL {
let fileManager = FileManager.default
let mediaDirectory = directory.url
// Check whether or not the file path exists for the Media directory.
// If the filepath does not exist, or if the filepath does exist but it is not a directory, try creating the directory.
// Note: This way, if unexpectedly a file exists but it is not a dir, an error will throw when trying to create the dir.
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: mediaDirectory.path, isDirectory: &isDirectory) == false || isDirectory.boolValue == false {
try fileManager.createDirectory(at: mediaDirectory, withIntermediateDirectories: true, attributes: nil)
}
return mediaDirectory
}
func saveBook(bookName:String,bookData:String)->Error?{
//TODO: Save book into Document directory
do {
var finalBookName = bookName
if !finalBookName.contains(".json"){
finalBookName = "\(bookName).json"
}
let bookPath = try? self.directoryURL().appendingPathComponent(finalBookName)
print(bookPath?.relativePath)
do {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: bookPath!.relativePath){
try fileManager.removeItem(at: bookPath!)
}
let data = Data(bookData.utf8)
try? data.write(to: bookPath!, options: .atomic)
//Just for Testing purpose call load book
// let bookModel = try loadBookFromDocumentDirectory(bookName: finalBookName)
// print(bookModel?.coverImage)
}
catch let error as NSError {
print(error)
return error
}
}
catch let error as NSError{
print(error)
return error
}
return nil
//fileManager.wri wr(bookPath.relativePath, contents: Data(bookData), attributes: nil)
}
//https://stackoverflow.com/questions/39415249/best-practice-for-swift-methods-that-can-return-or-error
func loadBookFromDocumentDirectory(bookName:String) throws -> BookModel? {
let fileManager = FileManager.default
do {
var finalBookName = bookName
if !finalBookName.contains(".json"){
finalBookName = "\(bookName).json"
}
let bookPath = try? self.directoryURL().appendingPathComponent(finalBookName)
print(bookPath?.relativePath)
do {
if fileManager.fileExists(atPath: bookPath!.relativePath){
let jsonBookString = fileManager.contents(atPath: bookPath!.relativePath)
do {
let data = try Data(jsonBookString!)
guard let parsedBookObject:BookModel? = try JSONDecoder().decode(BookModel.self, from: data) else {
throw BookParseError.bookParsingFailed
}
return parsedBookObject ?? nil
//print(parsedBookObject)
}
catch let error as NSError{
print("error: \(error)")
throw error
}
}else{
}
}
catch let error as NSError {
print(error)
throw error
}
}
catch let error as NSError{
print(error)
throw error
}
return nil
}
func loadAllSavedBooks()->[BookModel]?{
var allBooks:[BookModel] = []
let fileManager = FileManager.default
guard let booksPath = try? self.directoryURL() else {
return []
}
print(booksPath)
do {
// Get the directory contents urls (including subfolders urls)
let directoryContents = try fileManager.contentsOfDirectory(at: booksPath, includingPropertiesForKeys: nil)
print(directoryContents)
// if you want to filter the directory contents you can do like this:
let books = directoryContents.filter{ $0.pathExtension == "json" }
let bookNames = books.map{ $0.deletingPathExtension().lastPathComponent }
print("bookNames list:", bookNames)
//TODO: Load all the books and send array back
for bookName in bookNames {
do {
let book = try loadBookFromDocumentDirectory(bookName:bookName)
allBooks.append(book!)
} catch BookParseError.bookParsingFailed {
continue
}
}
return allBooks
} catch let error as NSError {
print(error)
}
return allBooks
}
func isBookAlreadyExists (bookName:String)->Bool {
return try! (loadBookFromDocumentDirectory(bookName: bookName) != nil)
}
func deleteBook (bookName:String)->Bool {
// guard let book = try! loadBookFromDocumentDirectory(bookName: bookName) else {
// return false
// }
do {
var finalBookName = bookName
if !finalBookName.contains(".json"){
finalBookName = "\(bookName).json"
}
let bookPath = try? self.directoryURL().appendingPathComponent(finalBookName)
// print(bookPath?.relativePath)
do {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: bookPath!.relativePath){
try fileManager.removeItem(at: bookPath!)
return true
}
}
catch let error as NSError {
print(error)
return false
}
}
catch let error as NSError{
print(error)
return false
}
return false
}
}

Trying to save data in UserDefaults and show them in list view

Trying to save some data in UserDefaults but I'm getting nil in the view.
I don't know where is the problem
This is my code in ContentView:
var saveButton: some View {
Button("Save Meal") {
let meal = Meal(name: self.mealGenerator.currentMeal!.name,
imageUrlString: self.mealGenerator.currentMeal!.imageUrlString,
ingredients: self.mealGenerator.currentMeal!.ingredients,
instructions: self.mealGenerator.currentMeal!.instructions,
area: self.mealGenerator.currentMeal!.area,
category: self.mealGenerator.currentMeal!.category)
self.savedMeals.meals.append(meal)
self.savedMeals.saveMeals()
}
This is my class I'm trying to save:
class SavedMeals: ObservableObject {
#Published var meals: [Meal]
func saveMeals() {
if let encoded = try? JSONEncoder().encode(meals) {
UserDefaults.standard.set(encoded, forKey: "Meals")
}
}
init() {
if let meals = UserDefaults.standard.data(forKey: "Meals") {
if let decoded = try? JSONDecoder().decode([Meal].self, from: meals) {
self.meals = decoded
return
}
}
self.meals = []
}
}
And I'm trying to list in a view:
struct SavedMealsView: View {
#ObservedObject var savedMeals: SavedMeals
var body: some View {
NavigationView {
List(savedMeals.meals) { meal in
Text(meal.name)
}
.navigationBarTitle("Saved Meals", displayMode: .inline)
}
}
}
You do meals = [] at the end of your init regardless of what you decode. Perhaps this will work better:
init() {
if data = UserDefaults.standard.data(forKey: "Meals") {
do {
meals = try JSONDecoder().decode([Meal].self, from: meals)
} catch {
assertionFailure("Oops!")
meals = []
}
} else {
meals = []
}
}

SwiftUI: How to set UserDefaults first time view renders?

So I have this code, where I fetch a url from firestore and then append it to an array, which is then stored in userDefaults(temporarily).
In the view I basically just iterate over the array stored in userdefaults and display the images.
But the problem is, that I have to rerender the view before the images show.
How can i fix this?
struct PostedImagesView: View {
#State var imagesUrls : [String] = []
#ObservedObject var postedImagesUrls = ProfileImages()
var body: some View {
VStack{
ScrollView{
ForEach(postedImagesUrls.postedImagesUrl, id: \.self) { url in
ImageWithURL(url)
}
}
}
.onAppear{
GetImage()
print("RAN GETIMAGE()")
}
}
// Get Img Url from Cloud Firestore
func GetImage() {
guard let userID = Auth.auth().currentUser?.uid else { return }
let db = Firestore.firestore()
db.collection("Users").document(userID).collection("PostedImages").document("ImageTwoTester").getDocument { (document, error) in
if let document = document, document.exists {
// Extracts the value of the "Url" Field
let imageUrl = document.get("Url") as? String
UserDefaults.standard.set([], forKey: "postedImagesUrls")
imagesUrls.append(imageUrl!)
UserDefaults.standard.set(imagesUrls, forKey: "postedImagesUrls")
} else {
print(error!.localizedDescription)
}
}
}
}

Userdefaults can not retrieve custom Struct in Share Extension

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.
I had the same problem but with another type of extension. Hope it works for you too.
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 {
}
}
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)
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
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

Resources