Skip to content

A simple SwiftUI property wrapper for synchronized interaction with Keychain.

License

Notifications You must be signed in to change notification settings

danyaffff/KeychainStorage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

KeychainStorage

A simple SwiftUI property wrapper for synchronized interaction with Keychain.

Features

  • Supports all types that confirm Sendable, Equatable, and Codable protocols.
  • Synchorizes data across all @KeychainStorages with the same key and data type.
  • Supports any custom Keychain objects that confirm a KeychainProvider protocol.
  • Supports Optional values to delete objects from Keychain.

Motivation

SwiftUI provides two awesome property wrapper types @AppStorage and @SceneStorage to deal with UserDefaults. One of the most interesting features of these property wrappers is synchronization across all the similar property wrappers. So, changes made in one property wrapper automatically synchronizes in corresponding property wrappers.

struct WriterView: View {
	@AppStorage("someKey") private var someValue = false
	
	var body: some View {
		Button("Toggle") {
			someValue.toggle()  // Updates UserDefault's object with key "some Key"
		}
	}
}

struct ReaderView: View {
	@AppStorage("someKey") private var someValue = false
	
	var body: some View {
		Text("\(someValue)")  // Automatically displays new data
	}
}

Despite the easy-to-use UserDefaults property wrappers, there are no objects to work with Keychain that way. This library aims to provide a similar experience interacting with Keychain.

How does it work?

I don’t know how @AppStorage and @SceneStorage work internally. I suppose they may use KVO (or a similar approach) to observe UserDefaults changes to receive actual data. We can’t use Keychain that way, because there is no API to provide a callback that will be called when an object for a certain key changes (or any other callbacks except this only one deprecated function that can be used only on macOS).

So I decided to use the NotificationCenter to post and observe internal-library notification that contains a key (to determine what was changed) and a new value (to update all the other storages). This approach doesn’t respect any changes that are made in Keychain directly, it only synchronizes property wrappers with each other.

Property wrapper itself does not guarantee that any data was written to Keychain, but Keychain provider may control property wrapper's local value updating by returning a Bool value.

Installation

Swift Package Manager

Add the following line into the package dependencies value of your Package.swift file or the Package Dependencies list in Xcode:

.package(url: "https://github.com/danyaffff/KeychainStorage.git", .upToNextMajor(from: "1.0.0"))

Requirements

  • iOS 15
  • macOS 12
  • tvOS 15
  • watchOS 8
  • visionOS 1

Usage

You can use the @KeychainStorage similarly to other storages.

struct WriterView: View {
	@KeychainStorage(provider: myKeychainProvider.shared, key: "someKey")
	private var someValue = false
	
	var body: some View {
		Button("Toggle") {
			someValue.toggle()
		}
	}
}

struct ReaderView: View {
	@KeychainStorage(provider: myKeychainProvider.shared, key: "someKey")
	private var someValue = false
	
	var body: some View {
		Text("\(someValue)")
	}
}

First, confirm your Keychain accessor class to the KeychainProvider protocol. This can be either an object from a third-party library or your self-written solution.

Next, create a property wrapper with .init(wrappedValue:provider:key:) initializer inside a View.

A wrappedValue that can be any object that confirms Sendable, Equatable, and Codable protocols. If this was the first time when the value was specified by a certain key, this value will be written to Keychain. It is also possible to initialize (or assign) a wrapped value with an Optional type, the nil value will be automatically detected and will cause the object at the specified key to be deleted.

A keychainProvider object that confirms a KeychainProvider protocol and interacts with Keychain. To be honest, there are no limitations that object, confirmed to the protocol, must interact with Keychain. It can be any other database that supports storing, reading, and deleting Data objects (or it can be an empty object just like in ExampleView.swift, then it can be used as a shared @State property wrapper), but it primary designed to be used with Keychain.

A key is a string value that specifies which object in the database will be interacted with. Property wrappers with the same key in different views will interact with the same object. If you set different data types for the same key, the first one (which was written to Keychain first) will be chosen, and changes proposed by the second one will be discarded.

Finally, you can modify the stored value just like any other wrapped values, and all changes will reflect on the view. If, for some reason, the new value cannot be encoded as a Data (and thus cannot be written to Keychain), the property wrapper will continue to store the old one. If you set a non-Optional type, then you will not be able to delete a value from Keychain through the property wrapper.

Lisence

KeychainStorage is available under the MIT License

About

A simple SwiftUI property wrapper for synchronized interaction with Keychain.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages