KingFisher + UICollectionView + XCTest with NSURLSession mocking doesn't load images - ios

I'm writing unit tests for some of the views/viewcontrollers in my apps.
My app uses UICollectionView, with the cells containing images loaded using kingfisher. I am using FBSnapshotTestCase to record images of the view and compare them against known good images (and as an aside, using buddybuild's CI to automate running the tests when our developers own pull requests, which is really cool).
I'm using NSURLSession-Mock to insert precanned data (both JSON and images) into the tests.
My problem is that it seems hard to write tests that get the final same end result the users see; I'm frequently finding that (unless the images are already cached - which they aren't as I clear out the cache in test's setup to make sure the tests are running from a clean state!) all the screenshots I take are missing the images, showing only the placeholders.

I've found ways to get this apparently working reliably, but I can't see that I'm 100% happy with my solutions.
Firstly I do this in didFinishLaunchingWithOptions to avoid the application's main UI getting loaded, which caused all sort of confusion when also trying to write tests for the app's home screen:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
BuddyBuildSDK.setup()
//Apply Itison UI Styles
ItIsOnUIAppearance.apply()
#if DEBUG
if let _ = NSClassFromString("XCTest") {
// If we're running tests, don't launch the main storyboard as
// it's confusing if that is running fetching content whilst the
// tests are also doing so.
let viewController = UIViewController()
let label = UILabel()
label.text = "Running tests..."
label.frame = viewController.view.frame
label.textAlignment = .center
label.textColor = .white
viewController.view.addSubview(label)
self.window!.rootViewController = viewController
return true
}
#endif
then in the test, once I've fully setup the UIViewController I need to do things like this:
func wait(for duration: TimeInterval) {
let waitExpectation = expectation(description: "Waiting")
let when = DispatchTime.now() + duration
DispatchQueue.main.asyncAfter(deadline: when) {
waitExpectation.fulfill()
}
waitForExpectations(timeout: duration+1)
}
_ = viewController.view // force view to load
viewController.viewWillAppear(true)
viewController.view.layoutIfNeeded() // forces view to layout; necessary to get kingfisher to fetch images
// This is necessary as otherwise the blocks that Kingfisher
// dispatches onto the main thread don't run
RunLoop.main.run(until: Date(timeIntervalSinceNow:0.1));
viewController.view.layoutIfNeeded() // forces view to layout; necessary to get kingfisher to fetch images
wait(for: 0.1)
FBSnapshotVerifyView(viewController.view)
The basic issue if I don't do this is that KingFisher only starts to load the images when the FBSnapshotVerifyView forces the view to be laid out, and (as KingFisher loads images by dispatching blocks to background threads, which then dispatch blocks back to the main thread) this is too late - the blocks sent to the main thread can't run as the main thread is blocked in FBSnapshotVerifyView(). Without the calls to 'layoutIfNeeded()' and RunLoop.main.run() the KingFisher dispatch_async GCD to the main queue doesn't get to run until the /next/ test lets the runloop run, which is far too late.
I'm not too happy with my solution (eg. it's far from clear why I need to layoutIfNeeded() twice and run the runloop twice) so would really appreciate other ideas, but I hope this at least helps other people that run into the same situation as it took a little bit of head scratching to figure out what was happening.

Related

UI not updating (in Swift) during intensive function on main thread

I wondered if anyone could provide advice on how I can ‘force’ the UI to update during a particularly intensive function (on the main thread) in Swift.
To explain: I am trying to add an ‘import’ feature to my app, which would allow a user to import items from a backup file (could be anything from 1 - 1,000,000 records, say, depending on the size of their backup) which get saved to the app’s CodeData database. This function uses a ‘for in’ loop (to cycle through each record in the backup file), and with each ‘for’ in that loop, the function sends a message to a delegate (a ViewController) to update its UIProgressBar with the progress so the user can see the live progress on the screen. I would normally try to send this intensive function to a background thread, and separately update the UI on the main thread… but this isn't an option because creating those items in the CoreData context has to be done on the main thread (according to Swift’s errors/crashes when I initially tried to do it on a background thread), and I think this therefore is causing the UI to ‘freeze’ and not update live on screen.
A simplified version of the code would be:
class CoreDataManager {
var delegate: ProgressProtocol?
// (dummy) backup file array for purpose of this example, which could contain 100,000's of items
let backUp = [BackUpItem]()
// intensive function containing 'for in' loop
func processBackUpAndSaveData() {
let totalItems: Float = Float(backUp.count)
var step: Float = 0
for backUpItem in backUp {
// calculate Progress and tell delegate to update the UIProgressView
step += 1
let calculatedProgress = step / totalItems
delegate?.updateProgressBar(progress: calculatedProgress)
// Create the item in CoreData context (which must be done on main thread)
let savedItem = (context: context)
}
// loop is complete, so save the CoreData context
try! context.save()
}
}
// Meanwhile... in the delegate (ViewController) which updates the UIProgressView
class ViewController: UIViewController, ProgressProtocol {
let progressBar = UIProgressView()
// Delegate function which updates the progress bar
func updateProgressBar(progress: Float) {
// Print statement, which shows up correctly in the console during the intensive task
print("Progress being updated to \(progress)")
// Update to the progressBar is instructed, but isn't reflected on the simulator
progressBar.setProgress(progress, animated: false)
}
}
One important thing to note: the print statement in the above code runs fine / as expected, i.e. throughout the long ‘for in’ loop (which could take a minute or two), the console continuously shows all the print statements (showing the increasing progress values), so I know that the delegate ‘updateProgressBar’ function is definitely firing correctly, but the Progress Bar on the screen itself simply isn’t updating / doesn’t change… and I’m assuming it’s because the UI is frozen and hasn’t got ‘time’ (for want of a better word) to reflect the updated progress given the intensity of the main function running.
I am relatively new to coding, so apologies in advance if I ask for clarification on any responses as much of this is new to me. In case it is relevant, I am using Storyboards (as opposed to SwiftUI).
Just really looking for any advice / tips on whether there are any (relatively easy) routes to resolve this and essentially 'force' the UI to update during this intensive task.
You say "...Just really looking for any advice / tips on whether there are any (relatively easy) routes to resolve this and essentially 'force' the UI to update during this intensive task."
No. If you do time-consuming work synchronously on the main thread, you block the main thread, and UI updates will not take effect until your code returns.
You need to figure out how to run your code on a background thread. I haven't worked with CoreData in quite a while. I know it's possible to do CoreData queries on a background thread, but I no longer remember the details. That's what you're going to need to do.
As to your comment about print statements, that makes sense. The Xcode console is separate from your app's run loop, and is able to display output even if your code doesn't return. The app UI can't do that however.

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).

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
}

How to solve process timing issues with Swift when working with iOS and OSX

I am facing with this specific timing issue sometimes. I am using timer to overcome of that but using timer trashing my code structure.
"The timing issue" is that. We are creating functions and call them one after other but some functions takes longer time finishing their job and when other function is called before the previous one has finished its job
that causing problems.
As I stated at the beginning I can solve that rare issue by using timer after this functions causing issue but looking for better solution which is not going to disturb code structure.
The last function causing the problem was the one below.
When it is called by user action such as touching a button there is
no issue.
But when it is called by an other function it casing above mentioned problem.
func getImageOfView (viewName: NSView) -> NSImage {
let width = viewName.frame.size.width
let height = viewName.frame.size.height
let pageSize = CGSizeMake(width ,height);
let image = NSImage.init(size: pageSize)
image.lockFocus()
let contextPointer = NSGraphicsContext.currentContext()?.graphicsPort
let context: CGContextRef = Unmanaged.fromOpaque(COpaquePointer(contextPointer!)).takeUnretainedValue()
viewName.layer!.renderInContext(context)
image.unlockFocus()
return image
}

background processing a difficult task on viewDidLoad in swift

I am currently making an app which initiates a very long task on the viewDidLoad method. The nature of it is stopping the view from loading at all for extended periods of time and sometimes causes the app to crash. As such, I need to be able to process this particular task in the background so that the view can load instantly and then, in the background, complete the task and update the view when it is done. Does anybody know how to do this?
Try to use GCD similar to this:
let backgroundQueue = dispatch_get_global_queue(CLong(DISPATCH_QUEUE_PRIORITY_HIGH), 0)
dispatch_async(backgroundQueue) {
// Do your stuff on global queue.
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// After finish update UI on main queue.
})
}
Or you can use NSOperationQueue. At WWDC 2105 was very nice talk about it. https://developer.apple.com/videos/wwdc/2015/?id=226

Resources