Skip to content

Instantly share code, notes, and snippets.

@lcs-rgordon
Last active September 12, 2021 18:05
Show Gist options
  • Save lcs-rgordon/01cff5a68d75c58fd3ef7ad216b3a252 to your computer and use it in GitHub Desktop.
Save lcs-rgordon/01cff5a68d75c58fd3ef7ad216b3a252 to your computer and use it in GitHub Desktop.
A class written to provide data for my implementation of the Friends app, for the Days 60-61 challenge in #100DaysOfSwiftUI
//
// DataProvider.swift
// Friends
//
// Created by Russell Gordon on 2020-12-26.
//
import CoreData
import Foundation
import SwiftUI
// BASED ON: https://developer.apple.com/documentation/coredata/loading_and_displaying_a_large_data_feed
class DataProvider {
init() {
// Count records on a background thread
let backgroundManagedObjectContext = PersistenceController.shared.container.newBackgroundContext()
backgroundManagedObjectContext.performAndWait {
// Access User entity
let fetchRequest : NSFetchRequest<User> = User.fetchRequest()
do {
let itemCount = try backgroundManagedObjectContext.count(for: fetchRequest)
if itemCount == 0 {
// No user data exists, so go get it
print("No user data exists, going out to get it...")
fetchAndSaveUserData()
} else {
print("User data already exists.")
}
}
catch let error as NSError {
print("Error counting users: \(error.localizedDescription)")
}
}
}
/// Get the basic user data from Hacking with Swift site
func fetchAndSaveUserData() {
// 1. Prepare a URLRequest to get user data
let url = URL(string: "https://www.hackingwithswift.com/samples/friendface.json")
var request = URLRequest(url: url!)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "GET"
// 2. Run the request and process the response
URLSession.shared.dataTask(with: request) { data, response, error in
// handle the result here – attempt to unwrap optional data provided by task
guard let unwrappedData = data else {
// Show the error message
print("No data in response: \(error?.localizedDescription ?? "Unknown error")")
return
}
// DEBUG: Inspect contents of JSON file retrieved from the server
print(String(data: unwrappedData, encoding: .utf8)!)
// Now decode from JSON directly into Core Data managed objects
// Do this on a background thread to avoid concurrency issues
// SEE: https://stackoverflow.com/questions/49454965/how-to-save-to-managed-object-context-in-a-background-thread-in-core-data
// SEE: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/Concurrency.html
// SEE: https://www.donnywals.com/using-codable-with-core-data-and-nsmanagedobject/
let backgroundManagedObjectContext = PersistenceController.shared.container.newBackgroundContext()
// Set unused undoManager to nil for macOS (it is nil by default on iOS)
// to reduce resource requirements.
backgroundManagedObjectContext.undoManager = nil
// Set the merge policy for duplicate entries – ensure that duplicate entries get merged into a single unique record
// "To help resolve this, Core Data gives us constraints: we can make one attribute constrained so that it must always be unique. We can then go ahead and make as many objects as we want, unique or otherwise, but as soon as we ask Core Data to save those objects it will resolve duplicates so that only one piece of data gets written."
backgroundManagedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
// Decode on a background thread
backgroundManagedObjectContext.performAndWait {
// Decode into Core Data objects
let decoder = JSONDecoder(context: backgroundManagedObjectContext)
if let decodedData = try? decoder.decode([User].self, from: unwrappedData) {
print("There were \(decodedData.count) users placed into Core Data")
} else {
print("Could not decode from JSON into Core Data")
}
// Required to actually write the changes to Core Data
do {
// Save Core Data objects
try backgroundManagedObjectContext.save()
// // DEBUG: Count records
// let fetchRequest : NSFetchRequest<User> = User.fetchRequest()
// do {
// let itemCount = try backgroundManagedObjectContext.count(for: fetchRequest)
// print("There are \(itemCount) users now.")
// }
// catch let error as NSError {
// print("Error counting users: \(error.localizedDescription)")
// }
// Now actually try to load the avatars
self.fetchAndSaveAvatars()
} catch {
fatalError("Failure to save context: \(error)")
}
}
}.resume()
}
/// Get avatars for all the users just loaded
func fetchAndSaveAvatars() {
// We know there should be exactly 100 users, so...
for _ in 1...100 {
// 1. Prepare a URL to fetch the image from
let someNumber = Int.random(in: 0...99)
let avatarAddress = "https://randomuser.me/api/portraits/women/\(someNumber).jpg"
let url = URL(string: avatarAddress)!
// 2. Run the request and process the response
URLSession.shared.dataTask(with: url) { data, response, error in
// DEBUG: Did avatar data come back?
// print("retrieved avatar data")
// Handle the result here – attempt to unwrap optional data provided by task
guard let imageData = data else {
// Show the error message
print("No data in response: \(error?.localizedDescription ?? "Unknown error")")
return
}
// Attempt to create an instance of UIImage using the data from the server
if let loadedImage = UIImage(data: imageData) {
// Set up a managed object context to run on a background thread
let backgroundManagedObjectContext = PersistenceController.shared.container.newBackgroundContext()
backgroundManagedObjectContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
backgroundManagedObjectContext.undoManager = nil
// Run this block on a background thread
backgroundManagedObjectContext.performAndWait {
// Build a fetch request that will get the next single user without an avatar image
let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "avatar == nil")
fetchRequest.sortDescriptors = [
NSSortDescriptor(keyPath: \User.name, ascending: true)
]
fetchRequest.fetchLimit = 1
// Get the user
var results: [User] = []
do {
results = try backgroundManagedObjectContext.fetch(fetchRequest)
}
catch let error as NSError {
print("Error getting results from Users entity: \(error.localizedDescription)")
}
// Get a reference to the user returned
// There should only be one object in the result set
if let user = results.first {
// Save data to object encoded as a JPEG
user.avatar = loadedImage.jpegData(compressionQuality: 1.0)
// DEBUG
// print("successfully retrieved and saved avatar image to core data")
// Required to actually write the changes to Core Data
do {
// Save Core Data objects
try backgroundManagedObjectContext.save()
} catch {
fatalError("Failure to save context: \(error)")
}
}
}
} else {
print("Couldn't load avatar image from server.")
}
}.resume()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment