This library makes session management easier. There are a few main goals:
- Provide a simple way to create "session" objects for storing, updating, deleting, and refreshing session-related data (credentials, tokens, etc.).
- Provide a pre-built
UserSession
to simplify the work needed to deal with user login/logout. - Broadcast login/logout/update notifications when your model object changes.
- Store your model object in a secure storage mechanism since it usually contains sensitive information.
- Session - A base class for creating something that can store, retrieve, and delete an item in a
SessionContainer
. Can post notifications by providing something that conforms toNotificationPosting
(NotificationCenter
conforms to this by default). - SessionContainer - A container for storing data to the keychain.
- Refreshable - Represents something that can be refreshable. In our use case, a
Session<T>
. - NotficationPosting - Represents something that can post a notification.
- UserSession - Handles storage, deletion, and retrieval of the current user. Broadcasts notifications when user session state changes. Can call a
RefreshHandler
block if provided. - KeychainStorageContainer - A container that uses the keychain as the backing store. You can make your own container by subclassing
SessionContainer
. - KeychainContainerConfig - A class to configure the
KeychainStorageContainer
for use.
SessionTools out of the box uses the keychain to store your session data. To allow for maximum flexibility, you can use the SessionTools/Base
subspec to integrate SessionTools without the keychain dependencies. Going forward, we are assuming you are using this on iOS and want to use the keychain.
You've probably already created some variation of this in your codebase.
struct Model: Codable {
let firstName: String
let lastName: String
let email: String
let token: String
}
The default container configuration uses an unmanaged keychain container. This means the framework will make no attempt to remove the session's data on your behalf and you will be responsible for removing this session's data by calling Session.deleteItem()
when needed. Because of differences between OS versions, we cannot make any guarantees on how long the data will persist in the keychain beyond the current install. For more discussion, see below.
let config = KeychainContainerConfig(keychainName: "your.keychain.name")
If you only want the session's data to persist for the current installation, instantiate your KeychainContainerConfig
with lifecycle KeychainLifecycle.currentInstall()
and pass in an installation identifier. This identifier should remain stable for the current installation but change between installations.
This installation indentifier is prepended to the keychain name before running keychain operations. Because the identifier changes between installations the previous key will no longer match. You could theoretically still get that key back if you reuse a previous installation identifier, but because of differences between OS versions, we cannot make any guarantees on how long the data will persist in the keychain beyond the current install. For more discussion, see below.
let managedConfig = KeychainContainerConfig(keychainName: "com.app.name", lifecycle: .currentInstall(identifier: installationIdentifier))
let container = KeychainStorageContainer<Model>(config: config)
You can also create your own object conforming to SessionContainer
and instantiate it if you're not wanting to use the default keychain storage mechanism.
struct MyStorageContainer: SessionContainer {
func hasItem(forIdentifier identifier: String) -> Bool {
// ...
}
func item(forIdentifier identifier: String, jsonDecoder: JSONDecoder) throws -> Item? {
// ...
}
func removeItem(forIdentifier identifier: String) throws {
// ...
}
func storeItem(_ item: Item, forIdentifier identifier: String, jsonEncoder: JSONEncoder) throws {
// ...
}
}
let anyContainer = AnySessionContainer(container)
You just need to supply your model object's type, the container to store it in, and the key that will be associated with your object in the storage container.
let session = Session<Model>(container: anyContainer, storageIdentifier: "identifier.for.your.model.object")
Optionally, conform to Refreshable
if you want to automatically handle refreshing your model when it's expired (e.g. an API token).
class ModelSession: Session<Model>, Refreshable {
// your class code here
// MARK: - Refreshable
func refresh(completion: @escaping RefreshCompletion) {
// your refresh code here
completion(nil)
}
}
Option 3 (Most Common) - Use UserSession<T>
, a Session<T>
subclass already setup for you to deal with common log in/log out operations.
let userSession = UserSession<Model>(container: anyContainer, storageIdentifier: "identifier.for.your.model.object", notificationPoster: NotificationCenter.default)
You can also supply a refreshHandler
to the UserSession initializer if you want to automatically handle refreshing your model when it's expired (e.g. an API token).
private static func userRefreshHandler(_ completion: @escaping RefreshCompletion) -> Void {
// your refresh code
completion(nil)
}
let userSession = UserSession<Model>(container: container, storageIdentifier: "identifier.for.your.model.object", notificationPoster: NotificationCenter.default, refreshHandler: userRefreshHandler)
Now you can easily get a reference to your app's current user.
let currentUser = userSession.currentUser
You can also check if there is currently a user logged in.
let isUserLoggedIn = userSession.isLoggedIn
UserSession<T>
also contains methods that can be called to log in, log out, or update the information when you deem appropriate.
do {
try userSession.didLogIn(model)
try userSession.didLogOut(nil) // Optionally provide the error that triggered the logout
try userSession.didUpdate(model)
} catch {
// Handle container read/write errors here
}
Parts of your code can optionally observe these log in/out/update events by subscribing to the Notification.Name.userSessionStateDidChange
notification.
NotificationCenter.default.addObserver(self, selector: #selector(didUpdateUser:), name: .userSessionStateDidChange, object: nil)
Access the userSessionState
property on the notification to easily get the state change that occurred.
@objc private func didUpdateUser(_ notification: Notification) {
guard let sessionState = notification.userSessionState else { return }
// Do something with the state
switch sessionState {
case .loggedIn:
// ...
case .loggedOut(let error): // Optionally get a reference to the error that triggered the logout
// ...
case .updated:
// ...
}
}
To run the example project, you'll first need to use Carthage to install SessionTool's dependency (KeychainAccess.
After installing Carthage, clone the repo:
git clone https://github.com/BottleRocketStudios/iOS-SessionTools.git
Next, use Carthage to install the dependencies:
carthage update
From here, you can open up SessionTools.xcworkspace
and run the examples:
- iOS 9.0+
- watchOS 2.0+
- tvOS 9.0+
- macOS 10.9+
- Swift 5.0
dependencies: [
.package(url: "https://github.com/BottleRocketStudios/iOS-SessionTools.git", from: "1.2.0")
]
SessionTools is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'SessionTools'
Or if you're not working in an environment with access to the keychain, use the base subspec:
pod 'SessionTools/Base'
Add the following to your Cartfile:
github "BottleRocketStudios/iOS-SessionTools"
Run carthage update
and follow the steps as described in Carthage's README.
NOTE: Don't forget to add both SessionTools.framework
and the KeychainAccess.framework
dependency to your project if your environment has access to the keychain.
In the past, the keychain data you add from your app persists across installs. While this is still the case, we can't guarantee this will remain the case in future versions. This post summarizes that fact. In iOS 10.3 Beta 2, Apple added a feature to remove all application keychain data on uninstall, but reverted when it caused issues with existing apps. When/if Apple formalizes the behavoir, we will formalize here as well.
See the CONTRIBUTING document. Thank you, contributors!