We talk about multi-user scenario when an application has multiple persisted user data at the same time and so anyone can be restored to be used in the app. However only one of them can be active or opened. When an application is used more people but the application gets reset before the next user onboards then it is a single user scenario - the default how applications are generated.
To support multi-user scenarios you should replace the default SingleUserOnboardingIDManager
to your custom implementation in the OnboardingController
. This class must implement the OnboardingIDManaging protocol and is responsible to store the onboardingIDs
as well to decide to create a new onboarding session or restore an existing one using the onboardingID
. It is up to your application design how you support these options.
In this sample a view controller will be presented to the users where the user can select one from the existing sessions or create a new one.
- Xcode 10.1+
- SAP Cloud Platform SDK for iOS 3.0 SP01+
- Existing SAP Cloud Platform Mobile Services account
- The
Cloud Platform mobile services
configuration for this application must be created on the server. Create one or import the com.sap.multiuser.sample_1.0.zip at theMobile Services Cockpit
under theMobile Applications
/Native/Hybrid
. This is a simple configuration which points to the publicly available read/write OData service at https://services.odata.org - After downloading this iOS application the server parameters must be updated as well so it will connect to your server. To copy the proper URL go to the
Cloud Platform mobile services
cockpit, select theMulti User Sample
under theMobile Applications
/Native/Hybrid
. Go to theAPIs
tab. Under theAPIs
section there is aServer
item with a URL. This is your applications base server URL. Open theConfigurationProvider.plist
in theOnboarding
folder. Update the value of thehost
key to the host part of theServer
URL (something like https://hcpms-xyztrial.hanatrial.ondemand.com) without thehttps://
prefix and without the '/' suffix. - Add the
SAPCommon.framework
,SAPFoundation.framework
,SAPOData.framework
,SAPOfflineOData.framework
,SAPFiori.framework
, andSAPFioriFlows.framework
to the Embedded Binaries and Linked Frameworks and Libraries.
This app already shows the working end-state of a multi-user enabled app. Here we describe the required changes and enhancements for that in more detail.
Create a new Swift class named MultiUserOnboardingIDManager
and implement the OnboardingIDManaging
protocol with a simple implementation first: stores the id, returns with .onboard
when it is asked in flowToStart
... To make it more realistic we introduce a User
type which contains a name
and the onboardingID
. This will be managed by the MultiUserOnboardingIDManager
. Auto-implement the Codable protocol to ease the persistatition and restoration. Declare a variable coder
and assign the PlistCoder
from SAPFoundation
. This will be used to run the Codable protocol. Also declare an error type used to signal any problem.
```swift
public enum UserError: Error {
case cannotLoad
case internalError
}
public struct User: Codable {
let name: String
let onboardingID: UUID
}
// PlistCoder to ease the code/decode of User types
let coder = SAPFoundation.PlistCoder()
```
It is worth to create some private helper methods to store/retrieve the Users
. This way the users can see user names instead of onboardingIDs
and the type can be extended later on demand.
Let's differentiate the keys in the user defaults with a custom prefix. Also use the
swift let OnboardedUserKeyPrefix = "Onboarded_"
Implement the base functionality: create and read items
```swift
// MARK: - Private methods
private func userdefaultsKey(for onboardingID: UUID) -> String {
return "\(OnboardedUserKeyPrefix)\(onboardingID)"
}
private func user(for onboardingID: UUID) throws -> User {
let key = userdefaultsKey(for: onboardingID)
guard let userData = UserDefaults.standard.object(forKey: key) as? Data else {
throw UserError.cannotLoad
}
let user = try coder.decode(User.self, from: userData)
return user
}
private func saveOnboardedUser(_ user: User) throws {
let key = userdefaultsKey(for: user.onboardingID)
let userData = try coder.encode(user)
UserDefaults.standard.set(userData, forKey: key)
}
private func removeOnboardedUser(_ onboardingID: UUID) {
let key = userdefaultsKey(for: onboardingID)
UserDefaults.standard.removeObject(forKey: key)
}
```
Implement a public function which will be used to list all the items
```swift
/// Returns all the onobarded users
///
/// - Returns: array of users currently stored in the UserDefaults
public func allUsers() -> [User] {
var users = [User]()
for key in UserDefaults.standard.dictionaryRepresentation().keys where key.hasPrefix(OnboardedUserKeyPrefix) {
let onboardingIDString = key.dropFirst(OnboardedUserKeyPrefix.count)
do {
guard let onboardingID = UUID(uuidString: String(onboardingIDString)) else {
Logger.root.error("Failed to create UUID for key: \(onboardingIDString)")
continue
}
let user = try self.user(for: onboardingID)
users.append(user)
}
catch {
Logger.root.error("Failed to load User for key: \(onboardingIDString)", error: error)
}
}
return users
}
```
Create a new UITableViewController
descendant Swift class and name it SelectUserViewController
. This will present the list of existing onboarding sessions.
- Create a small new class for the presented UITableViewCells used by SelectUserViewController. Name it
UserTableViewCell
. - The cell should have a UILabel as an IBOutlet.
- create a const cellID: will be used to get the cell, must be set in the storyboard as well
static let cellID = "UserSelectorCell"
- Declare two properties:
flowSelectionCompletion
: the completion handler to call when the user selected the action- users: array of
User
s; set by theMultiUserOnboardingIDManager
with the available users and used by the tableview to present the items
- Create new IBAction for the add user option:
addUser
- Remove the
numberOfSections
method as we only have one section - In the
tableView(_ tableView: UITableView, numberOfRowsInSection section: Int)
just return with the users.count - uncomment the
cellForRowAt: indexPath
method and dequeue the cell with the id. Fill the cell with the user name blonging to that line/indexPathcell.userName.text = "\(users[indexPath.row].name)"
- implement the
didSelectRowAt indexPath
method to handle the user selection of items. Just call back on the flowCompletionHandler with the username and onboardingID - remove any other unnecessary commented code snippet from the file
Open the Main.storyboard
- Drop in a new UITablewViewController scene
- Set the class of the view controller to
SelectUserViewController
- Set the
Storyboard ID
toSelectUserViewController
- Select the prototype cell and set its class to
UserTableViewCell
- Add a UILabel to the cell and bind it to the userName outlet. Don't forget to set up the autolayout constraints properly
- Set the
identifier
of the prototype cell toUserSelectorCell
- drop a
NavigationItem
to the view controller - drop a
UIBarButtonItem
to the right-top, set itsSystem Item
toAdd
in the Attributes Inspector - Bind the action of this button to the
addUser
IBAction
Finalize MultiUserOnboardingIDManager
. When flowToStart
called we present the UI where the user can select to start a new onboarding session or restore and existing one. So in flowToStart
- get a reference to the main storyboard and load the
SelectUserViewController
from it - set the
flowSelectionCompletion
; in the closure save the selected user name and dismiss the view controller, then call thecompletionHandler
of theflowToSelect
. - Create a new property where we can save the user name. When onboarding finished and the onboardingID mus tbe persisted we can attach the name to the ID creating a
User
and save it to the UseDefaults
var selectedUserName: String?
When the other delegate methods are called make sure to nil out the property!
- set the available users on
SelectUserViewController
selectUserViewController.users = self.allUsers()
- present the view controller in a NavigationController to be able to present the buttons in the Navigation Bar. Make sure you call it on the main queue
DispatchQueue.main.async {
topViewController.present(navCtrl, animated: true)
}
- Bind the
OnboardingController
to theMultiUserOnboardingIDManager
. By default theOnboardingController
is created inside a convenience init ofOnboardingSessionManager
so we will modify that call In yourAppDelegate
find theinitializeOnboarding
method (it is implemented as an extension on AppDelegate) and modify the line initialization ofOnboardingSessionManager
by adding the parameter to the initializer (after theflowProvider
parameter), onboardingIDManager: MultiUserOnboardingIDManager()
Before:
self.sessionManager = OnboardingSessionManager(presentationDelegate: presentationDelegate, flowProvider: self.flowProvider, delegate: self.onboardingErrorHandler)
After:
self.sessionManager = OnboardingSessionManager(presentationDelegate: presentationDelegate, flowProvider: self.flowProvider, onboardingIDManager: MultiUserOnboardingIDManager(), delegate: self.onboardingErrorHandler)
Try out what we have! When the app starts instead of starting with the standard 'Welcome screen' it presents an empty list - this will contain the onboarding sessions. Press the '+' button on the top left corner to initiate a new onboard.
Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved.
This project is licensed under the SAP Sample Code License except as noted otherwise in the LICENSE file.