Parse: Update user location upon app open from background/inactive state (Swift) - swift

I have a location-based social networking iPhone app written in Swift and using the Parse SDK.
When the app launches, if the user is logged in, the PFQueryTableViewController is loaded. This is the home page that basically shows a live feed of posts by users within a x-mile radius where users can like or comment on those posts.
Currently, when the user moves to a new location (outside the set mile radius) then goes back into the app, the same feed shows, even if the user pulls-to-refresh the tableview. The new feed of posts only updates to the new location when the user quits the app and launches the app again.
My goal is to have the user's current location update after the user exits (not quits) the app by pressing the iPhone home button or multi-tasking to another app, then opens (not relaunches) the app again. In other words, the feed should update to the new location when the app becomes active again. I think I may have to somehow call some of the functions in the PFQueryTableViewController from applicationDidBecomeActive but I am not sure.
I greatly appreciate any help. Thank you so much.
In my PFQueryTableViewController:
override func viewDidLoad() {
...
queryForLocation()
queryForLikes()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.tableView.reloadData()
self.loadObjects()
}
func queryForLocation() {
/* From documentation:
When the following code is run, the following happens:
1) An internal CLLocationManager starts listening for location updates (via startsUpdatingLocation).
2) Once a location is received, the location manager stops listening for location updates (via stopsUpdatingLocation) and a PFGeoPoint is created from the new location. If the location manager errors out, it still stops listening for updates, and returns an NSError instead.
3) Your block is called with the PFGeoPoint
***Are we supposed to use `startsUpdatingLocation` somewhere? (So that it updates the location (currLocation) because it is used in queryForTable() to return the query***
*/
PFGeoPoint.geoPointForCurrentLocationInBackground {
(geoPoint: PFGeoPoint?, error: NSError?) -> Void in
if error == nil {
// store the new geoPoint in currLocation GeoPoint property
self.currLocation = geoPoint
// this should call queryForTable
self.loadObjects()
} else {
// Unable to fetch the current device's location.
}
}
}
override func queryForTable() -> PFQuery {
let query = PFQuery(className: "Posts")
// self.currLocation obtained from `geoPointForCurrentLocationInBackground` call in queryForLocation() but does not seem to be update to new location on app open (not launch).
if let userLocation = self.currLocation {
query.whereKey("location", nearGeoPoint: userLocation, withinMiles: 5)
query.includeKey("User")
query.cachePolicy = PFCachePolicy.NetworkOnly
// If no objects are loaded in memory, we look to the cache first to fill the table and then subsequently do a query against the network.
if (self.objects?.count == 0) {
query.cachePolicy = PFCachePolicy.CacheThenNetwork
}
query.limit = 200;
return query
} else {
/* How the application should react if there is no location available */
return PFQuery()
}
}

You are calling your queryForLocation() only in your viewDidLoad method but when the app goes background then foreground, viewDidLoad is not recalled, that's why you don't have a new position.
You might just register for the UIApplicationWillEnterForegroundNotification notification, so you will be notified when the app is brought back to the foreground.
Just add (if you are not using Xcode 7.3, you may have to use selector: "willEnterForeground") to your viewDidLoad method :
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(YourViewController.comeFromBackground), name: UIApplicationWillEnterForegroundNotification, object: nil)
func comeFromBackground() {
// This is where you should do your request for location again
}
So everytime your app goes background->foreground this function will be called.
Don't forget to remove the observer in the viewDidDisappear :
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIApplicationWillEnterForegroundNotification, object: nil)
Hope this will help you !

Related

Unable to update NSTouchBar programmatically

I am currently developing a very simple Live Scores MAC OSX app for personal use where I show a bunch of labels (scores) on the touch bar. What I am trying to achieve in a few steps:
Fetch live soccer scores from a 3rd party API every 30 seconds
Parse the scores and make them into labels
Update the touch bar with new scores
[Please note here that this app will not be published anywhere, and is only for personal use. I am aware of the fact that Apple strictly advises against such type of content in the Touch Bar.]
Here is the code that I wrote following basic Touch Bar tutorial from RW (https://www.raywenderlich.com/883-how-to-use-nstouchbar-on-macos). Skeleton of my code is picked from the RW tutorial:
In WindowController (StoryBoard entry point), override makeTouchBar like this:
override func makeTouchBar() -> NSTouchBar? {
guard let viewController = contentViewController as? ViewController else {
return nil
}
return viewController.makeTouchBar()
}
In ViewController, which is also the Touch Bar Delegate, implement the makeTouchBar fn:
override func makeTouchBar() -> NSTouchBar? {
let touchBar = NSTouchBar()
touchBar.delegate = self
touchBar.customizationIdentifier = .scoresBar
touchBar.defaultItemIdentifiers = [.match1, .flexibleSpace, .match2, ... , .match10]
return touchBar
}
NSTouchBarDelegate in ViewController. scores is where I store my fetched scores (See 5). I return nil for views if scores aren't fetched yet:
extension ViewController: NSTouchBarDelegate {
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
if (<scores not fetched yet>) {
return nil
}
// matchNum is the match number for which I am showing scores for
let customViewItem = NSCustomTouchBarItem(identifier: identifier)
customViewItem.view = NSTextField(labelWithString: self.scores[matchNum ?? 0])
return customViewItem
}
}
To fetch scores periodically I am running a scheduled task Timer in viewDidLoad() of my viewcontroller like this:
_ = Timer.scheduledTimer(timeInterval: 30.0, target: self, selector: #selector(ViewController.fetchScores), userInfo: nil, repeats: true)
And finally, this is my fetchScores function that also makes a call to update the Touch Bar:
#objc func fetchScores() {
let url = "<scores api end point>"
Alamofire.request(url).responseJSON { response in
if let json = response.result.value {
// update self.scores here and fill it with latest scores
if #available(OSX 10.12.2, *) {
//self.touchBar = nil
self.touchBar = self.makeTouchBar() // This is where I am calling makeTouchBar again to update Touch Bar content dynamically
}
}
}
My understanding from the code above is that once I make a call to makeTouchBar in fetchScores and assign it to my viewcontroller's touchBar property, it should ideally call touchBar(:makeItemForIdentifier) delegate function to update the Touch Bar view (SO thread on this). But in my case, touchBar(:makeItemForIdentifier) is never called. The only time touchBar(:makeItemForIdentifier) is called is the first time, when makeTouchBar is called from my WindowController (See point 1 above). And since scores have not been retrieved yet, my touch bar remains empty.

Creating an observer to check if MediaPlayer playbackState is paused or not

I have a music app and I wish to determine if playback has been paused while the app was closed (due to an event like a phone call or AirPods being taken out of ear etc)
My first approach was to run a func inside of viewWillAppear that checked
if mediaPlayer.playbackState == .paused {
...
}
If it was paused I updated the play/pause button image. However, this did not work, the play/pause button would still show Play even if it was paused.
Next, I tried adding an observer to the viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(self.wasSongInterupted(_:)), name: UIApplication.didBecomeActiveNotification, object: self.mediaPlayer)
The self.wasSongInterupted I call is
#objc func wasSongInterupted(_ notification: Notification) {
DispatchQueue.main.async {
if self.mediaPlayer.playbackState == .paused {
print("paused")
self.isPlaying = false
self.playPauseSongButton.isSelected = self.isPlaying
} else if self.mediaPlayer.playbackState == .playing {
self.isPlaying = true
self.playPauseSongButton.isSelected = self.isPlaying
}
}
}
However, I am still having the same issue.
What is the best way to determine if my music player is playing or paused when I reopen the app?
Edit 1: I Edited my code based on comments.
wasSongInterrupted was not being called, and through breakpoints and errors I discovered the code was mostly not needed. I changed my code to be
func wasSongInterrupted() {
DispatchQueue.main.async {
if self.mediaPlayer.playbackState == .interrupted {
var isPlaying: Bool { return self.mediaPlayer.playbackState == .playing }
print("Playback state is \(self.mediaPlayer.playbackState.rawValue), self.isPlaying Bool is \(self.isPlaying)")
self.playPauseSongButton.setImage(UIImage(named: "playIconLight"), for: .normal)
//self.playPauseSongButton.isSelected = self.isPlaying
}
}
}
and inside my AppDelegate's applicationDidBecomeActive I have
let mediaPlayerVC = MediaPlayerViewController()
mediaPlayerVC.wasSongInterupted()
Now the code runs, however I have an issue.
If I run the following code:
if self.mediaPlayer.playbackState == .interrupted {
print("interrupted \(self.isPlaying)")
}
and then make a call and come back to the app it will hit the breakpoint. It will print out interrupted as well as false which is the Bool value for self.isPlaying
However if I try to update the UI by
self.playPauseSongButton.isSelected = self.isPlaying
or by
self.playPauseSongButton.setImage(UIImage(named: "playIconLight.png"), for: .normal)
I get an error message Thread 1: EXC_BREAKPOINT (code=1, subcode=0x104af9258)
You trying to update you player UI from viewWillAppear. From Apple Documentation:
viewWillAppear(_:)
This method is called before the view controller's view is about to be added to a view hierarchy and before any animations are configured for showing the view.
So if your app was suspended and the becomes active again, this method won't be called, because your UIViewController is already at Navigations Stack.
If you want to catch the moment when your app becomes active from suspended state, you need to use AppDelegate. From Apple Documentation:
applicationDidBecomeActive(_:)
This method is called to let your app know that it moved from the inactive to active state. This can occur because your app was launched by the user or the system.
So you need to use this method at your AppDelegate to handle app running and update your interface.
UPDATE
You saying the inside this AppDelegate method you're doing
let mediaPlayerVC = MediaPlayerViewController()
mediaPlayerVC.wasSongInterupted()
That's wrong because you're creating a new view controller. What you need to do, is to access you existing view controller from navigation stack and update it.
One of the possible solutions is to use NotificationCenter to send a notification. You view controller should be subscribed to this event of course.
At first, you need to create a notification name
extension Notification.Name {
static let appBecameActive = Notification.Name(rawValue: "appBecameActive")
}
Then in you AppDelegate add following code to post your notifications when app becomes active
func applicationDidBecomeActive(_ application: UIApplication) {
NotificationCenter.default.post(name: .appBecameActive, object: nil)
}
And finally in your view controller add to subscribe it on notifications
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(wakeUp),
name: .appBecameActive,
object: nil)
...
}
#objc func wakeUp() {
// Update your UI from here
}
Hope it helps you.

Loading Data from Bundled Realm Database in Swift

I’m relatively new to Realm. My task is to bundle a RealmDB and make it writable. Thus far I have copied the bundled realm file into the project and implemented the following code in the app delegate. Above the "func application(application: UIApplication, didFinishLaunchingWithOptions” I used the following function:
func bundleURL(name: String) -> NSURL? {
return NSBundle.mainBundle().URLForResource("data", withExtension: "realm") }
And below didFinishLaunchingWithOptions, I used the following:
if let v0URL = bundleURL("data.realm") {
do {
try NSFileManager.defaultManager().removeItemAtURL(defaultURL)
try NSFileManager.defaultManager().copyItemAtURL(v0URL, toURL: defaultURL)
} catch {}
The issue is that I have to load the app twice to get the data to show up in a MapViewController, which is the first controller upon launch. In this case, I want map pins in the MapViewController to automatically appear upon build. I tried to implement a notification in the MapViewController using the following:
let results = try! Realm().objects(Spaces)
notificationToken = results.addNotificationBlock {[weak self](changes: RealmCollectionChange<Results<Sapces>>) in
self!.populateMap()
I also tried to implement a Database Manager:
func getDBItems() -> [Spaces] {
let dbItemsFromRealm = try! Realm().objects(Spaces)
var bathroom = [Spaces]()
if dbItemsFromRealm.count > 0 {
for dbItemsInRealm in dbItemsFromRealm {
let spaces = dbItemsInRealm as Spaces
space.append(space)
}
}
return space
}
}
However, I can’t get the pins to load upon launch. Any help would be much appreciated.
The behavior you describe is what I'd expect to see if you've already opened the Realm at the target path prior to copying the bundled Realm over to that location. You can confirm this by putting a breakpoint on the Realm initializer and on your code that calls removeItemAtURL and seeing which is hit first.

Pass saved Workout from Watch to iPhone

On Watch, I'm able to pass a saved workout from the WorkoutInterfaceController to the SummaryInterfaceController. But I was wondering how to pass the saved workout from the Watch to the iPhone (so I can display it in a Summary View Controller too).
Do you know? Or is there a better way I'm supposed to do this?
Here's what I use to pass the saved workout from WorkoutInterfaceController to the SummaryInterfaceController:
private func saveWorkout() {
// Create and save a workout sample
let configuration = workoutSession!.workoutConfiguration
let isIndoor = (configuration.locationType == .indoor) as NSNumber
print("locationType: \(configuration)")
let workout = HKWorkout(activityType: configuration.activityType,
start: workoutStartDate!,
end: workoutEndDate!,
workoutEvents: workoutEvents,
totalEnergyBurned: totalEnergyBurned,
totalDistance: totalDistance,
metadata: [HKMetadataKeyIndoorWorkout:isIndoor]);
healthStore.save(workout) { success, _ in
if success {
self.addSamples(toWorkout: workout)
}
}
WKInterfaceController.reloadRootControllers(withNames: ["SummaryInterfaceController"], contexts: [workout])
}
private func addSamples(toWorkout workout: HKWorkout) {
// Create energy and distance samples
let totalEnergyBurnedSample = HKQuantitySample(type: HKQuantityType.activeEnergyBurned(),
quantity: totalEnergyBurned,
start: workoutStartDate!,
end: workoutEndDate!)
// Add samples to workout
healthStore.add([totalEnergyBurnedSample], to: workout) { (success: Bool, error: Error?) in
if success {
// Samples have been added
}
}
}
Let me know if any questions or information needed, thanks!
As a part of my research and development,I discovered how the pairing of the iPhone and the Apple Watch has the potential to be useful.
In this case, tapping on a button on the Watch app will send text on the iPhone.
To make a simple demo of this functionality, place a button on the WatchKit interface and a label on the iOS app’s storyboard.
Now, hook up the button to the WatchKit Interface Controller as an IBAction in order to respond to button tap events. Also hook up the Label to the UI View Controller as an IBOutlet.
In the Interface Controller, we make up a string variable to send to the label and in the button’s IBAction method, make a dictionary that includes the string variable you made. This dictionary is what is passed to the iPhone app.
class InterfaceController: WKInterfaceController {
Var str: String = "Hello Phone"
#IBAction func button() {
let dict: Dictionary = ["message": str]
}
Use the following method to send the dictionary to the iPhone.
WKInterfaceController.openParentApplication(dict, reply: {(reply, error) -> void in
print("Reply receive from iPhone app")
})
In the AppDelegate of the iOS app, add the following application method. This is what will handle the previous methods communication from the Watch. Also we can use a notification to notify a view controller that data has been received and to pass it along.
func application(application: UIApplication, handleWatchkitExtensionRequest userInfo: [NSObject : AnyObject]?, reply:(([NSObject : AnyObject]!) {
NSNotificationCenter.defaultCenter().postNotificationName("WatchKitReq", object: userInfo)
}
Finally in the view controller, make a listener for the notification that will update the label’s text to the string that was sent with the data.
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector("handleWatchKitNotification:"), name: "WatchKitReq", object: nil)
}
func handleWatchKitNotification(notification: NSNotification) {
if let userInfo = notification.object as? [String : String] {
label.text = userInfo["message"]
}
}
Hope this will help you to understand. For more concerns you can look on this,
Delegate Method
To do this you have to create an App Group which is essentially a space which both apps can use. It was brought in with the exetension framework in iOS8 so apps can communicate with their Today widgets, or custom keyboards, and amongst other applications.
ADD CAPABILITIES
The first thing we have to do is add the app group capability to both our iPhone and Watch Watch Extension targets.
To do this open up the project settings (blue icon at the top of the list of files) and select the iPhone target. You will need to select the “capabilities” tab at the top of the page and scroll down to turn on app groups.
This requires a connected developer profile, and will take a while to enable. You’ll need to do the same steps to switch on app groups for the watch kit extension also.
Next you need to ensure that the app group string is an identifier string you want and that makes sense for your app, it must start with the word group or it complains. You can also add multiple groups if you wish. Whatever you pick they must be enabled with a blue tick (again this might take a while) and are exactly the same for both the iPhone and Watch extension targets!
To use App Groups, it’s not that different or difficult to use than NSUserDefaults:
var userDefaults = NSUserDefaults(suiteName: "group.com.example.My-App")
userDefaults.setObject(true, forKey: "isDarkModeEnabled")
userDefaults.synchronize()
The only differences here are how NSUserDefaults is instantiated and calling synchronize at the end. You feed it the container ID to the constructor parameter called “suiteName”, then call “synchronize()”, and your data flies to the cloud for other apps and devices to consume.
Taking It to the Next Level
You can take this one step further by creating a class for your app and abstract the underlying storage for your properties. Here’s an example:
public class ConfigurationModel: NSObject {
public static let storageKey = "group.com.example.My-App"
public let userStorage = NSUserDefaults(suiteName: storageKey)
public var isDarkModeEnabled: Bool {
 get {
// Get setting from storage or default
if userStorage?.objectForKey("isDarkModeEnabled") == nil {
userStorage?.setObject(false, forKey: "isDarkModeEnabled")
userStorage?.synchronize()
}
return userStorage?.objectForKey("isDarkModeEnabled")
}
set {
// Set new value in storage
userStorage?.setObject(newValue, forKey: "isDarkModeEnabled")
userStorage?.synchronize()
}
}
At the top of the class, I am declaring my group container ID and creating the NSUserDefault object out of it. Then my properties for the class have getters and setters to store the data to the App Group. If the key doesn’t exist, it creates it with a default value and synchronizes it. Using the class from this point forward is simple:
var configModel = ConfigurationModel()
configModel.isDarkModeEnabled = true
This property is stored in the cloud! Everything is abstracted away for you. You don’t have to be bothered about storing and synchronizing it into the App Group. It’s all done automatically for you in the getters and setters!
Hope, this will help you to understand how you can share data between the iPhone and Apple Watch app.

exc_bad_access Code=2, address=0x38 at the bottom of a function

This is pretty straight forward. I am trying to execute a function that triggers the pre-filling of data. However at the end of the function it dies. I am executing all of this within the AppDelegate file.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
// Override point for customization after application launch.
let initialize = Initialize()
// Check to see if the application ID exists
if (NSUserDefaults.standardUserDefaults().objectForKey("appId") == nil ){
// Refresh the info
self.preload()
println("Data Preload Completed")
} else {
// running anyways to trigger the error.
self.preload()
println("Data Already Preloaded")
}
return true
}
func preload()->Bool {
// Preload static data for the app.
var conditions = Conditions()
conditions.preloadData()
var imageTypes = ImageTypes()
imageTypes.preloadData()
var propertyTypes = PropertyTypes()
propertyTypes.preloadData()
//Save the appId 58AEF58E-2794-4F60-B0A6-0FAB4A943811
NSUserDefaults.standardUserDefaults().setObject("58AEF58E-2794-4F60-B0A6-0FAB4A943811", forKey: "appId")
NSUserDefaults.standardUserDefaults().synchronize()
println(NSUserDefaults.standardUserDefaults().dictionaryRepresentation())
return true
}
I am not sure if this "The" answer to the issue, but I had to delete all of the Simulator profiles, and for each device hit [iOS Simulator].[Reset Content & Settings] and then hit the "Reset" button to confirm. I suspect that somewhere along the way I corrupted the plist file that NSUserDefaults connects to.