UI Testing Failure when displaying UIAlertController with no buttons - swift

We're using a UIAlertController as a loading indicator while a network request occurs. There are no actions associated with this UIAlertController as it is closed automatically when the network activity is completed. We display this alert after the user taps the login button for our app.
When we run our tests, they fail at the point right after this with:
UI Testing Failure - Did not receive view did disappear notification within 2.0s
Per other answers on SO, I've tried to use addUIInterruptionMonitor to handle the alert, but without any success. I think this is because there are no actionable buttons on the UIAlertController. Since there's no action that can be taken on the alert, the interruption monitor just looks like this:
addUIInterruptionMonitor(withDescription: "Loading") { handler in
return true
}
Even with this though, I get the same error. How can I work around this?
EDIT: Relevant UI testing code below:
class UI_Tests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = true
XCUIApplication().launch()
}
func testLogin() {
let app = XCUIApplication()
let tablesQuery = app.tables
let secureTextField = tablesQuery.cells.containing(.staticText, identifier:"PIN").children(matching: .secureTextField).element
secureTextField.tap()
secureTextField.typeText("1234")
app.buttons["Login"].tap()
addUIInterruptionMonitor(withDescription: "Loading") { handler in
return true
}
// Test failure occurs here.
let searchField = tablesQuery.searchFields.element(boundBy: 0)
searchField.tap()
searchField.typeText("hey")
}
}

After reaching out to Apple DTS, it turns out that when displaying a UI interruption / UIAlertController that dismisses based on a timeout, that you need to combine the UI interruption monitor with a timeout-based expectation (otherwise, the interruption monitor will return before the alert has dismissed!).
Using the UI testing example in the question, this approach looks like this:
class UI_Tests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = true
XCUIApplication().launch()
}
func testLogin() {
let app = XCUIApplication()
let tablesQuery = app.tables
let secureTextField = tablesQuery.cells.containing(.staticText, identifier:"PIN").children(matching: .secureTextField).element
secureTextField.tap()
secureTextField.typeText("1234")
app.buttons["Login"].tap()
addUIInterruptionMonitor(withDescription: "Loading") { alert in
self.expectation(for: NSPredicate(format: "exists == false"), evaluatedWith: alert, handler: nil);
self.waitForExpectations(timeout: 10, handler: nil)
return true
}
// Test failure occurs here.
let searchField = tablesQuery.searchFields.element(boundBy: 0)
searchField.tap()
searchField.typeText("hey")
}
}
This expectation will wait for 10 seconds to be filled. If the alert doesn't dismiss after 10 seconds, the expectation won't be met and the test will fail, but if it does, the test will succeed.

Related

Presenting a series of alert view controllers sequentially, then performing a Present Modally segue - simultaneous presentation errors sometimes occur

In a certain app I'm developing, there are occasions where the user may be shown multiple popups (UIAlertViewControllers) in a row. Sometimes, this coincides with a Present Modally segue directly afterwards (i.e. the user presses a button, which is supposed to display all alerts in order, then perform a segue to a different screen). The user has the option of dismissing the alerts manually with the 'OK' button, but if they do nothing, each alert will automatically disappear after a set time.
It isn't absolutely essential that the user sees these popups, as they are just to notify the user that they have gained a new achievement. Users can always check their achievements by going directly to that page.
After a lot of experimentation, I got to the setup I have now. This mostly works as intended, except for the specific case where the user attempts to dismiss an alert manually right before that alert was set to disappear. This causes an error to be logged to the console that it's attempting to present an alert when one is already being presented. Other than this message -- which of course the user won't see :) --, the only issues caused by this error are some alert popups getting skipped, and the possibility that the user must press the button a second time to trigger the Present Modally segue to the next screen.
Although the issue is minor, I'm still looking to eliminate it completely if possible. I'm open to completely redoing the timing mechanism if the current implementation is flawed and someone proposes an alternative. (In other words, I realize this may be an instance of the "XY problem" that's often discussed on Meta!)
Below is an MCVE which reproduces the timing issue.
The desired behavior is to see the 4 alerts pop up in order, followed by the segue from the first view controller to the second. There are two View Controller scenes in Main.storyboard, with segues connecting them in each direction. Each View Controller has a UIButton which is connected to an IBAction to perform the segue to the other VC.
Note that allowing each alert to time out causes no errors. Similarly, dismissing each alert manually as soon as it appears (or shortly afterwards) also causes no errors. The only situation I've encountered where an error may occur is when an alert appears and you attempt to dismiss it very close to when it should auto-dismiss (3 seconds after it appears).
FIRST VIEW CONTROLLER
import UIKit
class ViewController: UIViewController {
// A failsafe in case the event queue timing gets messed up. This kind of error could otherwise cause an infinite cycle of alert view controllers being shown whenever the button is pressed, preventing the main segue from ever being performed. Having this boolean ensures that alert VCs can only be shown the first time the button is pressed, and that regardless of event queue 'success' or 'failure', a subsequent button press will always trigger the main segue (which is what is wanted).
var showedAlerts = false
var alertMessages = ["1st Item", "2nd Item", "3rd Item", "4th Item"]
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func goForward(_ sender: UIButton) {
if !showedAlerts {
for i in 0..<alertMessages.count {
// This function is defined in Constants.swift
K.alerts.showAlertPopup(alertMessages[i], counter: K.alerts.alertCounter, VC: self)
}
showedAlerts = true
}
// Refer to Constants.swift for documentation of these variables
if K.alerts.canPresentNextSegue {
self.performSegue(withIdentifier: K.segues.toSecond, sender: self)
} else {
K.alerts.suspendedSegueParameters = (identifier: K.segues.toSecond, VC: self)
}
}
}
SECOND VIEW CONTROLLER
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func goBack(_ sender: UIButton) {
self.performSegue(withIdentifier: K.segues.toFirst, sender: self)
}
}
CONSTANTS FILE
import UIKit
struct K {
struct segues {
static let toSecond = "toSecondVC"
static let toFirst = "toFirstVC"
}
struct alerts {
// Avoids segue conflicts by showing alert VC popups sequentially, and delaying any 'present modally' segue until all popups have been shown
static var canPresentNextSegue = true {
didSet {
if canPresentNextSegue == true {
if !suspendedAlertParameters.isEmpty {
// Take the first element out of the array each time, not the last, otherwise the order of all popups after the first will be reversed (i.e. will show popups in order of 1st, 4th, 3rd, 2nd)
let p = suspendedAlertParameters.removeFirst()
showAlertPopup(p.alertItem, counter: alertCounter, VC: p.VC)
}
// Don't perform the main segue until all alert popups have been shown! This should be true when the suspendedAlertParameters array is empty.
else if let p = suspendedSegueParameters {
p.VC.performSegue(withIdentifier: p.identifier, sender: p.VC)
suspendedSegueParameters = nil
}
}
}
}
// The purpose of this counter is to ensure that each Alert View Controller has an associated unique ID which can be used to look it up in the alertDismissals dictionary.
static var alertCounter = 0
// Keeps track of which alert VCs have been dismissed manually by the user using the 'OK' button, to avoid the DispatchQueue block in 'showAlertPopup' from setting the status of canPresentNextSegue to 'true' erroneously when the already-dismissed alert 'times out' and attempts to dismiss itself again
static var alertDismissals: [Int: Bool] = [:]
// Tuple representations of any alert view controllers which were not able to be immediately presented due to an earlier alert VC still being active. This allows them to be retrieved and presented once there is an opening.
static var suspendedAlertParameters: [(alertItem: String, counter: Int, VC: UIViewController)] = []
// Analogous to the preceding variable, but for the main 'Present Modally' segue
static var suspendedSegueParameters: (identifier: String, VC: UIViewController)? = nil
static func showAlertPopup(_ alertItem: String, counter: Int, VC: UIViewController) {
alertDismissals[counter] = false
alertCounter += 1 // Automatially increment this, so that the next alert has a different ID
// Present the alert if there isn't one already being presented
if canPresentNextSegue {
let alert = UIAlertController(title: "Test Alert", message: alertItem, preferredStyle: .alert)
let OKButton = UIAlertAction(title: "OK", style: .cancel) { (action) in
alertDismissals[counter] = true
canPresentNextSegue = true
return
}
alert.addAction(OKButton)
VC.present(alert, animated: true, completion: nil)
// Automatically dismiss alert after 3 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if alertDismissals.keys.contains(counter) && !alertDismissals[counter]! {
alert.dismiss(animated: true)
canPresentNextSegue = true
}
}
// Because this current alert is now being shown, nothing else can be presented until it is dismissed, resetting this boolean to 'true' (either with the OK button or the DispatchQueue block)
canPresentNextSegue = false
}
// If there is another alert being presented, store this one in tuple representation for later retrieval
else {
suspendedAlertParameters.append((alertItem: alertItem, counter: counter, VC: VC))
}
}
}
}
Since you are not familiar with RxSwift I present below the entirety of the solution. This solution doesn't use segues. The CLE library takes over all view controller routing for you. It does this with generic coordinators that it creates and destroys for you so you never have to worry about them.
Create a new project.
Import the RxSwift, RxCocoa and Cause_Logic_Effect libraries.
Remove all the swift code in it and the storyboard. Remove the manifest from the info.plist and remove "main" from the "Main interface" entry in the target.
Add a new .swift file to the project and paste all the code below into it.
import Cause_Logic_Effect
import RxCocoa
import RxSwift
import UIKit
// the app delegate for the app.
#main
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = {
let result = UIWindow(frame: UIScreen.main.bounds)
result.rootViewController = ViewController().configure { $0.connect() }
result.makeKeyAndVisible()
return result
}()
return true
}
}
// the main view controller, notice that the only thing in the VC itself is setting up the views. If you use a
// storyboard or xib file, you can delete the viewDidLoad() completely.
final class ViewController: UIViewController {
#IBOutlet var goForwardButton: UIButton!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// create the view. This could be set up in a xib or storyboard instead.
view.backgroundColor = .white
let button = UIButton(frame: CGRect(x: 100, y: 100, width: 50, height: 50)).setup {
$0.backgroundColor = .green
}
view.addSubview(button)
goForwardButton = button
}
}
// The second view controller, same as the first but the button name and color is different.
final class SecondViewController: UIViewController {
#IBOutlet var goBackButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// create the view. This could be set up in a xib or storyboard instead.
view.backgroundColor = .white
let button = UIButton(frame: CGRect(x: 100, y: 100, width: 50, height: 50)).setup {
$0.backgroundColor = .red
}
view.addSubview(button)
goBackButton = button
}
}
// here's where the magic happens.
extension ViewController {
func connect() {
let alertMessages = ["1st Item", "2nd Item", "3rd Item", "4th Item"]
goForwardButton.rx.tap
.flatMap {
displayWarnings(messages: alertMessages, interval: .seconds(3))
}
.subscribe(onNext: presentScene(animated: true) {
SecondViewController().scene { $0.connect() }
})
.disposed(by: disposeBag)
/*
When the goForwardButton is tapped, the closure in the flatMap will present the alerts, then the closure in the
subscribe will present the second view controller.
*/
}
}
func displayWarnings(messages: [String], interval: RxTimeInterval) -> Observable<Void> {
Observable.from(messages)
.concatMap(presentScene(animated: true) { message in
UIAlertController(title: nil, message: message, preferredStyle: .alert)
.scene { $0.dismissAfter(interval: interval) }
})
.takeLast(1)
/*
This displays the alerts in succession, one for each message in the array and sets up the dismissal.
When the last alert has been dismissed, it will emit a next event and complete. Note that this function is reusable
for any number of alerts with any duration in any view controller.
*/
}
extension UIAlertController {
func dismissAfter(interval: RxTimeInterval) -> Observable<Void> {
let action = PublishSubject<Void>()
addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in action.onSuccess(()) }))
return Observable.amb([action, .just(()).delay(interval, scheduler: MainScheduler.instance)])
/*
This will close the alert after `interval` time or when the user taps the OK button.
*/
}
}
extension SecondViewController {
func connect() -> Observable<Void> {
return goBackButton.rx.tap.take(1)
/*
When the user taps the goBackButton, this will notify the CLE library that it's complete and the library will dismiss it.
*/
}
}
You almost could do this with standard callback closures, except implementing the concatMap with callback closures would be a huge PITA.
If you look inside the CLE library, you will see that it sets up a serial queue to present/dismiss view controllers and waits for their completion before allowing the next one to present dismiss. Also, it finds the top view controller itself so you don't ever have to worry about presenting from a VC that is already presenting something.

Swift: Ignored request to start a new background task because RunningBoard has already started the expiration timer

I have a series of tasks that is triggered by a silent push notification. Upon receiving the push notification, it wakes the iOS up in the background and performs the following tasks:
Opens up a WebViewController that contains a WKWebview
Goes to a webpage, and clicks some buttons automated by javascript injection
Once completed, dismisses the WebViewController
I have added selected BackgroundTasks handlers to manage it by following this tutorial but the console is flooded with the following warning.
[ProcessSuspension] 0x280486080 - WKProcessAssertionBackgroundTaskManager: Ignored request to start a new background task because RunningBoard has already started the expiration timer
Note that the tasks that needs to be done are still performed correctly.
class WebViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
lazy var webView: WKWebView = {
let v = WKWebView()
v.translatesAutoresizingMaskIntoConstraints = false
v.navigationDelegate = self
return v
}()
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
//Remove BG task when not needed
deinit {
NotificationCenter.default.removeObserver(self)
endBackgroundTask()
}
override func viewDidLoad() {
super.viewDidLoad()
//Register notification for background task
NotificationCenter.default.addObserver(self,
selector: #selector(reinstateBackgroundTask),
name: UIApplication.didBecomeActiveNotification,
object: nil)
registerBackgroundTask()
//Load webview with URL
if let url = url {
let request = URLRequest(url: url)
webView.load(request)
}
}
//MARK:- Handle BG Tasks
func registerBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
self?.endBackgroundTask()
}
}
func endBackgroundTask() {
Log("Background task ended.")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
#objc func reinstateBackgroundTask() {
if backgroundTask == .invalid {
registerBackgroundTask()
}
}
func endBackgroundTaskIfNotInvalid() {
if backgroundTask != .invalid {
self.endBackgroundTask()
}
}
//This is the final task that needs to be done
fileprivate func updateScheduler(visitedPlace: VisitedPlace) {
if navigator == .scheduler {
if let jobId = jobId {
let data = [
"status": "scheduled",
"completedOn": Date()
] as [String : Any]
///Do some work here...
//Dismiss controller after completing
self.dismiss(animated: true) {
self.endBackgroundTaskIfNotInvalid()
}
}
} else {
self.endBackgroundTaskIfNotInvalid()
}
}
}
What is triggering all these warnings and how do I silence it?
I'm having the same console flood. For me it turned out to be adMob that was the cause.
This happens to me when I run unit tests that wait for test expectations to be filled. I was hoping that it was just a simulator issue, since I don't see it in production, but it sounds like that's not the case.
I fixed the flood by removing the AdMob banner from the view hierarchy on app suspension:
self.bannerView?.removeFromSuperview()
AdMob would still try infrequently which would generate a single log message.

Dismiss In-App AppStore rating in UITests in Swift

Hello dear developers,
I'm currently stuck due to a problem with In-App AppStore rating (SKStoreReviewController).
Here is the situation, I've a screen "FirstScreen" with a button. When I tap on it, I'm going to the next screen "SecondScreen" and an in app alert for AppStore rating pop over.
I'm trying to find a solution for my UITests in order to dismiss this Alert.
I tried many solutions but I'm looking for one which do not depends on string (I don't want to localize the content of this Alert):
override func setUp() {
app = XCUIApplication()
app.launch()
addUIInterruptionMonitor(withDescription: "System Dialog") { (alert) -> Bool in
let allowButton = alert.buttons.element(boundBy: 1)
if allowButton.exists {
allowButton.tap()
}
}
}
I also tried to add an interaction ("app.swipeUp()") when I'm coming to "SecondScreen" in order to trigger this handler.
I've also tried another solution, as I know when this alert will be triggered:
let dismissButton = XCUIApplication(bundleIdentifier: "com.apple.springboard").buttons.element(boundBy: 1)
if dismissButton.exists {
dismissButton.tap()
}
No one worked and I'm still stuck :(
Does anybody found a solution in order to dismiss this alert ?
Thanks
Swiping up doesn't work but ironically swiping down does. Here is a very simplistic example
import UIKit
import StoreKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
SKStoreReviewController.requestReview()
}
}
}
import XCTest
class UITests: XCTestCase {
override func setUp() {
continueAfterFailure = false
}
func test() {
let app = XCUIApplication()
app.launch()
sleep(5)
app.swipeDown()
sleep(3)
}
}

Background refresh task not triggered in watchos 6 independent app

I'm developing an independent watch app on XCode 11.0 beta 5. Everything works alright except background refresh. I'm using the following code to schedule the background refresh task when I open the app:
let fireDate = Date(timeIntervalSinceNow: 60.0 * 30.0)
// optional, any SecureCoding compliant data can be passed here
let userInfo = ["reason" : "update UI"] as NSDictionary
WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate: fireDate, userInfo: userInfo) { (error) in
if (error == nil) {
print("successfully scheduled background task, use the crown to send the app to the background and wait for handle:BackgroundTasks to fire.")
}
}
The func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) is never called.
If I use the Debug->Simulate background fetch option from XCode the method gets called.
I have exactly the same problem, even if I don't run the app as a standalone. The problem only occurred with watchOS 6.
Does anyone have an idea what the solution is?
Here is my source code:
import WatchKit
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
print("applicationDidFinishLaunching")
self.reloadActiveComplications()
scheduleNextReload()
}
func applicationDidBecomeActive() {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
self.reloadActiveComplications()
scheduleNextReload()
}
func applicationWillResignActive() {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, etc.
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
print("background")
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
// Be sure to complete the background task once you’re done.
scheduleNextReload()
self.reloadActiveComplications()
backgroundTask.setTaskCompletedWithSnapshot(true)
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
// Snapshot tasks have a unique completion call, make sure to set your expiration date
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
// Be sure to complete the connectivity task once you’re done.
connectivityTask.setTaskCompletedWithSnapshot(true)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once you’re done.
urlSessionTask.setTaskCompletedWithSnapshot(true)
default:
// make sure to complete unhandled task types
task.setTaskCompletedWithSnapshot(true)
}
}
}
func reloadActiveComplications() {
let server = CLKComplicationServer.sharedInstance()
print("ExtensionDelegate: requesting reload of complications")
for complication in server.activeComplications ?? [] {
server.reloadTimeline(for: complication)
}
}
func scheduleNextReload() {
var targetDate:Date
let currentDate = Date()
let timezoneOffset = TimeZone.current.secondsFromGMT()
let epochDate = currentDate.timeIntervalSince1970
let timezoneEpochOffset = (epochDate + Double(timezoneOffset))
let timeZoneOffsetDate = Date(timeIntervalSince1970: timezoneEpochOffset)
targetDate = timeZoneOffsetDate.addingTimeInterval(120)
print("ExtensionDelegate: scheduling next update at %#", "\(timeZoneOffsetDate)")
print("ExtensionDelegate: scheduling next update at %#", "\(targetDate)")
WKExtension.shared().scheduleBackgroundRefresh(
withPreferredDate: targetDate,
userInfo: nil,
scheduledCompletion: { error in
// contrary to what the docs say, this is called when the task is scheduled, i.e. immediately
NSLog("ExtensionDelegate: background task %#",
error == nil ? "scheduled successfully" : "NOT scheduled: \(error!)")
}
)
}
}
Try scheduling it in applicationDidResignActive, rather than from a controller.
It only does it IF the application is in the background. It doesn't seem to think it needs to do it if the application isn't in the background.
You can make applicationDidResignActive fire by pressing the crown button.

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.