The title reflects what I think is happening, but I haven't been able to prove it beyond a shadow of a doubt. I've added a number of log calls to our sentry instance to try to narrow down what's happening
Background: Our app has a root VC called LoadingViewController which has some logic to determine whether we have a logged in user or not. If we do, it shows the homescreen, if we don't it shows the login/register screen. Intermittently, the login screen is shown when the homescreen should show. Our app uses JWT to authenticate with our backend, but I'm fairly confident token expiry is not the issue; the logging around an expired token forcing a logout is not getting called according to the logs
Research: I've also reviewed these questions/issues and while similar, don't solve what I'm seeing
- Realm query sometimes return empty data I've checked for where I call delete on any User objects or deleting the entire Realm on logout. This code only fires when a user explicitly logs out or a token expires
- Empty DB after application restart I am not doing any manual filesystem work (though I am doing keychain / encrypted Realm which I'll show the code for below)
- Why is my Realm object not saving stored values?
- Realm database object seems empty, but then isn't My user object has the appropriate dynamic properties
- Clear complete Realm Database
What I see: The LoadingViewController starting up. Going into bootUser, not getting a User back from the UserDataService, returning false, and then resetting everything. This is why I think that the User object isn't there when it's being queried for.
This usually happens after I haven't used the app for a while, and not all the time. I haven't been able to force the issue by either force-quitting the app or loading up a bunch of games to try to force it out of memory and going back to it.
I'm at a little bit of a loss. Is there anything I haven't thought of or could check?
Thanks
Code
Subset of our LoadingViewController:
class LoadingViewController: UIViewController {
override func viewDidLoad() {
breadcrumbs.append("LoadingViewController.viewDidLoad \(UserDataService.getCurrentUser()?.id) \(UserDataService.getCurrentUser()?.token)")
super.viewDidLoad()
// determine if we have a user
if self.bootUser() {
return
}
// If we got hereUser is not logged in. wipe it
NSOperationQueue().addOperationWithBlock() {
let realm = KidRealm.realm()
CacheUtils.purgeRealm(realm)
}
}
func bootUser() -> Bool {
breadcrumbs.append("LoadingViewController.bootUser 0 \(UserDataService.getCurrentUser()?.id) \(UserDataService.getCurrentUser()?.token)")
if let user = UserDataService.getCurrentUser(),
let checkToken = user.token
where checkToken != "" {
breadcrumbs.append("LoadingViewController - 1 have user \(UserDataService.getCurrentUser()?.id) \(UserDataService.getCurrentUser()?.token)")
KA.initUser(currentUser)
// user is a full user, bring them to the homepage
if let vc = self.storyboard?.instantiateViewControllerWithIdentifier("betacodeview") as? BetaCodeViewController {
breadcrumbs.append("LoadingViewController - 3 betacodeview \(UserDataService.getCurrentUser()?.id) \(UserDataService.getCurrentUser()?.token)")
self.performSegueWithIdentifier("gotohomeview", sender: self)
return true
}
breadcrumbs.append("LoadingViewController - 4 skip? \(UserDataService.getCurrentUser()?.id) \(UserDataService.getCurrentUser()?.token)")
logLogoutIssues("bootUser with token. !full !vc \(UserDataService.count())")
}
breadcrumbs.append("LoadingViewController - 5 no token / user \(UserDataService.getCurrentUser()?.id) \(UserDataService.getCurrentUser()?.token)")
logLogoutIssues("bootUser with no token. token: (\(token)) \(UserDataService.count())")
return false
}
Subset of the User model
class User: Object {
dynamic var id: Int = -1
dynamic var UUID = ""
dynamic var email: String?
dynamic var firstName: String?
dynamic var lastName: String?
dynamic var facebookId: String?
dynamic var instagramId: String?
dynamic var photoUrl: String?
dynamic var token: String?
dynamic var fullUser = false
dynamic var donationPercent: Int = 0
// Extra info
dynamic var birthday: NSDate?
dynamic var phone: String?
dynamic var address1: String?
dynamic var address2: String?
dynamic var zip: String?
dynamic var city: String?
dynamic var state: String?
// User settings
dynamic var allowNotification: Bool = true
// temporary in memory, not saved
var profilePhoto = NSData()
override static func primaryKey() -> String? {
return "UUID"
}
func setUuid(UUID: String) {
self.UUID = UUID
}
func setUuid() {
self.UUID = SwiftyUUID.UUID().CanonicalString()
}
}
Subset of the UserDataService which the LoadingViewController uses to get the current user
struct UserDataService: Saveable, Deletable {
static func getCurrentUser(realm: Realm) -> User? {
let getUser = realm.objects(User)
if let user = getUser.first {
return user
}
return nil
}
static func getCurrentUser() -> User? {
let realm = KidRealm.realm()
return UserDataService.getCurrentUser(realm)
}
}
Subset of CacheUtils, used to clear the realm for the user to login or register (also called during logout)
struct CacheUtils {
static func purgeRealm(realm: Realm) {
try! realm.write {
realm.deleteAll()
}
}
}
Lastly, KidRealm, which sets up the encryption. This is used both in the main app, and a background process when push notifications come in
struct KidRealm {
static func realm() -> Realm {
let key = getKey()
let configuration = Realm.Configuration(encryptionKey: key)
let realm = try! Realm(configuration: configuration)
return realm
}
static func getKeyString(key: NSData) -> String {
return "\(key)".stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: "<>")).stringByReplacingOccurrencesOfString(" ", withString: "")
}
static func getKey() -> NSData {
// Identifier for our keychain entry - should be unique for your application
let keychainIdentifier = "com.kidfund.kidfund1.keychain"
let keychainIdentifierData = keychainIdentifier.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
// First check in the keychain for an existing key
var query: [NSString: AnyObject] = [
kSecClass: kSecClassKey,
kSecAttrApplicationTag: keychainIdentifierData,
kSecAttrKeySizeInBits: 512,
kSecReturnData: true,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock
]
// To avoid Swift optimization bug, should use withUnsafeMutablePointer() function to retrieve the keychain item
// See also: https://stackoverflow.com/questions/24145838/querying-ios-keychain-using-swift/27721328#27721328
var dataTypeRef: AnyObject?
var status = withUnsafeMutablePointer(&dataTypeRef) { SecItemCopyMatching(query, UnsafeMutablePointer($0)) }
if status == errSecSuccess {
return dataTypeRef as! NSData
}
// No pre-existing key from this application, so generate a new one
let keyData = NSMutableData(length: 64)!
let result = SecRandomCopyBytes(kSecRandomDefault, 64, UnsafeMutablePointer<UInt8>(keyData.mutableBytes))
assert(result == 0, "Failed to get random bytes")
// Store the key in the keychain
query = [
kSecClass: kSecClassKey,
kSecAttrApplicationTag: keychainIdentifierData,
kSecAttrKeySizeInBits: 512,
kSecValueData: keyData,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock
]
status = SecItemAdd(query, nil)
assert(status == errSecSuccess, "Failed to insert the new key in the keychain")
return keyData
}
}
Update 1:
I also have a global currentUser helper, which wraps calls to my UserDataService. I don't think this is an issue, but adding for completeness. This exists for legacy reasons and is on the list to be refactored out at some point.
var currentUser: User {
get {
if let user = UserDataService.getCurrentUser() {
return user
}
breadcrumbs.append("Getting empty currentUser \(UserDataService.count())")
logLogoutIssues("Getting empty currentUser \(UserDataService.count())")
return User()
}
}
Update 2:
Based on the thread that Peba pointed me to, I'm pushing some fixes now that include:
- extra logging around the keychain activity
- a sleep(1) retry if it doesn't get the key on the first try
- same for the write
- cache the key in memory so I don't hit the keychain as much (not thrilled about this but it is what it is)
- adding the Keychain Sharing