Skip to content

Commit

Permalink
Add FileState (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xLeif authored Feb 4, 2024
1 parent 6eb073a commit ce10e98
Show file tree
Hide file tree
Showing 9 changed files with 585 additions and 1 deletion.
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
3 changes: 2 additions & 1 deletion Sources/AppState/Application/Application+internal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ extension Application {
"AppState/Application+StoredState.swift",
"AppState/Application+SyncState.swift",
"AppState/Application+SecureState.swift",
"AppState/Application+Slice.swift"
"AppState/Application+Slice.swift",
"AppState/Application+FileState.swift",
]
let isFileIDValue: Bool = excludedFileIDs.contains(fileID.description) == false

Expand Down
94 changes: 94 additions & 0 deletions Sources/AppState/Application/Application+public.swift
Original file line number Diff line number Diff line change
Expand Up @@ -992,3 +992,97 @@ 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()
}

/**
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 `path` and `filename`. 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 `FileManager.defaultFileStatePath`.
- filename: The name of the file to read.
- isBase64Encoded: Boolean to determine if the value should be encoded as Base64. The default is `true`.
- Returns: The state of type `Value`.
*/
func fileState<Value>(
initial: @escaping @autoclosure () -> Value,
path: String = FileManager.defaultFileStatePath,
filename: String,
isBase64Encoded: Bool = true
) -> FileState<Value> {
FileState(
initial: initial(),
scope: Scope(name: path, id: filename),
isBase64Encoded: isBase64Encoded
)
}

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

- Parameters:
- path: The path to the directory containing the file. The default is `FileManager.defaultFileStatePath`.
- filename: The name of the file to read.
- isBase64Encoded: Boolean to determine if the value should be encoded as Base64. The default is `true`.
- Returns: The state of type `Value`.
*/
func fileState<Value>(
path: String = FileManager.defaultFileStatePath,
filename: String,
isBase64Encoded: Bool = true
) -> FileState<Value?> {
fileState(
initial: nil,
path: path,
filename: filename,
isBase64Encoded: isBase64Encoded
)
}
}
1 change: 1 addition & 0 deletions Sources/AppState/Application/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ open class Application: NSObject {
/// Loads the default dependencies for use in Application.
private func loadDefaultDependencies() {
load(dependency: \.userDefaults)
load(dependency: \.fileManager)
}

#if !os(Linux) && !os(Windows)
Expand Down
157 changes: 157 additions & 0 deletions Sources/AppState/Application/Types/Helper/FileManager+AppState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import Foundation

extension FileManager {
/// Gets the documentDirectory from FileManager and appends "/App". Otherwise if it can not get the documents directory is will return "~/App". This variable can be set to whatever path you want to be the default.
public static var defaultFileStatePath: String = {
let fileManager: FileManager = Application.dependency(\.fileManager)
guard let path = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
return "~/App"
}

#if !os(Linux) && !os(Windows)
if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
return "\(path.path())/App"
} else {
return "\(path.path)/App"
}
#else
return "\(path.path)/App"
#endif
}()

/// 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 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 !os(Linux) && !os(Windows)
if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
return URL(filePath: filePath)
} else {
return URL(fileURLWithPath: filePath)
}
#else
return URL(fileURLWithPath: filePath)
#endif
}
}
Loading

0 comments on commit ce10e98

Please sign in to comment.