Skip to content

Commit

Permalink
Add FileState
Browse files Browse the repository at this point in the history
  • Loading branch information
0xLeif committed Jan 31, 2024
1 parent 6eb073a commit afab8e3
Show file tree
Hide file tree
Showing 6 changed files with 594 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ AppState is a Swift Package that simplifies the management of application state
- **Application:** Centralized class for all application-wide data with built-in observability for reactive changes.
- **State:** Struct for encapsulating and broadcasting value changes.
- **StoredState:** Struct for encapsulating and broadcasting stored value changes, using `UserDefaults`.
- **FileState:** Struct for encapsulating and broadcasting stored value changes, using `FileManager`.
- 🍎 **SyncState:** Struct for encapsulating and broadcasting stored value changes, using `iCloud`.
- 🍎 **SecureState:** Struct for securely encapsulating and broadcasting stored value changes, using the device's Keychain.

Expand All @@ -33,6 +34,7 @@ AppState is a Swift Package that simplifies the management of application state

- **AppState:** Bridges `Application.State` with `SwiftUI`.
- **StoredState:** Stores its values to `UserDefaults`.
- **FileState:** Stores its values using `FileManager`.
- 🍎 **SyncState:** Stores its values to `iCloud`.
- **Slice:** Allows users to access and modify specific AppState's state parts.
- **OptionalSlice:** Allows users to access and modify specific AppState's state parts. Useful if the state value is optional.
Expand Down
106 changes: 106 additions & 0 deletions Sources/AppState/Application/Application+public.swift
Original file line number Diff line number Diff line change
Expand Up @@ -992,3 +992,109 @@ extension Application {
}
}

// MARK: - FileState Functions

public extension Application {
/// Resets the value to the inital value. If the inital value was `nil`, then the value will be removed from `FileManager`
static func reset<Value>(
fileState keyPath: KeyPath<Application, FileState<Value>>,
_ fileID: StaticString = #fileID,
_ function: StaticString = #function,
_ line: Int = #line,
_ column: Int = #column
) {
log(
debug: "🗄️ Resetting FileState \(String(describing: keyPath))",
fileID: fileID,
function: function,
line: line,
column: column
)

var fileState = shared.value(keyPath: keyPath)
fileState.reset()
}

/// Removes the value from `FileManager` and resets the value to the inital value.
@available(*, deprecated, renamed: "reset")
static func remove<Value>(
fileState keyPath: KeyPath<Application, FileState<Value>>,
_ fileID: StaticString = #fileID,
_ function: StaticString = #function,
_ line: Int = #line,
_ column: Int = #column
) {
reset(
fileState: keyPath,
fileID,
function,
line,
column
)
}

/**
Retrieves a stored state from Application instance using the provided keypath.

- Parameter keyPath: KeyPath of the state value to be fetched
- Returns: The requested state of type `Value`.
*/
static func fileState<Value>(
_ keyPath: KeyPath<Application, FileState<Value>>,
_ fileID: StaticString = #fileID,
_ function: StaticString = #function,
_ line: Int = #line,
_ column: Int = #column
) -> FileState<Value> {
let fileState = shared.value(keyPath: keyPath)

log(
debug: "🗄️ Getting FileState \(String(describing: keyPath)) -> \(fileState.value)",
fileID: fileID,
function: function,
line: line,
column: column
)

return fileState
}

/**
Retrieves a `FileManager` backed state for the provided `id`. If the state is not present, it initializes a new state with the `initial` value.

- Parameters:
- initial: The closure that returns initial state value.
- path: The path to the directory containing the file. The default is `App`.
- filename: The name of the file to read.
- Returns: The state of type `Value`.
*/
func fileState<Value>(
initial: @escaping @autoclosure () -> Value,
path: String = "./App",
filename: String
) -> FileState<Value> {
FileState(
initial: initial(),
scope: Scope(name: path, id: filename)
)
}

/**
Retrieves a `FileManager` backed state for the provided `id` with a default value of `nil`.

- Parameters:
- path: The path to the directory containing the file. The default is `App`.
- filename: The name of the file to read.
- Returns: The state of type `Value`.
*/
func fileState<Value>(
path: String = "./App",
filename: String
) -> FileState<Value?> {
fileState(
initial: nil,
path: path,
filename: filename
)
}
}
169 changes: 169 additions & 0 deletions Sources/AppState/Application/Types/Helper/FileManager+AppState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import Foundation

extension FileManager {
enum FileError: String, LocalizedError {
case invalidStringFromData = "String could not be converted into Data."

var errorDescription: String? { rawValue }
}

/// Creates a directory at the specified path.
///
/// - Parameters:
/// - path: The path at which to create the directory.
/// - Throws: An error if the directory could not be created.
func createDirectory(path: String) throws {
try createDirectory(atPath: path, withIntermediateDirectories: true)
}

/// Read a file's data as the type `Value`
///
/// - Parameters:
/// - path: The path to the directory containing the file. The default is `.`, which means the current working directory.
/// - filename: The name of the file to read.
/// - Returns: The file's data decoded as an instance of `Value`.
/// - Throws: If there's an error reading the file or decoding its data.
func `in`<Value: Decodable>(
path: String = ".",
filename: String
) throws -> Value {
let data = try data(
path: path,
filename: filename
)

return try JSONDecoder().decode(Value.self, from: data)
}

/// Read a file's data
///
/// - Parameters:
/// - path: The path to the directory containing the file. The default is `.`, which means the current working directory.
/// - filename: The name of the file to read.
/// - Returns: The file's data.
/// - Throws: If there's an error reading the file.
func data(
path: String = ".",
filename: String
) throws -> Data {
let directoryURL: URL = url(filePath: path)

let fileData = try Data(
contentsOf: directoryURL.appendingPathComponent(filename)
)

guard let base64DecodedData = Data(base64Encoded: fileData) else {
return fileData
}

return base64DecodedData
}

/// Write data to a file using a JSONEncoder
///
/// - Parameters:
/// - value: The data to write to the file. It must conform to the `Encodable` protocol.
/// - path: The path to the directory where the file should be written. The default is `.`, which means the current working directory.
/// - filename: The name of the file to write.
/// - base64Encoded: A Boolean value indicating whether the data should be Base64-encoded before writing to the file. The default is `true`.
/// - Throws: If there's an error writing the data to the file.
func out<Value: Encodable>(
_ value: Value,
path: String = ".",
filename: String,
base64Encoded: Bool = true
) throws {
let data = try JSONEncoder().encode(value)

try out(
data: data,
path: path,
filename: filename,
base64Encoded: base64Encoded
)
}

/// Write a string to a file
///
/// - Parameters:
/// - string: The string to write to the file.
/// - path: The path to the directory where the file should be written. The default is `.`, which means the current working directory.
/// - filename: The name of the file to write.
/// - using: The String.Encoding to encode the string with. The default is `.utf8`.
/// - base64Encoded: A Boolean value indicating whether the data should be Base64-encoded before writing to the file. The default is `true`.
/// - Throws: If there's an error writing the data to the file.
func out(
string: String,
path: String = ".",
filename: String,
using stringEncoding: String.Encoding = .utf8,
base64Encoded: Bool = true
) throws {
guard let data = string.data(using: stringEncoding) else {
throw FileError.invalidStringFromData
}

try out(
data: data,
path: path,
filename: filename,
base64Encoded: base64Encoded
)
}

/// Write data to a file
///
/// - Parameters:
/// - value: The data to write to the file.
/// - path: The path to the directory where the file should be written. The default is `.`, which means the current working directory.
/// - filename: The name of the file to write.
/// - base64Encoded: A Boolean value indicating whether the data should be Base64-encoded before writing to the file. The default is `true`.
/// - Throws: If there's an error writing the data to the file.
func out(
data: Data,
path: String = ".",
filename: String,
base64Encoded: Bool = true
) throws {
let directoryURL: URL = url(filePath: path)

var data = data

if base64Encoded {
data = data.base64EncodedData()
}

if fileExists(atPath: path) == false {
try createDirectory(path: path)
}

try data.write(
to: directoryURL.appendingPathComponent(filename)
)
}

/// Delete a file
///
/// - Parameters:
/// - path: The path to the directory containing the file. The default is `.`, which means the current working directory.
/// - filename: The name of the file to delete.
/// - Throws: If there's an error deleting the file.
func delete(
path: String = ".",
filename: String
) throws {
let directoryURL: URL = url(filePath: path)

try removeItem(
at: directoryURL.appendingPathComponent(filename)
)
}

func url(filePath: String) -> URL {
if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
return URL(filePath: filePath)
} else {
return URL(fileURLWithPath: filePath)
}
}
}
Loading

0 comments on commit afab8e3

Please sign in to comment.