func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
load("jsonFilePath")
The function extracts the data form a JSON.
What is that 'parameter' type: T.Type = T.self for? If I remove this parameter the code still works. So what do I need it for?
This parameter is just a helper to use three kind of declarations as below,
1) let model: Model = self.load("Countries")
2) let model = self.load("Countries", as: Model.self)
3) let model = self.load("Countries") as Model
You can remove it from the method signature if you want to use the first kind of declaration.
Related
Given this (partially pasted) struct:
struct InstrumentsSet: Identifiable, Decodable {
static func withJSON(_ fileName: String) -> InstrumentsSet? {
guard let url = Bundle.main.url(forResource: fileName, withExtension: "json", subdirectory: "Sets") else { return nil }
guard let data = try? Data(contentsOf: url) else { return nil }
return try? JSONDecoder().decode(InstrumentsSet.self, from: data)
}
let name: String
let bpm: Double
let timeSignature: Int
var tracks: [Track]
}
And this extension:
extension InstrumentsSet {
struct Track: Identifiable, Decodable {
static func withJSON(_ fileName: String) -> InstrumentsSet.Track? {
guard let url = Bundle.main.url(forResource: fileName, withExtension: "json", subdirectory: "Sets") else { return nil }
guard let data = try? Data(contentsOf: url) else { return nil }
//At this point data has the JSON loaded and decode makes it jump to the fatal error located at the call
return try? JSONDecoder().decode(InstrumentsSet.Track.self, from: data)
}
let instrumentType: InstrumentType
let startType: StartType
}
}
I can load successfully an instrument set from JSON with nested Tracks this way:
guard var instrumentSet1 = InstrumentsSet.withJSON("file-with-set") else {
fatalError("Error loading set JSON")
}
Next I'd like to add a track to the existing set by loading a json with just Track data:
guard let masterTrack = InstrumentsSet.Track.withJSON("file-with-track") else {
fatalError("Error loading track JSON")
}
When running this part I can see the JSON data loading successfully, but on this line return try? JSONDecoder().decode(InstrumentsSet.Track.self, from: data) the fatalError is triggered without further explanation. My guess is something is missing in the instrumentSet part since the function "InstrumentsSet.Track.withJSON" is called through the original InstrumentsSet struct so passing down just a track is not possible?
My question, given the struct and its extension, is it possible to load just the extension part (Track) of the struct into the let masterTrack?
Example Set JSON:
{
"name": "Set name",
"bpm": 124.00,
"timeSignature": 4,
"tracks": [
{
"instrumentType": "exsSampler",
"startType": "trigger"
}
]
}
Example Track JSON. (I've tried passing this as an Array, but still no success)
{
"instrumentType": "audioBuffer",
"startType": "global"
}
Thank you!
I am trying to construct a generic persistence routine for a personal Swift package I can use in my own projects.
The idea:
I can feed the save(instance:) method and it will
create a new folder in the documents folder named with the name of the type of instance I pass (i.e. if I pass an instance of Contact, the folder will be called Contact
save the instance as json
to load all instances of a specific type, I want to use a loadAll(of type:) method which
decodes all files within that folder if it exists
returns an array of these instances
Here's the packages persistence class:
public class ATPersistLocally {
public static let shared = ATPersistLocally()
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
private let fileManager: FileManager = {
return FileManager.default
}()
private let docPath: URL = {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
guard let docPath = urls.first else { fatalError() }
ATLogger.shared.logToConsole(message: "Doc path is: \(docPath)", type: .debug)
return docPath
}()
//MARK: - Loading
public func loadAll<T: Codable & Identifiable>(of type: T) -> [T] {
var result = [T]()
let typeName = getTypeName(of: type) //custom helper method, see below
let pathFolder = docPath.appendingPathComponent(String(describing: typeName))
if fileManager.fileExists(atPath: pathFolder.path) {
var urls = [URL]()
do {
urls = try fileManager.contentsOfDirectory(at: pathFolder, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
} catch let error {
ATLogger.shared.logToConsole(message: "Could not load content of directory: \(error.localizedDescription)", type: .error)
}
for url in urls {
if let data = fileManager.contents(atPath: url.path) {
do {
let instance = try decoder.decode(T.self, from: data)
result.append(instance)
} catch let error {
ATLogger.shared.logToConsole(message: "Could not decode data: \(error.localizedDescription)", type: .error)
}
}
}
}
return result
}
//MARK: - Saving
public func save<T: Codable & Identifiable>(instance: T) {
let typeName = getTypeName(of: instance)
let pathFolder = docPath.appendingPathComponent(String(describing: typeName))
if !fileManager.fileExists(atPath: pathFolder.absoluteString) {
do {
try fileManager.createDirectory(atPath: pathFolder.relativePath, withIntermediateDirectories: true)
} catch let error {
ATLogger.shared.logToConsole(message: "Could not create folder for type \(typeName): \(error.localizedDescription)", type: .error)
}
}
let path = pathFolder.appendingPathComponent(instance.id as! String)
do {
let data = try encoder.encode(instance)
do {
try data.write(to: URL(fileURLWithPath: path.relativePath))
ATLogger.shared.logToConsole(message: "Saved instance of \(typeName)", type: .debug)
} catch let error {
ATLogger.shared.logToConsole(message: "Could not save data: \(error.localizedDescription)", type: .error)
}
} catch let error {
ATLogger.shared.logToConsole(message: "Could not encode data: \(error.localizedDescription)", type: .error)
}
}
//MARK: - Helper Functions
private func getTypeName<T: Codable & Identifiable>(of type: T) -> T.Type {
return T.self
}
}
While this compiles and saving already works just fine, I do have a problem with loading.
Here's an example struct (which has to be Codable & Identifiable if I understand correctly) that I am trying to use:
import Foundation
struct Contact: Codable, Identifiable {
let id = UUID()
let contactTitle: String
var name: String? = nil
var firstname: String? = nil
var address: String? = nil
var zip: String? = nil
var city: String? = nil
}
And here's the load call I am trying to make:
var contacts = ATPersistLocally.shared.loadAll(of: Contact.self)
The error I am getting at build time is Argument type 'Contact.Type' does not conform to expected type 'Decodable'.
I also tried to call contacts = ATPersistLocally.shared.loadAll(of: Contact), which results in the exact same error message.
Clearly I am misunderstanding something concerning the generics concept here - which is why any hint in the right direction would be highly appreciated!
The type of the parameter type in loadAll must be T.Type
public func loadAll<T: Codable & Identifiable>(of type: T.Type) -> [T] { ...
This makes the method getTypeName obsolete because you can write
let pathFolder = docPath.appendingPathComponent(String(describing: T.self))
Side note:
The error instance is implicitly available in a catch block, let error is redundant.
I already accepted vadian's answer as the correct answer, so all the credit's go to him.
I also want to provide the whole ATPersistLocally class that is working for me now, hoping that it might help others as well:
import Foundation
public class ATPersistLocally {
public static let shared = ATPersistLocally()
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
private let fileManager: FileManager = {
return FileManager.default
}()
private let docPath: URL = {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
guard let docPath = urls.first else { fatalError() }
ATLogger.shared.logToConsole(message: "Doc path is: \(docPath)", type: .debug)
return docPath
}()
//MARK: - Loading
public func loadAll<T: Codable & Identifiable>(of type: T.Type) -> [T] {
var result = [T]()
let pathFolder = docPath.appendingPathComponent(String(describing: type))
if fileManager.fileExists(atPath: pathFolder.path) {
var urls = [URL]()
do {
urls = try fileManager.contentsOfDirectory(at: pathFolder, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
} catch {
ATLogger.shared.logToConsole(message: "Could not load content of directory: \(error.localizedDescription)", type: .error)
}
for url in urls {
if let data = fileManager.contents(atPath: url.path) {
do {
let instance = try decoder.decode(T.self, from: data)
result.append(instance)
} catch {
ATLogger.shared.logToConsole(message: "Could not decode data: \(error.localizedDescription)", type: .error)
}
}
}
}
return result
}
//MARK: - Saving
public func save<T: Codable & Identifiable>(instance: T) {
let typeName = getTypeName(of: instance)
let pathFolder = docPath.appendingPathComponent(String(describing: typeName))
if !fileManager.fileExists(atPath: pathFolder.absoluteString) {
do {
try fileManager.createDirectory(atPath: pathFolder.relativePath, withIntermediateDirectories: true)
} catch {
ATLogger.shared.logToConsole(message: "Could not create folder for type \(typeName): \(error.localizedDescription)", type: .error)
}
}
let path = pathFolder.appendingPathComponent(String(describing: instance.id))
do {
let data = try encoder.encode(instance)
do {
try data.write(to: URL(fileURLWithPath: path.relativePath))
ATLogger.shared.logToConsole(message: "Saved instance of \(typeName)", type: .debug)
} catch {
ATLogger.shared.logToConsole(message: "Could not save data: \(error.localizedDescription)", type: .error)
}
} catch let error {
ATLogger.shared.logToConsole(message: "Could not encode data: \(error.localizedDescription)", type: .error)
}
}
//MARK: - Helper Functions
private func getTypeName<T: Codable & Identifiable>(of type: T) -> T.Type {
return T.self
}
}
In projects where I want to use this logic, I import my package through SPM and make sure the struct to save / load conforms Codable and Identifiable.
Be aware that Identifiable requires iOS13 as it was added to the standard library only with Swift 5.1.
I am attempting to use the NSKeyedArchiver to write a Codable to disk.
All the questions I could find on the subject using deprecated methods. I can't find any SO questions or tutorials using the Swift 4 syntax.
I am getting the error
-[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance
Which I am guessing is the try writeData.write(to: fullPath) line in my UsersSession class.
What is the proper way to write data to a file in Swift 4.2?
struct UserObject {
var name : String?
}
extension UserObject : Codable {
enum CodingKeys : String, CodingKey {
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
}
}
UserSession.swift
class UserSession {
static let shared = UserSession()
let fileName = "userdata.dat"
var user : UserObject?
lazy var fullPath : URL = {
return getDocumentsDirectory().appendingPathComponent(fileName)
}()
private init(){
print("FullPath: \(fullPath)")
user = UserObject()
load()
}
func save(){
guard let u = user else { print("invalid user data to save"); return}
do {
let writeData = try NSKeyedArchiver.archivedData(withRootObject: u, requiringSecureCoding: false)
try writeData.write(to: fullPath)
} catch {
print("Couldn't write user data file")
}
}
func load() {
guard let data = try? Data(contentsOf: fullPath, options: []) else {
print("No data found at location")
save()
return
}
guard let loadedUser = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? UserObject else {
print("Couldn't read user data file.");
return
}
user = loadedUser
}
func getDocumentsDirectory() -> URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
}
}
Since you are using Codable, you should first encode to Data and then archivedData. Here is the code:
func save(){
guard let u = user else { print("invalid user data to save"); return}
do {
// Encode to Data
let jsonData = try JSONEncoder().encode(u)
let writeData = try NSKeyedArchiver.archivedData(withRootObject: jsonData, requiringSecureCoding: false)
try writeData.write(to: fullPath)
} catch {
print("Couldn't write user data file")
}
}
func load() {
guard let data = try? Data(contentsOf: fullPath, options: []) else {
print("No data found at location")
save()
return
}
guard let loadedUserData = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Data else {
print("Couldn't read user data file.");
return
}
// Decode Data
user = try? JSONDecoder().decode(UserObject.self, from: loadedUserData)
}
I'm trying to create a generic decode function to decode my two different models. I get the error "Argument type 'PrivateSchoolType.Type' does not conform to expected type 'Decodable'".
Model
struct PrivateSchoolModel: Decodable {
var name: String
var id: String
var city: String
var state: String
}
Calling Function
function getInfo() {
// does not work => ERROR
guard let schools = decode(jsonData: jsonData, using: PrivateSchoolModel) else { return }
// does work
guard let schools = specificDecode()
}
Specific Decode Function (DOES WORK)
private func specificDecode() -> [PrivateSchoolModel]? {
guard let jsonData = getJSONData(from: .privateSchool) else { return }
do {
let decoder = JSONDecoder()
let schools = try decoder.decode([PrivateSchoolModel].self, from:
jsonData)
return schools
} catch let error {
print("Error: \(error.localizedDescription)")
}
return nil
}
Generic Decode Function (DOES NOT WORK)
private func decode<M: Decodable>(jsonData: Data, using model: M) -> [M]? {
do {
//here dataResponse received from a network request
let decoder = JSONDecoder()
let schools = try decoder.decode([M].self, from:
jsonData) //Decode JSON Response Data
return schools
} catch let parsingError {
print("Error", parsingError)
}
return nil
}
Change the method signature as below,
private func decode<M: Decodable>(jsonData: Data, using modelType: M.Type) -> M? {
do {
//here dataResponse received from a network request
let decoder = JSONDecoder()
let schools = try decoder.decode(modelType, from: jsonData) //Decode JSON Response Data
return schools
} catch let parsingError {
print("Error", parsingError)
}
return nil
}
Usage
guard let schools = decode(jsonData: jsonData, using: [PublicSchoolModel].self) else { return }
I already have read Read and write data from text file
I need to append the data (a string) to the end of my text file.
One obvious way to do it is to read the file from disk and append the string to the end of it and write it back, but it is not efficient, especially if you are dealing with large files and doing in often.
So the question is "How to append string to the end of a text file, without reading the file and writing the whole thing back"?
so far I have:
let dir:NSURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last as NSURL
let fileurl = dir.URLByAppendingPathComponent("log.txt")
var err:NSError?
// until we find a way to append stuff to files
if let current_content_of_file = NSString(contentsOfURL: fileurl, encoding: NSUTF8StringEncoding, error: &err) {
"\(current_content_of_file)\n\(NSDate()) -> \(object)".writeToURL(fileurl, atomically: true, encoding: NSUTF8StringEncoding, error: &err)
}else {
"\(NSDate()) -> \(object)".writeToURL(fileurl, atomically: true, encoding: NSUTF8StringEncoding, error: &err)
}
if err != nil{
println("CANNOT LOG: \(err)")
}
Here's an update for PointZeroTwo's answer in Swift 3.0, with one quick note - in the playground testing using a simple filepath works, but in my actual app I needed to build the URL using .documentDirectory (or which ever directory you chose to use for reading and writing - make sure it's consistent throughout your app):
extension String {
func appendLineToURL(fileURL: URL) throws {
try (self + "\n").appendToURL(fileURL: fileURL)
}
func appendToURL(fileURL: URL) throws {
let data = self.data(using: String.Encoding.utf8)!
try data.append(fileURL: fileURL)
}
}
extension Data {
func append(fileURL: URL) throws {
if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
}
else {
try write(to: fileURL, options: .atomic)
}
}
}
//test
do {
let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! as URL
let url = dir.appendingPathComponent("logFile.txt")
try "Test \(Date())".appendLineToURL(fileURL: url as URL)
let result = try String(contentsOf: url as URL, encoding: String.Encoding.utf8)
}
catch {
print("Could not write to file")
}
Thanks PointZeroTwo.
You should use NSFileHandle, it can seek to the end of the file
let dir:NSURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last as NSURL
let fileurl = dir.URLByAppendingPathComponent("log.txt")
let string = "\(NSDate())\n"
let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
if NSFileManager.defaultManager().fileExistsAtPath(fileurl.path!) {
var err:NSError?
if let fileHandle = NSFileHandle(forWritingToURL: fileurl, error: &err) {
fileHandle.seekToEndOfFile()
fileHandle.writeData(data)
fileHandle.closeFile()
}
else {
println("Can't open fileHandle \(err)")
}
}
else {
var err:NSError?
if !data.writeToURL(fileurl, options: .DataWritingAtomic, error: &err) {
println("Can't write \(err)")
}
}
A variation over some of the posted answers, with following characteristics:
based on Swift 5
accessible as a static function
appends new entries to the end of the file, if it exists
creates the file, if it doesn't exist
no cast to NS objects (more Swiftly)
fails silently if the text cannot be encoded or the path does not exist
class Logger {
static var logFile: URL? {
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
let dateString = formatter.string(from: Date())
let fileName = "\(dateString).log"
return documentsDirectory.appendingPathComponent(fileName)
}
static func log(_ message: String) {
guard let logFile = logFile else {
return
}
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
let timestamp = formatter.string(from: Date())
guard let data = (timestamp + ": " + message + "\n").data(using: String.Encoding.utf8) else { return }
if FileManager.default.fileExists(atPath: logFile.path) {
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
}
} else {
try? data.write(to: logFile, options: .atomicWrite)
}
}
}
Here is a way to update a file in a much more efficient way.
let monkeyLine = "\nAdding a šµ to the end of the file via FileHandle"
if let fileUpdater = try? FileHandle(forUpdating: newFileUrl) {
// Function which when called will cause all updates to start from end of the file
fileUpdater.seekToEndOfFile()
// Which lets the caller move editing to any position within the file by supplying an offset
fileUpdater.write(monkeyLine.data(using: .utf8)!)
// Once we convert our new content to data and write it, we close the file and thatās it!
fileUpdater.closeFile()
}
Here's a version for Swift 2, using extension methods on String and NSData.
//: Playground - noun: a place where people can play
import UIKit
extension String {
func appendLineToURL(fileURL: NSURL) throws {
try self.stringByAppendingString("\n").appendToURL(fileURL)
}
func appendToURL(fileURL: NSURL) throws {
let data = self.dataUsingEncoding(NSUTF8StringEncoding)!
try data.appendToURL(fileURL)
}
}
extension NSData {
func appendToURL(fileURL: NSURL) throws {
if let fileHandle = try? NSFileHandle(forWritingToURL: fileURL) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.writeData(self)
}
else {
try writeToURL(fileURL, options: .DataWritingAtomic)
}
}
}
// Test
do {
let url = NSURL(fileURLWithPath: "test.log")
try "Test \(NSDate())".appendLineToURL(url)
let result = try String(contentsOfURL: url)
}
catch {
print("Could not write to file")
}
In order to stay in the spirit of #PointZero Two.
Here an update of his code for Swift 4.1
extension String {
func appendLine(to url: URL) throws {
try self.appending("\n").append(to: url)
}
func append(to url: URL) throws {
let data = self.data(using: String.Encoding.utf8)
try data?.append(to: url)
}
}
extension Data {
func append(to url: URL) throws {
if let fileHandle = try? FileHandle(forWritingTo: url) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
} else {
try write(to: url)
}
}
}
Update: I wrote a blog post on this, which you can find here!
Keeping things Swifty, here is an example using a FileWriter protocol with default implementation (Swift 4.1 at the time of this writing):
To use this, have your entity (class, struct, enum) conform to this protocol and call the write function (fyi, it throws!).
Writes to the document directory.
Will append to the text file if the file exists.
Will create a new file if the text file doesn't exist.
Note: this is only for text. You could do something similar to write/append Data.
import Foundation
enum FileWriteError: Error {
case directoryDoesntExist
case convertToDataIssue
}
protocol FileWriter {
var fileName: String { get }
func write(_ text: String) throws
}
extension FileWriter {
var fileName: String { return "File.txt" }
func write(_ text: String) throws {
guard let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
throw FileWriteError.directoryDoesntExist
}
let encoding = String.Encoding.utf8
guard let data = text.data(using: encoding) else {
throw FileWriteError.convertToDataIssue
}
let fileUrl = dir.appendingPathComponent(fileName)
if let fileHandle = FileHandle(forWritingAtPath: fileUrl.path) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
} else {
try text.write(to: fileUrl, atomically: false, encoding: encoding)
}
}
}
All answers (as of now) recreate the FileHandle for every write operation. This may be fine for most applications, but this is also rather inefficient: A syscall is made, and the filesystem is accessed each time you create the FileHandle.
To avoid creating the filehandle multiple times, use something like:
final class FileHandleBuffer {
let fileHandle: FileHandle
let size: Int
private var buffer: Data
init(fileHandle: FileHandle, size: Int = 1024 * 1024) {
self.fileHandle = fileHandle
self.size = size
self.buffer = Data(capacity: size)
}
deinit { try! flush() }
func flush() throws {
try fileHandle.write(contentsOf: buffer)
buffer = Data(capacity: size)
}
func write(_ data: Data) throws {
buffer.append(data)
if buffer.count > size {
try flush()
}
}
}
// USAGE
// Create the file if it does not yet exist
FileManager.default.createFile(atPath: fileURL.path, contents: nil)
let fileHandle = try FileHandle(forWritingTo: fileURL)
// Seek will make sure to not overwrite the existing content
// Skip the seek to overwrite the file
try fileHandle.seekToEnd()
let buffer = FileHandleBuffer(fileHandle: fileHandle)
for i in 0..<count {
let data = getData() // Your implementation
try buffer.write(data)
print(i)
}