Skip to content

SocketMobile/clubkit-ios

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ClubKit

CI Status Version License Platform

ClubKit provides Membership/Loyalty functionality when paired with our Socket Mobile S550 NFC reader. Developers can use the S550 NFC reader to scan appropriate Mobile Pass and/or RFID cards carried by end users to update their local record. Examples include maintaining number of visits, time of last visit and much more when configured.

Under the hood, ClubKit is an umbrella for our iOS Capture SDK. So naturally, you need to provide credentials to get started.

You may provide your own subclass of MembershipUser class, to maintain more than the default information on end users

override func viewDidLoad() {
    super.viewDidLoad()

    setupClub()
}

private func setupClub() {
    let appKey =        <Your App Key>
    let appId =         <Your App ID>
    let developerId =   <Your Developer ID>
    
    Club.shared.setDelegate(to: self)
        .setCustomMembershipUser(classType: CustomMembershipUser.self)
        .setDispatchQueue(DispatchQueue.main)
        .setDebugMode(isActivated: true)
        .open(withAppKey:   appKey,
              appId:        appID,
              developerId:  developerID,
              completion: { (result) in
                
                if result != CaptureLayerResult.E_NOERROR {
                    // Open failed due to internal error.
                    // Display an alert to the user suggesting to restart the app
                    // or perform some other action.
                }
         })
    
}

By default, ClubKit offers an out-of-the-box user class: MembershipUser

This provides 5 basic values for each user record:

  • userId: A string that uniquely identifiers the user record.
  • username: A string for the user's name.
  • timeStampAdded: The time interval (since UTC Jan 1 1970) of the user record's creation date
  • numVisits: Number of times the user has scanned their mobile pass/RFID card.
  • timeStampOfLastVisit: The time interval (since UTC Jan 1 1970) of the last time the user has scanned their mobile pass/RFID card.

The MembershipUser superclass encodes and decodes its variables (and their values) into and from Data. This allows the record to be synced across different devices.

Using the example of CustomMembershipUser which, aside from the 5 basic values, adds a 6th value: Email Address:

@objcMembers class CustomMembershipUser: MembershipUser {
    // More code coming
}

Use the modifier @objcMembers in the class declaration to signify that the class will be using Objective C objects in our subclass. You will NOT be writing Objective C code here. It is merely a requirement for observing dynamic variables

Step 1/3

  • First, define a variable you would like to observe in this record. As noted before, in this example, you will add an Email Address to this User class.
  • Then, define an enum which conforms to String, CodingKey and CaseIterable. Then define cases for all of your variables NOTE: The case name must match the name of the variable it represents. camelCase, lowercased, UPPERCASED, etc. It must match exactly
@objcMembers class CustomMembershipUser: MembershipUser {

    // Step 1/3
    dynamic var userEmailAddress: String?

    enum CodingKeys: String, CodingKey, CaseIterable {
        case userEmailAddress
    }
    
    // More code coming
}

Step 2/3

  • Next, override an aptly named function called variableNamesAsStrings() -> [String] and return all the case values you created in Step 1, plus the superclass values. This allows your subclass to be synced between different devices. More on that later
@objcMembers class CustomMembershipUser: MembershipUser {

    // Step 1/3
    dynamic var userEmailAddress: String?

    enum CodingKeys: String, CodingKey, CaseIterable {
        case userEmailAddress
    }
    
    // Step 2/3
    override class func variableNamesAsStrings() -> [String] {

        let superclassVariableNames: [String] = super.variableNamesAsStrings()
        
        // Using CaseIterable, map through all CodingKeys enum and return its rawValue
        let mySubclassVariableNames: [String] = CodingKeys.allCases.map { $0.rawValue }
        
        return superclassVariableNames + mySubclassVariableNames
    }
    
    // More code coming
}

Step 3

Finally, provide implementation to the overriden Encodabe and Decodable functions:

@objcMembers class CustomMembershipUser: MembershipUser {

    // Step 1/3
    dynamic var userEmailAddress: String?

    enum CodingKeys: String, CodingKey, CaseIterable {
        case userEmailAddress
    }
    
    // Step 2/3
    override class func variableNamesAsStrings() -> [String] {

        let superclassVariableNames: [String] = super.variableNamesAsStrings()
        
        // Using CaseIterable, map through all CodingKeys enum and return its rawValue
        let mySubclassVariableNames: [String] = CodingKeys.allCases.map { $0.rawValue }
        
        return superclassVariableNames + mySubclassVariableNames
    }
    
    // Step 3/3
    // Encodable
    public override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        
        // Create container to encode your variables
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        // TRY to encode your variable using the key that matches
        try container.encode(emailAddress, forKey: .emailAddress)
        
        // ... Other variables if necessary
    }

    // Decodable
    public required init(from decoder: Decoder) throws  {
        try super.init(from: decoder)
        
        // Create container, again, but this time for decoding your variables
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        // TRY to decode the data into your original variable type and value
        emailAddress = try container.decode(String.self, forKey: .emailAddress)
    }

}

For encoding variables, it uses the matching Key you created in the CodingKeys enum, and encodes the variable to Data. For decoding, its reversed. The Data in the container which matches the specific key is decoded to its original variable.

Using the MembershipUserCollection you can display user records in a UITableView or UICollectionView. It accepts a generic parameter in its initializer. Pass in your custom membership user class:

private let usersCollection = MembershipUserCollection<CustomMembershipUser>()

Step 1/2

First, you need to begin observing changes to user records. Changes include new additions, updates and deletions. You can observe or stop observing changes for user records like so:

var tableView: UITableView...

private let usersCollection = MembershipUserCollection<CustomMembershipUser>()

// ...

private func observeChanges() {
    
    usersCollection.observeAllRecords({ [weak self] (changes: MembershipUserChanges) in
        switch changes {
        case .initial(_):
            
            // Reload tableView with initial data once
            self?.tableView.reloadData()
            
        case let .update(_, deletions, insertions, modifications):
            self?.tableView.performBatchUpdates({
            
                // Reload rows for updated user records
                self?.tableView.reloadRows(at: modifications.map { IndexPath(row: $0, section: 0) }, with: .automatic)
                
                // Insert newly created records
                self?.tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) }, with: .automatic)
                
                // Delete rows for records that have been deleted
                self?.tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) }, with: .automatic)
                
            }, completion: { (completed: Bool) in
                self?.tableView.reloadData()
            })
            break
        case let .error(error):
        
            // Handle error...
        
        }
        
    })
}

private func stopObserving() {
    // Will stop observing changes.
    // Call on viewWillDisappear, etc.
    userCollection.stopObserving()
}

Step 2/2

Next, implement the usual UITableViewDelegate and UITableViewDataSource functions to display the user records:

extension UserListViewController: UITableViewDelegate, UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return usersCollection.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath)
        
        if let user = usersCollection.user(at: indexPath.item) {
            
            // Configure your cell with all of the data in this user record
            
        }
        
        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if let user = usersCollection.user(at: indexPath.item) {
            // Perform action with selected user
        }
    }
}

Syncing user records between devices is as simple as airdropping a file containing user records between the two. Using the function below, you can generate a file containing the locally stored user records and export that file to wherever necessary

func getExportableURLForDataSource<T: MembershipUser>(ofType objectType: T.Type, fileType: IOFileType) -> URL?

objectType will be the CustomMembershipUser class or any other MembershipUser subclass

IOFileType provides two kinds of files:

  • UserList
  • CSV

The UserList file should only be used between two applications using ClubKit. It may be difficult opening it in other environments The CSV file (comma separated values) can be exported to other environments however.

Step 1/3 (Exporting)

if let exportableURL = Club.shared.getExportableURLForDataSource(ofType: CustomMembershipUser.self, fileType: .userList) {
    // Show UIActivityViewController with exportableURL
    
    let activityItems: [Any] = [
        exportableURL
    ]
    
    let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
    
    present(activityController, animated: true, completion: nil)
}

Step 2/3 (Importing)

Importing requires that the application "catches" incoming URLs and decodes user records from the URL

For pre-iOS 13.0 applications, implement the function below in AppDelegate to handle incoming URLs:

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    
    Club.shared.getImportedDataSource(ofType: CustomMembershipUser.self, from: url)
    
    return true
}

For applications that support iOS 13.0 and onward, implement the function below in SceneDelegate:

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    
    if let url = URLContexts.first?.url {
    
        Club.shared.getImportedDataSource(ofType: CustomMembershipUser.self, from: url)
    
    }
}

Step 3/3 (Info.plist)

Lastly, you need to configure your application to import and export this custom exportable URL from Step 1. To do so, you will need to add entries to your Info.plist file that let the application know how to handle the custom file types that ClubKit provides for exporting user records.

The simplest method would be to follow these steps:

  • "Right+Click" on your Info.plist file
  • Open as->
  • Source code
  • Then paste these entries in between the <dict> tags
<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>CFBundleTypeIconFiles</key>
        <array/>
        <key>CFBundleTypeName</key>
        <string>Membership User List Document</string>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>LSHandlerRank</key>
        <string>Owner</string>
        <key>LSItemContentTypes</key>
        <array>
            <string>com.socketmobile.MemberPass.MembershipUserListDocument</string>
        </array>
    </dict>
    <dict>
        <key>CFBundleTypeIconFiles</key>
        <array/>
        <key>CFBundleTypeName</key>
        <string>Membership User CSV Document</string>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>LSHandlerRank</key>
        <string>Owner</string>
        <key>LSItemContentTypes</key>
        <array>
            <string>com.socketmobile.MemberPass.MembershipUserCSVDocument</string>
        </array>
    </dict>
</array>

<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>

<key>UISupportsDocumentBrowser</key>
<false/>

<key>UTExportedTypeDeclarations</key>
<array>
    <dict>
        <key>UTTypeConformsTo</key>
        <array>
            <string>public.data</string>
        </array>
        <key>UTTypeDescription</key>
        <string>Membership User List Document</string>
        <key>UTTypeIconFiles</key>
        <array/>
        <key>UTTypeIdentifier</key>
        <string>com.socketmobile.MemberPass.MembershipUserListDocument</string>
        <key>UTTypeTagSpecification</key>
        <dict>
            <key>public.filename-extension</key>
            <array>
                <string>MUSRL</string>
                <string>musrl</string>
            </array>
        </dict>
    </dict>
    <dict>
        <key>UTTypeConformsTo</key>
        <array>
            <string>public.data</string>
        </array>
        <key>UTTypeDescription</key>
        <string>Membership User CSV Document</string>
        <key>UTTypeIconFiles</key>
        <array/>
        <key>UTTypeIdentifier</key>
        <string>com.socketmobile.MemberPass.MembershipUserCSVDocument</string>
        <key>UTTypeTagSpecification</key>
        <dict>
            <key>public.filename-extension</key>
            <array>
                <string>MUCSV</string>
                <string>mucsv</string>
            </array>
        </dict>
    </dict>
</array>

If you'd like a detailed description about this process, there's a good article on that here

The next step is to implement a ClubKit delegate which provides the opportunity to approve or deny merging the incoming user records with the local store.

ClubKit provides notifications on other events through delegate calls. Conform to ClubMiddlewareDelegate to receive these events.

Notifies receiver of errors

@objc optional func club(_ clubMiddleware: Club, didReceive error: Error)

Notifies receiver that a new MembershipUser object has been created. Use this to show popup views, or update the UI, etc. if desired. NOTE If displaying list of records in UITableView or UICollectionView, refer to this section for updating the list

@objc optional func club(_ clubMiddleware: Club, didCreateNewMembership user: MembershipUser)

Notifies the delegate that a MembershipUser object has been updated. Use this to show popup views, or update the UI, etc. if desired. NOTE If displaying list of records in UITableView or UICollectionView, refer to this section for updating the list

@objc optional func club(_ clubMiddleware: Club, didUpdateMembership user: MembershipUser)

Notifies the delegate that a MembershipUser object has been deleted. Use this to show popup views, or update the UI, etc. if desired. NOTE If displaying list of records in UITableView or UICollectionView, refer to this section for updating the list

@objc optional func club(_ clubMiddleware: Club, didDeleteMembership user: MembershipUser)

Notifies the delegate that an array of MembershipUser objects have been imported from another device Use this to store new list of transferred data in local Realm

@objc optional func club(_ clubMiddleware: Club, didReceiveImported users: [MembershipUser])

This function can be used to determine if the incoming list of user records should be merged with the existing local store For example, an alert is displayed giving the developer, clerk, etc. the opportunity to accept or decline the imported user records.

func club(_ clubMiddleware: Club, didReceiveImported users: [MembershipUser]) {

    var alertStyle = UIAlertController.Style.actionSheet
    if (UIDevice.current.userInterfaceIdiom == .pad) {
        alertStyle = UIAlertController.Style.alert
    }
    
    let title = "Import"
    let message = "Received \(users.count) users to import. Would you like to save them?"
    
    let alertController = UIAlertController(title: title,
                                            message: message,
                                            preferredStyle: alertStyle)
                                            
    let yesAction = UIAlertAction(title: "Yes", style: UIAlertAction.Style.default) { (_) in
        Club.shared.merge(importedUsers: users)
    }
    let noAction = UIAlertAction(title: "No", style: UIAlertAction.Style.cancel) { (_) in
        // Just decline and do nothing
    }
    
    alertController.addAction(yesAction)
    alertController.addAction(noAction)
    
    present(alertController, animated: true, completion: nil)
}

To run the example project, clone the repo, and run pod install from the Example directory first.

  • Xcode 7.3

  • iOS 9.3

ClubKit is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'ClubKit'

Author

Chrishon, chrishon@socketmobile.com

License

ClubKit is available under the MIT license. See the LICENSE file for more info.