XCTests and PHPhotoLibrary - ios

I have functionality in the app that saved picture in photo gallery. An I wonder how to test this code:
func saveInPhotoGallery() {
guard self.cameraOutput != nil else { return }
if self.cameraOutput is UIImage {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: (self.cameraOutput as? UIImage)!)
}, completionHandler: { (saved, error) in
guard error == nil else {
self.unsucessfullSavingOperation(error)
return
}
})
}
}
Let's assume now that I want to test in my case scenario that self.cameraOutput is and UIImage and sth went wrong and there is an error in completionHandler so I ended up in self.unsucessfullSavingOperation(error) method. This has separate tests of course, but what I want to cover is:
Make sure whenever something will went wrong with inserting image in Camera Roll I will end up calling this method
And when I try to call saveInPhotoGallery() in test target it produce Alert that this require access to your photo library (doh!). But there is a way to skip this alert in Unit Tests or check whanever it popup and press allow? (like I said, for this test, let's assume that I have this permissions)
Or there is a way to mock this behaviour?

Yes, I'd mock PHPhotoLibrary. The main thing you'll need to replace is your use of PHPhotoLibrary.shared() which creates a dependency to a concrete instance. Instead, we can depend on an abstraction, that is, a protocol.
Then production code can supply PHPhotoLibrary.shared() as the instance to use. Test code can supply a mock object.
Let me know if you need more elaboration on breaking the dependency, or on making a mock object, or both.

Related

firestore collection path giving bugs with constants value and String value

So my goal is to get rid of these bugs completely. I am in a dilemma where each decision leads to a bug.
The first thing I can do that eventually becomes an issue is use a String-interpolated collection path in all my query functions like so:
func getEventName() {
listener = db.collection("school_users/\(user?.uid)/events").order(by: "time_created", descending: true).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the data: \(error)")
} else {
self.events = querySnapshot!.documents.map { document in
return EventName(eventName: (document.get("event_name") as! String))
}
self.tableView.reloadData()
}
}
}
The thing with this is, when I run the app on the simulator, I am restricted from pressing buttons and then sometimes I can press them and then sometimes they get restricted again. This bug is so confusing because it makes no sense where it springs from.
The other issue is I can use a Constants value in all the query functions in my collections path.
static let schoolCollectionName = "school_users/\(user?.uid)/events"
This is nested in a Firebase struct within the Constants struct. In order to keep Xcode from giving errors I create a let users = Auth.auth().currentUser variable outside the Constants struct. The issue with this value is that when I put that in all of my query functions collection paths, all the buttons are accessible and selectable all the time, but when a user logs out and I log in as a new user, the previous user's data shows up in the new user's tableview.
It would obviously make more sense to use the Constants value because you prevent typos in the future, but I can't figure out how to get rid of the bug where the old user's data shows up in the new user's tableview. Thanks in advance.
The user id should definitely not be a constant. What it sounds like is that right now, you have no reliable way to change users -- your setup probably depends on which user is logged in at app startup, since that's where your variable gets set.
I would do something more like this:
func getEventName() {
guard let user = Auth.auth().currentUser else {
//handle the fact that you don't have a user here -- don't go on to the next query
return
}
listener = db.collection("school_users/\(user.uid)/events").order(by: "time_created", descending: true).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
Note that now, user.uid in the interpolated path doesn't have the ? for optionally unwrapping it (which Xcode is giving you a warning for right now). It will also guarantee that the correct query is always made with the currently-logged-in user.
Regarding being able to press the buttons, that sounds like an unrelated issue. You could run your app in Instruments and check the Time Profiler to see if you have long-running tasks that are gumming up the main/UI thread.

Best practice for presenting App internal error to user?

I use guard statement and fatalError() a lot in my app to make sure data are in consistent state. They help to catch bugs in development phase. Now I'm in the late phase of the project and start to consider how to deal with those fatalError() calls in release build.
I don't want to remove them because they help to expose unknown bugs. I don't want to just leave them as is in the product release either because they would just abort the App, which doesn't provide user any helpful information about what went wrong. What I'd like to achieve is to show an error message on the screen and then abort when user press "OK". I think there may be two approaches to do it:
1) Don't call fatalError(). Throw error instead. Let the top level code handles the error (e.g., showing an alert). The issue with the approach is that it requires to change many functions to become throwable, which I think is inconvenient.
2) The second approach is that from what I read on the net, it's possible for code to create alert without access to the current view controller on screen. The trick is to create a new window. I haven't investigated the details yet.
My concern is that I think both approaches have same inherent limitation and are not applicable in all situations. For example, suppose there is something goes wrong in a UITableViewControler's data source delegate method, does it work to present an alert from within the delegate method? I doubt it.
So I wonder what's the common practice to present the fatal error message to user? Thanks for any suggestions.
similar to create a window, there is a way to get 'currentViewController', you can use it to show an alert anywhere.
{
let view = UIViewController.current.view
Alert.show(on: view, message: errorMsg)
//or just: Alert.show(error), handle it in Alert class
}
extension UIViewController {
class func current(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return current(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
return current(base: tab.selectedViewController)
}
if let presented = base?.presentedViewController {
return current(base: presented)
}
return base
}
}
for UITableView/UIScrollView/UICollectionView, you can use runtime swizzle method to add a placeholder image when there is no data or an error occored for all views. such as EmptyDataSet
recored the errors and save the log into a local file, upload it to your server if necessary, analyse them and help users to solve there problem.

Test Driven Upload method?

I'm in a situation where I'm trying to use Test Driven Development.
I've got no experienced in Swift, Xcode, Apple, IOS, TDD or even the macbook I'm using for development. Basically I'm a .Net Developer in a very unfamiliar situation.
My current problem arrises from my ignorance on how to make a unit test that test a void method.
I'm trying to make a method that sends a Image to a server.
But my issue here is that I do not know how to test a method that doesn't return a value.
I imagine that my method is going to be something similar to this:
public func Upload(_ image: UIImage)
and I imagine that I'll need to implement some version of URLSession that eventually will have to call a resume() method. but how do I test if this method is doing what it's supposed to be doing without invoking the network ? and after that how do I make an integration test where I can see that the expected result is in fact a file uploaded to the server ?
Currently the server will be on the computer I'm developing on but the actual software will run from a testIphone that I've been issued.
I've been searching online for days now and the best I've come across have been this link http://swiftdeveloperblog.com/image-upload-with-progress-bar-example-in-swift/
But it only approaches bits and pieces I imagine will be part of the solution not the testing of said solution.
I think it's important to add I'm very much against creating to much complexity for testing purposes. testing should be simple and straight forward.
The approach to take is to use a a test double to check that the correct networking calls are made by your upload method. You'll be making an asynchronous call to a networking library, which may be URLSession or may be another library such as AlamoFire. It shouldn't matter to your upload method which library is in use.
To achieve this, you want to avoid directly using URLSession, and use a wrapper which conforms to an interface that you can then mock in your tests. This means that your code will use a different implementation of the networking class at runtime than at test time, and you'll "inject" the correct one as required.
For example, you could have this interface to your networking library:
protocol NetworkRequesting {
func post(data: Data, url: URL)
}
With the following real implementation to be used at runtime:
struct NetworkRequester: NetworkRequesting {
func post(data: Data, url: URL) {
let session = URLSession()
let task = session.uploadTask(with: URLRequest(url: url), from: data)
task.resume()
}
}
However, at test time, you use the following mock instead:
class MockNetworkRequester: NetworkRequesting {
var didCallPost = false
var spyPostData: Data? = nil
var spyPostUrl: URL? = nil
func post(data: Data, url: URL) {
didCallPost = true
spyPostData = data
spyPostUrl = url
}
}
And then, given the following class under test:
class ImageUploader {
let networkRequester: NetworkRequesting
init(networkRequester: NetworkRequesting) {
self.networkRequester = networkRequester
}
func upload(image: UIImage, url: URL) {
}
}
You can test the implementation of upload like so:
class UploadImageTests: XCTestCase {
func test_uploadCallsPost() {
let mockNetworkRequester = MockNetworkRequester()
let uploader = ImageUploader(networkRequester: mockNetworkRequester)
uploader.upload(image: UIImage(), url: URL(string:"http://example.com")!)
XCTAssert(mockNetworkRequester.didCallPost)
}
}
Currently, that test will fail as upload does nothing, but if you put the following into the class under test, the test will pass:
func upload(image: UIImage, url: URL) {
guard let otherUrl = URL(string:"https://example.org") else { return }
networkRequester.post(data: Data(), url: otherUrl)
}
And that's your first TDD cycle. Clearly it's not yet behaving as you'd like, so you need to write another test to make sure that the url used is the one you expect, or the data passed is what you expect.
There are a number of ways to get your code to use the real network requester at runtime, you could have the init method use default parameter values to get it to use NetworkRequester, or use a static factory method to create it, and there are other options like Inversion of Control, which is well beyond the scope of this answer.
The important thing to remember is that you're testing that you make the correct calls to the networking framework, you're not testing the networking framework. I like to keep my protocol interfaces pretty declarative, passing the things required to make a request in any framework, but you might find you prefer to go closer to the metal and essentially mirror the implementation of URLSession - it's up to you, and more of an art than a science, in my opinion.

Custom, content based validator for Alamofire (in Swift)

I know you can add a status code and content type validators, but I'd really love to be able to write my own validator based on the result content - basically I want to make sure the json I'm getting back contains some fields, and that their value is valid.
The way the app I'm working on is currently designed is there's a Server class that handles all the api calls, and the response object is returned to whoever called it, so they can do their logic / update ui, etc.
Now I have a status code validator on all the requests, so I don't need to have it on all external, but I have several apis, that require that custom validation logic, which means I have to add it in all the places that call it, AND that I can't use this amazing syntax:
switch resp.result {
case .Success(let value):
print("yay")
case .Failure:
print("nay")
}
I'd love any answer/pointer that can help me find a solution,
Thank you all so much in advance! :)
I wound up having this exact same question and found out what you want to do is write your own response serializer and stop using .validate().
The serializer I'm using is very, very close to the out-of-the-box JSONResponseSerializer, except I make a check for an error.
The only change I make to the stock serializer is within the do-catch statement:
do {
let JSON = try NSJSONSerialization.JSONObjectWithData(validData, options: options)
if let responseDict = JSON as? NSDictionary, apiError = NSError.APIErrorFromResponse(responseDict) {
return .Failure(apiError)
}
return .Success(JSON)
} catch {
return .Failure(error as NSError)
}
APIErrorFromResponse is simply an extension method on NSError that checks the JSON for an error dictionary and populates a custom NSError out of that.
Hopefully this points you in the right direction if you haven't already found a solution!

Disable confirmation on delete request in PHPhotoLibrary

What I am trying to do is to save videos to PHPhotoLibrary, and then remove them when upload to clients remote server in the application completes (basically, photo library serves as temporary storage to add additional layer of security in case anything at all fails (I already save my vides it in the applications directory).
Problem:
The problem is for that to work, everything has to work without input from the user. You can write video to photos library like this:
func storeVideoToLibraryForUpload(upload : SMUpload) {
if PHPhotoLibrary.authorizationStatus() != PHAuthorizationStatus.Authorized {
// Don't write to library since this is disallowed by user
return
}
PHPhotoLibrary.sharedPhotoLibrary().performChanges({ () -> Void in
// Write asset
let assetRequest = PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(NSURL(fileURLWithPath: upload.nonsecureFilePath!)!)
let assetPlaceholder = assetRequest.placeholderForCreatedAsset
let localIdentifier = assetPlaceholder.localIdentifier
// Store local identifier for later use
upload.localAssetIdentifier = localIdentifier
}, completionHandler: { (success, error) -> Void in
....
})
}
And that works flawlessly, I get local identifier, I store it for later use.. Unicorns and rainbows.
Now when I want to remove that video immediately after upload finishes, I call following:
func removeVideoFromLibraryForUpload(upload : SMUpload) {
// Only proceed if there is asset identifier (video previously stored)
if let assetIdentifier = upload.localAssetIdentifier {
// Find asset that we previously stored
let assets = PHAsset.fetchAssetsWithLocalIdentifiers([assetIdentifier], options: PHFetchOptions())
// Fetch asset, if found, delete it
if let fetchedAssets = assets.firstObject as? PHAsset {
PHPhotoLibrary.sharedPhotoLibrary().performChanges({ () -> Void in
// Delete asset
PHAssetChangeRequest.deleteAssets([fetchedAssets])
}, completionHandler: { (success, error) -> Void in
...
})
}
}
}
Which successfully deletes the video, BUT user have to confirm deletion first. That is a problem as that backing up won't work.
I obviously know why there is confirmation (so you don't clear entire user library for example, but the thing is, My app made the video - and so I thought there will be way around it, since as an "owner" I should not be doing that, or at least have option to disable confirmation.
Thanks in advance!
TLDR: How can I disable confirmation on delete request, if my application created that content? (I don't want to delete anything else).
Note: Somebody can probably say this is rather strange thing to do but the application is distributed internally and there is good reason to do it like this (the video content is too valuable to be lost, even if user deletes the application for some reason, or there is anything at all that goes wrong, we need to be able to preserve the videos), so please don't question that and just focus your attention on the question :)
I cannot see a way to avoid the delete confirmation. It is an implementation detail of the Photos framework, similar to the way you cannot prevent the device from asking the user's permission to use the microphone when your app tries to use it, and is a matter of security & trust. Once you have saved an asset to the device photo library your app is no longer the owner of that asset, so as you noted in your question the device must of course ensure the app has the user's permission before it goes about deleting such data.
You can never entirely safeguard your users' data against their own unpredictable behaviour - if they decide to remove your app, or delete a particular asset from within Photos, it is up to them. I think your best option is to either put up with the built-in delete confirmation, or to provide a guide to your users that makes it clear that they should be careful to protect this important data by backing up their device, and not deleting the app!
If you did decide to stick to this approach, perhaps the best thing you could do is to prepare the user for the fact that their device may ask them for confirmation to delete a file that is being uploaded to your own servers. For example, put up your own modal alert just before trying to delete the asset. I wouldn't normally suggest that kind of approach for a public shipping app, but since you're only distributing internally it may be acceptable for your team.

Resources