To pass data from my IOS App to my Watch app, I am caching the data into NSUserDefaults (in an App Group) in my iOS AppDelegate. (I call this method both in didFinishLaunching and willTerminate.)
I then pass this data to my Watch app via the App Group and all works well.
The problem is that when I change the data on my iPhone app I want to update the data on my Watch App as well, but it is not updated unless I uninstall and reinstall the Watch App.
I have tried my fetch from App Groups method in both awakeWithContext and willActivate but the data is still stale. How can I refresh the data on the Watch short of uninstalling/reinstalling?
*edit: In other words, the Apple Watch app apparently keeps running on the watch... whereas I would prefer that it was killed, so that when the user changes data on the iPhone app, then taps on the watch app it opens with the new data, instead of just awaking with the stale data.
class InterfaceController: WKInterfaceController {
var nameList: [String]?
var currentUserName: String?
var peopleDict: [String : [String : [String : String]]]?
#IBOutlet weak var personTable: WKInterfaceTable!
private func loadTableData() {
if let list = nameList {
personTable.setNumberOfRows(list.count, withRowType: "PersonTableRowController")
for (index, personName) in enumerate(list) {
if let row = personTable.rowControllerAtIndex(index) as? PersonTableRowController {
row.personLabel.setText(personName)
} else { println("Could not cast as PersonTableRowController") }
}
}
}
func fetchFromAppGroup() {
let defaults = NSUserDefaults(suiteName: "group.com.myndarc.thredz2")
defaults?.synchronize()
if let fetchedPeopleDict: AnyObject = defaults?.valueForKey("peopleDict") {
//save peopleDict locally
peopleDict = fetchedPeopleDict as? [String : [String : [String : String]]]
if let dict = fetchedPeopleDict as? NSDictionary {
nameList = dict.allKeys as? [String]
}
}
if let user: AnyObject = defaults?.valueForKey("currentUser") {
currentUserName = user as? String
}
}
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
}
override func willActivate() {
super.willActivate()
fetchFromAppGroup()
loadTableData()
}
Related
For each UITextView using UserDefaults, I've made a function to save and a function to display.
Whatever text is added needs to be displayed at the time of adding, saved and then displayed again when opening the app again.
If I install the app with ONLY the function to save, quit the app and add the function to display then reinstall without deleting the installed app everything works perfectly.
If I install the app with both functions added it doesn't work.
There has to be a simple solution for this, I'm obviously doing something wrong.
The data from one textView is used to calculate results and then to display them on the other textView.
All data is added with other functions, none by the user.
numberHistoryView.isEditable = false
numberHistoryView.isSelectable = false
func saveHistoryTextView()
{
let defaults = UserDefaults.standard
let numberHistory = numberHistoryView.text
defaults.set(numberHistory, forKey: "combos")
}
func displaySavedHistory()
{
let defaults = UserDefaults.standard
let savedCombos = defaults.object(forKey: "combos") as? String ?? ""
numberHistoryView.text = savedCombos
}
func saveFrequencyTextView()
{
let defaults = UserDefaults.standard
let numberFrequency = numberFrequencyCount.text
defaults.set(numberFrequency, forKey: "frequency")
}
func displaySavedFrequency()
{
let defaults = UserDefaults.standard
let savedFrequency = defaults.object(forKey: "frequency") as? String ?? ""
numberFrequencyCount.text = savedFrequency
}
override func viewWillDisappear(_ animated: Bool)
{
saveHistoryTextView()
saveFrequencyTextView()
}
override func viewWillAppear(_ animated: Bool)
{
displaySavedHistory()
displaySavedFrequency()
}
This depends on the order and timing in which you are calling save and display methods.
When you're installing a fresh app, there will be no data in saved in UserDefaults. So when you call displaySavedHistory() and displaySavedFrequency() methods in viewWillAppear(_:), nothing will be fetched because nothing is saved yet.
Now, when you save the data using saveHistoryTextView() and saveFrequencyTextView() methods in viewWillDisappear(_:) and then you kill and run the app again, the saved data will be fetched and displayed.
Also, since you're saving the data in UserDefaults, and UserDefaults are saved within the sandbox, so the data won't persist when you delete the app. You've to save the data in iCloud or keychain etc. if you want to persist the data even after app deletion.
Once I put my brain into a theta state with the right frequency I managed to figure it out.
Thanks to #Naresh and all other contributors for trying to help as you may have assisted me a little.
The solution basically just required a simple if statement.
Everything now works perfectly.
func saveHistoryTextView()
{
if numberHistoryView.text?.count != nil
{
let defaults = UserDefaults.standard
defaults.set(numberHistoryView.text!, forKey: "combos")
}
}
func displaySavedHistory()
{
let defaults = UserDefaults.standard
if let savedCombos = defaults.string(forKey: "combos")
{
numberHistoryView.text = savedCombos
}
}
func saveFrequencyTextView()
{
if numberFrequencyCount.text?.count != nil
{
let defaults = UserDefaults.standard
defaults.set(numberFrequencyCount.text!, forKey: "frequency")
}
}
func displaySavedFrequency()
{
let defaults = UserDefaults.standard
if let savedFrequency = defaults.string(forKey: "frequency")
{
numberFrequencyCount.text = savedFrequency
}
}
}
override func viewWillDisappear(_ animated: Bool)
{
saveHistoryTextView()
saveFrequencyTextView()
}
override func viewWillAppear(_ animated: Bool)
{
displaySavedHistory()
displaySavedFrequency()
}
You can do it with property observer as:
private let DATA_KEY = "Saved Data"
//After initialising the outlet we can set the data
#IBOutlet weak var textView: UITextView! {
didSet {
textView.text = self.data
}
}
private var data: String {
set {
//Save data in user defaults
UserDefaults.standard.set("The value you will assign", forKey: DATA_KEY)
}
get {
//get the data from user defaults.
return UserDefaults.standard.value(forKey: DATA_KEY) as? String ?? ""
}
}
//UITextViewDelegate: set the text data on end of UITextView editing
func textViewDidEndEditing(_ textView: UITextView) {
self.data = textView.text
}
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
I want to implement an imessage app, however being new to the messages framework and iMessage apps being such a new thing there aren't many resources. So I am following the WWDC video and using Apples providing sample app for a guide.
I have three views, the MessageViewController which handles pretty much all the functionality and then a CreateViewController and a DetailsViewController.
I am simply trying to create an MSMessage from the CreateViewController and display in the DetailsViewController.. then add to the data.
However I get a crash when trying to create the data.
#IBAction func createAction(_ sender: AnyObject) {
//present full screen for create list
self.delegate?.createViewControllerDidSelectAdd(self as! CreateViewControllerDelegate)
}
The data type I am trying to pass is the dictionary from a struct:
struct data {
var title: String!
var date: Date!
var dictionary = ["title" : String(), "Array1" : [String](), "Array2" : [String]() ] as [String : Any]
}
So here's how things are set up;
MessagesViewController
class MessagesViewController: MSMessagesAppViewController, {
// MARK: Responsible for create list button
func composeMessage(for data: dataItem) {
let messageCaption = NSLocalizedString("Let's make", comment: "")
let dictionary = data.dictionary
func queryItems(dictionary: [String:String]) -> [URLQueryItem] {
return dictionary.map {
URLQueryItem(name: $0, value: $1)
}
}
var components = URLComponents()
components.queryItems = queryItems(dictionary: dictionary as! [String : String])
let layout = MSMessageTemplateLayout()
layout.image = UIImage(named: "messages-layout-1.png")!
layout.caption = messageCaption
let message = MSMessage()
message.url = components.url!
message.layout = layout
message.accessibilityLabel = messageCaption
guard let conversation = activeConversation else { fatalError("Expected Convo") }
conversation.insert(message) { error in
if let error = error {
print(error)
}
}
}
}
extension MessagesViewController: CreateViewControllerDelegate {
func createViewControllerDidSelectAdd(_ controller: CreateViewControllerDelegate) {
//CreatesNewDataItem
composeMessage(for: dataItem())
}
}
CreateViewController
/**
A delegate protocol for the `CreateViewController` class.
*/
protocol CreateViewControllerDelegate : class {
func createViewControllerDidSelectAdd(_ controller: CreateViewControllerDelegate)
}
class CreateViewController: UIViewController {
static let storyboardIdentifier = "CreateViewController"
weak var delegate: CreateViewControllerDelegate?
#IBAction func create(_ sender: AnyObject) {
//present full screen for create list
self.delegate?.createViewControllerDidSelectAdd(self as! CreateListViewControllerDelegate)
}
}
Would someone show where I am going wrong and how I can send a MSMessage? If I am able to send the message I should then be able to receive and resend.
One issue I see, without being able to debug this myself:
you are setting your components.queryItems to your dictionary var cast as [String:String], but the dictionary returned from data.dictionary is not a [String:String], but a [String:Any]
In particular, dictionary["Array1"] is an array of Strings, not a single string. Same for dictionary["Array2"]. URLQueryItem expects to be given two strings in its init(), but you're trying to put a string and an array of strings in (though I'm not sure that you're actually getting to that line in your queryItems(dictionary:) method.
Of course, your dataItem.dictionary is returning a dictionary with 4 empty values. I'm not sure that's what you want.
In my iOS app, I have two Firebase-related functions that I want to call within viewDidLoad(). The first picks a random child with .queryOrderedByKey() and outputs the child's key as a string. The second uses that key and observeEventType to retrieve child values and store it in a dict. When I trigger these functions with a button in my UI, they work as expected.
However, when I put both functions inside viewDidLoad(), I get this error:
Terminating app due to uncaught exception 'InvalidPathValidation', reason: '(child:) Must be a non-empty string and not contain '.' '#' '$' '[' or ']''
The offending line of code is in my AppDelegate.swift, highlighted in red:
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate
When I comment out the second function and leave the first inside viewDidLoad, the app loads fine, and subsequent calls of both functions (triggered by the button action) work as expected.
I added a line at the end of the first function to print out the URL string, and it doesn't have any offending characters: https://mydomain.firebaseio.com/myStuff/-KO_iaQNa-bIZpqe5xlg
I also added a line between the functions in viewDidLoad to hard-code the string, and I ran into the same InvalidPathException issue.
Here is my viewDidLoad() func:
override func viewDidLoad() {
super.viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
pickRandomChild()
getChildValues()
}
Here is the first function:
func pickRandomChild () -> String {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
})
return movieToGuess
}
Here is the second function:
func getChildValues() -> [String : AnyObject] {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
})
return movieDict
)
And for good measure, here's the relevant portion of my AppDelegate.swift:
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UITextFieldDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
FIRApp.configure()
return true
}
I'm guessing Swift is executing the code not in the order I expect. Does Swift not automatically wait for the first function to finish before running the second? If that's the case, why does this pairing work elsewhere in the app but not in viewDidLoad?
Edit: The issue is that closures are not called in order.
I'm not sure what your pickRandomChild() and getChildValues() methods are, so please post them as well, but the way I fixed this type issue was by sending the data through a closure that can be called in your ViewController.
For example when I wanted to grab data for a Full Name and Industry I used this. This method takes a Firebase User, and contains a closure that will be called upon completion. This was defined in a class specifically for pulling data.
func grabDataDict(fromUser user: FIRUser, completion: (data: [String: String]) -> ()) {
var myData = [String: String]()
let uid = user.uid
let ref = Constants.References.users.child(uid)
ref.observeEventType(.Value) { (snapshot, error) in
if error != nil {
ErrorHandling.defaultErrorHandler(NSError.init(coder: NSCoder())!)
return
}
let fullName = snapshot.value!["fullName"] as! String
let industry = snapshot.value!["industry"] as! String
myData["fullName"] = fullName
myData["industry"] = industry
completion(data: myData)
}
}
Then I defined an empty array of strings in the Viewcontroller and called the method, setting the variable to my data inside the closure.
messages.grabRecentSenderIds(fromUser: currentUser!) { (userIds) in
self.userIds = userIds
print(self.userIds)
}
If you post your methods, however I can help you with those specifically.
Edit: Fixed Methods
1.
func pickRandomChild (completion: (movieToGuess: String) -> ()) {
var movieCount = 0
movieRef.queryOrderedByKey().observeEventType(.Value, withBlock: { (snapshot) in
for movie in snapshot.children {
let movies = movie as! FIRDataSnapshot
movieCount = Int(movies.childrenCount)
movieIDArray.append(movies.key)
}
repeat {
randomIndex = Int(arc4random_uniform(UInt32(movieCount)))
} while excludeIndex.contains(randomIndex)
movieToGuess = movieIDArray[randomIndex]
excludeIndex.append(randomIndex)
if excludeIndex.count == movieIDArray.count {
excludeIndex = [Int]()
}
let arrayLength = movieIDArray.count
// Put whatever you want to return here.
completion(movieToGuess)
})
}
2.
func getChildValues(completion: (movieDict: [String: AnyObject]) -> ()) {
let movieToGuessRef = movieRef.ref.child(movieToGuess)
movieToGuessRef.observeEventType(.Value, withBlock: { (snapshot) in
movieDict = snapshot.value as! [String : AnyObject]
var plot = movieDict["plot"] as! String
self.moviePlot.text = plot
movieValue = movieDict["points"] as! Int
// Put whatever you want to return here.
completion(movieDict)
})
}
Define these methods in some model class, and when you call them in your viewcontroller, you should be able to set your View Controller variables to movieDict and movieToGuess inside each closure. I made these in playground, so let me know if you get any errors.
Your functions pickRandomChild() and getChildValues() are asynchronous, therefore they only get executed at a later stage, so if getChildValues() needs the result of pickRandomChild(), it should be called in pickRandomChild()'s completion handler / delegate callback instead, because when one of those are called it is guaranteed that the function has finished.
It works when you comment out the second function and only trigger it with a button press because there has been enough time between the app loading and you pushing the button for the asynchronous pickRandomChild() to perform it action entirely, allowing getChildValues() to use its returned value for its request.
My Today Extensions works perfectly on beta 4, and I have implement a simple cache when Today Extensions first loaded.
let defaults = NSUserDefaults(suiteName: "group.ReadWidget") // app group
var feed = [String: String]()
override func viewDidLoad() {
getCache()
// The feed is empty
}
func parseRSS() {
...
// Parse in background
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
self.parser = NSXMLParser(contentsOfURL:NSURL(string:url))
self.parser.delegate = self
self.parser.parse() // populate the feed
self.saveCache() // Save the feed
...
})
})
}
func saveCache() {
defaults.setObject(feed, forKey:"feed")
defaults.synchronize()
}
func getCache() {
if defaults.objectForKey("feed") {
feed = defaults.dictionaryForKey("feed") as [String : String]
}
else {
feed = [:]
}
}
In beta 5, after saveCache(), and when getCache() back in Today Extension reloaded, I always got back an empty dictionary.
I am using NSUserDefaults(suiteName:...) to store cache. I have also tried NSUserDefaults.standardUserDefaults() and the result is the same.
btw, which NSUserDefaults is the preferred method for loading and saving cache? The cache is not shared with the main app.
This works:
var defaults = NSUserDefaults.standardUserDefaults()
var feed = [String:String]()
func getCache() {
if defaults.objectForKey("feed") {
feed = defaults.dictionaryForKey("feed") as [String : String]
}
else {
feed = [:]
}
println("feed: \(feed)")
}