SwiftUI - background processing in a view - ios

I have a music player that needs to fetch the current song from a function over and over again.
up til now I have put the function inside the view (SwiftUI) using the function .onAppear{} while technically it worked I found that when a user clicks on the view to make it bigger, or even when they close and reopen the app from background mode it is re-running the code as if it has not already started.
So I thought by adding a self.timer?.invalidate() function #State var timer: Timer?
I could potentially allow the script to run while it's open and then stop it when it disappears, that worked all the way up to when I needed to add
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification), perform: { output in
self.timer?.invalidate()
})
The issue now is when I reopen the app it won't restart - not sure why.
But I am starting to feel that there must be a better way to do this function, after all it's only checking a function that is in another file on loop.
So my question: where is the best place to create a code that does this
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in
//CODE HERE
}
Clearly it needs to run once and in the background but not be called again unless the app is opened for the first time.
I was thinking I need to put it inside the AppDelegate file because I know that file is called only once or when certain functions need to run.
I was thinking this section
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//INSERT CODE HEER
}
I know that I would then need to just store the values to something like
UserDefaults.standard.set
and then just get the MediaPlayerView.swift to refresh these values.
Is this the best practice and what others would recommend?, And am I right by saying a view should only be reading information not trying to run functions.

Related

Why async / long running operations in BackgroundTasks don't work?

Trying to use BackgroundTasks for iOS 13+. Long running operations don't seem to work:
// in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "foo.bar.name", using: nil) { task in
print("start!")
task.expirationHandler = {
// Not executed
print("expired!")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// Not executed
print("finish!")
task.setTaskCompleted(success: true)
}
}
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
BGTaskScheduler.shared.cancelAllTaskRequests()
let request = BGProcessingTaskRequest(identifier: "foo.bar.name")
request.earliestBeginDate = nil // or Date(timeIntervalSinceNow: 0) or Date(timeIntervalSinceNow: 5)...
do {
try BGTaskScheduler.shared.submit(request)
} catch let e {
print("Couldn't submit task: \(e)")
}
}
I also tried using a queue with Operation (for which I modeled my flow synchronously). This also didn't work. As soon as there's something that takes a while to complete, it gets stuck.
It doesn't log anything else to the console, no errors, no expired task message. It shows the last message before the long running operation and that's it. I confirmed that it doesn't move forward by storing a preference and examining it when restarting. It's not stored.
I added "foo.bar.name" to the info.plist (in "Permitted background task scheduler identifiers") and enabled capabilities both for background fetch and background processing. I'm testing on an iPhone with iOS 13.3.1 and using Xcode 11.4.1.
Additional notes:
I've been starting the tasks immediately as described here: https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development
I also tested with Apple's demo project. It shows the same problem: The database cleaning operation doesn't complete (I added a log at the beginning of cleanDatabaseOperation.completionBlock and it never shows).
A couple of observations:
You should check the result code of register. And you should make sure you didn’t see your “Couldn't submit task” log statement.
Per that discussion in that link you shared, did you set your breakpoint immediately after the submit call? This accomplishes two things:
First, it makes sure you hit that line (as opposed, for example, to the SceneDelegate methods).
Second, if you just pause the app manually, some random amount of time after the app has gone into background, that’s too late. It has to be in that breakpoint immediately after you call submit. Then do e command. Then resume execution.
Anyway, when I do that, running your code, the BGProcessingTaskRequest ran fine. I’m running iOS 13.4.1 (and like you, Xcode 11.4.1).

How to track the time a user listens an specific podcast

I have an iOS app that is about podcasts and I want to track how long a user listens every podcast. I have tried the basic - when a user plays I save the timestamp and when stops it sends an event with the timestamp difference but it obviously doens't work because there's many edge cases.
I have issues to know when a user has the app in background and stops listening at some point through the the system controls. Also when the user or the system kills the app without tapping on "pause" or "stop". I think these 2 cases are my main non-tracked cases so far.
Any idea how can I build a working solution? I don't want/can't pay an external service - I am merely relying on Firebase.
Thanks!
You can override applicationWillTerminate method in your app, and save a current user progress to UserDefaults.
As docs say, you have few seconds to do it:
This method lets your app know that it is about to be terminated and
purged from memory entirely. You should use this method to perform any
final clean-up tasks for your app, such as freeing shared resources,
saving user data, and invalidating timers. Your implementation of this
method has approximately five seconds to perform any tasks and return.
Your code can look like this:
var player: AVPlayer!
func applicationWillTerminate(_ application: UIApplication) {
UserDefaults.standard.setValue(player.currentTime().seconds, forKey: "curPlayerTime")
}
Then, on application launch, you can restore it:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if let lastPlayerTime = UserDefaults.standard.value(forKey: "curPlayerTime") as? Double {
// update your player
}
return true
}

Change initial View Controller from AppDelegate after async request

I want to change the initial view controller when the user starts the app, but only after I get the information on which one to present, from the server.
It goes like this somewhat like this:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseManager.getUserProfile() { user in
if user?.sports.count == 0 {
// present View Controller 1
} else {
// present View Controller 2
}
}
return true
}
After the call is made, I decide which VC to be shown, but, the problem is that return true is triggered synchronously at the start.
This results in an unpleasant user experience because it shows the initial storyboard (Login.storyboard, as configured in the info.plist) and only after the request has finished (after several seconds), it changes to the correct view controller. I want it to change directly to the view controller, without showing anything else before (even if it will required more waiting from the user).
How can I avoid this? What is the best practice when dealing with such a situation?
I would suggest using Firebase remote config parameters. It stores your data locally. And you can remotely manage it.
Or store value in UserDefaults of FirebaseManager.getUserProfile() beforehand.

Best practice tu run code only once and only when you open an app. Swift 4, Xcode 8.

I am trying to run a function only once only when the app is opened.
Background
I have a stored array (we'll call it storedArray) in user defaults that takes its value from an array that can be modified by the user (userArray).
Problem1: Every time I open the app, storedArray has the values of the previous sessions and of course userArray is empty again. So when the storedArray takes values again from the userArray the storedArray looses its previous values.
Solution to problem1: When you open the app the userArray gets filled with the values of the storedArray (from the previous session).
No problem there. Now:
**Problem 2:**This must happen only once every time the app is opened, otherwise the userArray will start appending the storedArray values many times with repeated values. I figured out solutions 1 and 2, number 3 I tried to follow advice from other posts but failed to understand it and implement it.
Possible Solution 1: Declare a global variable
Struct globalVariables{
static var x = 0
}
Create an if statement so that the desired code runs only if globalVariables.x == 0
if globalVariables.x == 0 {
//code to append values from storedArray to userArray goes here
//Then give a new value to globalVariables.x so that the if never gets accessed again
globalVariables.x == 1
}
This works but I don't know if it as a good practice.
Possible Solution 2: Put the code to be run only once inside the application function in the AppDelegate file.
This functions looks like this.
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//code to append values from storedArray to userArray goes here
}
Again this works but I don't know if it is a good practice.
Possible Solution 3: If I understood correctly, the suggested solution in other posts was to use a singleton. I could never really make this work, (or properly understood what a singleton is).
I made a new .swift file with the following code:
struct Service {
static let sharedInstance = Service(
func getStoredInfo () {
//code to append values from storedArray to userArray goes here
}
}
}
Then call this function in a viewDidLoad with:
Service.sharedInstance.getStoredInfo()
The function still gets called every time the viewDidLoad runs. If the singleton can really help me with this I am totally missing something very basic about what a singleton is and how to implement it. Totally.
Are solutions 1 and 2 good practices? Maybe there are other solutions?
Thanks!
If something need to be done only after launch then 'application didFinishLaunchingWithOptions' is definitely the way to go.
Problem : On iOS you don't control when your app is terminated. You need to store how the app was terminated, so you can restore the session or start a fresh one.

Stub data for XC UI Tests

So, let me explain my problem first.
I don't want to relay on my web-server data, I want to stub data for my XCUITests.
So, I will be sure that it returns correct data in 100% times, as well as sometimes I need to test some specific(e.g. errors, or empty state) cases, which web-server may not return in exact that moment.
So, what I have tried, it is to run the local server in my XCUITest and then stub some requests, but turns out it didn't work out because XC UI Tests are running in complete separate bundle(even separate process) and local server can't be binded to localhost, so my Main app bundle can't connect to this server.
Another solution that I've tried is to pass some params through XCUIApplication().launchArguments, and based on this params - run stubs on main app, but then - it is a bit of a problem: "I have local-proxy server in main app", which I need only for UI testing.
I know, that also, I can just create stub-server and expose it to the web, so to say, create kind of development-env just for UI tesitng, but it seems to extreme for me. Because in that case maintaining only UI tests for my project is a big effort.
So, my question is, does anyone have better solution? Is there any way to get around this problem without modifying your main app and running external web-server?
You can use SBTUITestTunnel. This library allows to dynamically stub network requests (among other things) in a simple fashion.
The easiest way to add the library is to use cocoapods, then override the initialize method of your AppDelegate:
import UIKit
import SBTUITestTunnel
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
override class func initialize() {
SBTUITestTunnelServer.takeOff()
super.initialize()
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
return true
}
}
Once you added that, you're ready to go. You can add/remove stub to network requests to your UI Tests like in the example below:
func testThatSomethingStubbedWorks() {
let app = SBTUITunneledApplication()
app.launch()
let stubId = app.stubRequestsMatching:SBTRequestMatch(SBTRequestMatch.URL("google.com"), returnJsonDictionary: ["key": "value"], returnCode: 200, responseTime: SBTUITunnelStubsDownloadSpeed3G)
// from here on network request containing 'google.com' will return a JSON {"request" : "stubbed" }
...
app.stubRequestsRemoveWithId(stubId) // To remove the stub either use the identifier
app.stubRequestsRemoveAll() // or remove all active stubs
}

Resources