This library provides helpful utilities, like IO operations on the local disk, with encryption layer for security. It also provides a convenice Prefs
class for storing Key-Value pairs easily and safely with the same encryption layer.
SwiftExtensions is a Swift Package.
Use the swift package manager to install SwiftExtensions on your project. Apple Developer Guide
For easy, safe & scalable storage architecture, the Filer
class gives you the ability to read & write files while keeping them extra secure in the file system.
- IO (Read/Write) operations are synchronous, for more control and easier error handling.
- You are are required to use the
Filename
orFolder
structs to state your desired files/folders. best used withextension
like so:
extension Filename {
static let myFile1 = Filename(value: "name1InFiler")
static let myFile2 = Filename(value: "name2InFiler")
}
//Usage
let data = Data("Bubu is the king".utf8)
do {
try Filer.write(data: data, to: .myFile1) //encrypts & write to file
let sameData = try Filer.read(file: .myFile1) //reads & decrypts
print(String(decoding: sameData, as: UTF8.self)) //"Bubu is the king"
try Filer.delete(file: .myFile1)
} catch {
//Handle errors
}
when instantiating
Filename
orFolder
you can (and probably should) use obfusctated names, for exmaple: use "--" insated of "secret.info".
You are able to change some values in the library.
Filer.rootURL
: defaults to the documents url of the app, it can change for example to an AppGroup url:
Filer.rootURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "your.app.group")
Filer.encryptor
: controls the underlining SimpleEncryptor that handles cryptographics: (requires CryptoExtensions
)
Filer.encryptor = SimpleEncryptor(strategy: .gcm)
Insapired after iOS's UserDefaults
& Android's SharedPreferences
, The Prefs
class enables you to manage Key-Value pairs easily and securely using the encryption layer from CryptoExtensions
, also comes with a caching logic for fast & non blocking read/writes operation in memory.
You can either use the Standard instance, which is also using an obfuscated filename, or create your own instances for multiple files, like so:
let standardPrefs = Prefs.standard //the built-in standard instance
//OR
let myPrefs = Prefs(file: .myFile1) //new instance using the Filename struct
You can put values:
extension PrefKey {
static let name = PrefKey(value: "obfuscatedKey") //value should be obfuscated
}
let myPrefs.edit() //start editing
.put(key: .name, "Bubu") //using the static constant '.name'
.commit() //save your changes in memory & lcoal storage
And you can read them:
if let name = myPrefs.string(key: .name) {
print("\(name), is the king")
}
Observing changes with Combine
Framework
let cancelable1 = myPrefs.publisher
.sink { prefs in print("prefs changed") } //prints "prefs changed" whenever we commit changes.
//Detecting changes on a key
let cancelable2 = prefs.publisher
.compactMap { $0.string(key: .name) }
.removeDuplicates()
.sink { print("name changed to: \($0)") }
Prefs.publisher
will fire events when the prefs instance has committed non-empty commits.
Wrapping a variable with @PrefsValue
allows to manage a single value transparently in the prefs
extension PrefKey {
static let displayName = PrefKey(value: "someObfuscatedKey")
}
struct ContentView: View {
@PrefsValue(key: .displayName) var displayName: String = ""
var body: some View {...}
}
Views will re-render when a @PrefsValue
changes
SimpleEncryptor
class, great for convenience crypto operations (data and/or big files), using keychain to store keys.AES/CBC
implementation in Swift, on top of "Common Crypto" implementation.- useful "crypto" extension methods.
let data = Data("I am Groot!".utf8)
let encryptor = SimpleEncryptor(strategy: .cbc(iv: Data(...)))
do {
let encrypted = try encryptor.encrypt(data)
let decrypted = try encryptor.decrypt(encrypted)
print(String(decoding: decrypted, as: UTF8.self)) //"I am Groot!"
} catch {
//handle cryptographic errors
}
let data: Data = ... //some data to encrypt
let iv: Data = ... //an initial verctor
let key: SymmetricKey = ... //encryption key
do {
let encrypted = try AES.CBC.encrypt(data, using: key, iv: iv)
let decrypted = try AES.CBC.decrypt(encrypted, using: key, iv: iv)
} catch {
//handle cryptographic errors
}
struct MyType: Codable {
//values
}
let data = MyType(...).json() //convert to JSON encoded data
do {
let instance: MyType = try .from(json: data) //convert back to your type
} catch { ... }
post {
//run in the main thread
}
async {
//run in a background thread
}
let req = URLRequest(url: "https://your.end.point")
.set(method: .POST) //OR .GET, .PUT, .DELETE, .PATCH
.set(contentType: .json) //OR .xml, .urlEncoded etc.
.set(body: "some String or Data")
Another example:
let dict = ["title": "Bubu is the king", "message": "I am Groot"]
let req = URLRequest(url: "https://your.end.point")
.set(method: .PUT)
.set(contentType: .json)
.set(body: dict.json()) //allows encodable values
let helloWorld = "helloWorld".localized //provided you have "helloWorld" key in Localizable.strings files"
print(helloWorld) //will automatically use the wanted localization
//Navigation:
extension ControllerID {
static let myCtrl = ControllerID(value: "myCTRL") //make sure to have "myCTRL" identifier in Storyboard
}
viewController.push(to: .myCtrl)
//OR with `config` block
viewController.present(.myCtrl) { (vc: MyViewController) in
//do any configuration before presenting "myCTRL", for example: settting instance variables
}
Conveniece structs for working with dynamic JSON.
//new JSON example
let json = JsonObject()
.with(key: "name", value: "Bubu")
.with(key: "age", value: 10)
do {
let data: Data = try json.data()
} catch {
//handle encoding error
}
//receiving JSON as `Data` from an API
do {
let json = try JsonObject(data: dataFromApi) //given data from API
if let name = json.string(key: "name"),
let age = json.int(key: "age") {
print("name: \(name), age: \(age)")
}
} catch {
//handle decoding error
}
Apache License 2.0