Append multiple VNCoreMLModel ARKit and CoreML - swift

I'm a noob and I don't really know how can I happened multiple CoreML model to the VNCoreMLRequest.
With the code below is just using one model but I want to append also another model (visionModel2 on the example below). Can anyone help me? Thank you!
private func performVisionRequest(pixelBuffer: CVPixelBuffer){
let visionModel = try! VNCoreMLModel(for: self.iFaceModel.model)
let visionModel2 = try! VNCoreMLModel(for: self.ageModel.model)
let request = VNCoreMLRequest(model: visionModel){ request, error in
if error != nil {
return
}
guard let observations = request.results else {
return
}
let observation = observations.first as! VNClassificationObservation
print("Name \(observation.identifier) and confidence is \(observation.confidence)")
DispatchQueue.main.async {
if observation.confidence.isLess(than: 0.04) {
self.displayPredictions(text: "Not recognized")
print("Hidden")
}else {
self.displayPredictions(text: observation.identifier)
}
}
}

To evaluate an image using multiple ML models, you’ll need to perform multiple requests. For example:
let faceModelRequest = VNCoreMLRequest(model: visionModel)
let ageModelRequest = VNCoreMLRequest(model: visionModel2)
let handler = VNImageRequestHandler( /* my image and options */ )
handler.perform([faceModelRequest, ageModelRequest])
guard let faceResults = faceModelRequest.results as? [VNClassificationObservation],
let ageResults = ageModelRequest.results as? [VNClassificationObservation]
else { /*handle errors from each request */ }
(Yes, you can run Vision requests without a completion handler and then collect the results from multiple requests. Might want to check prefersBackgroundProcessing on the requests and dispatch everything to a background queue yourself, though.)
After that, you probably want to iterate the results from both requests together. Here’s a handy way you could do that with Swift standard library sequence functions, but it assumes that both models return information about the same faces in the same order:
for (faceObservation, ageObservation) in zip (faceResults, ageResults) {
print(“face \(faceObservation.classification) confidence \(faceObservation.confidence)”)
print(“age \(ageObservation.classification) confidence \(ageObservation.confidence)”)
// whatever else you want to do with results...
}
Disclaimer: Code written in StackExchange iOS app, not tested. But it’s at least a sketch of what you’re probably looking for — tweak as needed.

Related

URLSession.shared.dataTask Code Block Not Running

I'm trying to make a fairly simple API call in Swift but, for some reason, my dataTask code is not running. I've made sure that the .resume() is there. This code has worked in the past but, something has changed recently and I don't know what it is. The only thing I can think of is the url. I've changed the ingredients but, when putting the url into a browser, it returns JSON data normally. When running this function, I get two "Outside URLSession.shared.dataTask....." messages in a row with nothing in between, indicating that the URLSession block of code isn't running. I'm a little new to APIs so, any help would be greatly appreciated. Please let me know if there's any more information I can provide. Also, I'm on an older MacBook and am using Swift5 if that makes a difference. Thanks!
let url: URL! = URL(string: "https://api.spoonacular.com/recipes/findByIngredients?ingredients=" + ingredientString + "&apiKey=aaabbbccc111222333")
print("URL: " + url.absoluteString)
let request = URLRequest(url: url)
// Make the API call
print("Outide URLSession.shared.dataTask.....")
let session = URLSession.shared.dataTask(with: request) { data, response, error in
print("Inside URLSession.shared.dataTask.....")
DispatchQueue.main.async {
print("Inside DispatchQueue.main.async....")
if data == nil {
print("No data recieved.")
}
print("data != nil.... Moving on to JSONDecoder....")
self.model = try! JSONDecoder().decode([RecipeSearchElement].self, from: data!)
}
}
session.resume()
print("Outside URLSession.shared.dataTask.....")
Unrelated to your immediate question at hand (which I answered elsewhere), I would advise a few changes to the routine:
One should not build a URL through string interpolation. Use URLComponents. If, for example, the query parameter included a space or other character not permitted in a URL, URLComponents will percent-encode it for you. If do not percent-encode it properly, the building of the URL will fail.
I would avoid try!, which will crash the app if the server response was not what you expected. One should use try within a do-catch block, so it handles errors gracefully and will tell you what is wrong if it failed.
I would recommend renaming the URLSessionDataTask to be task, or something like that, to avoid conflating “sessions” with the “tasks” running on that session.
I would not advise updating the model from the background queue of the URLSession. Fetch and parse the response in the background queue and update the model on the main queue.
Thus:
var components = URLComponents(string: "https://api.spoonacular.com/recipes/findByIngredients")
components?.queryItems = [
URLQueryItem(name: "ingredients", value: ingredientString),
URLQueryItem(name: "apiKey", value: "aaabbbccc111222333")
]
guard let url = components?.url else {
print("Unable to build URL")
return
}
// Make the API call
let task = URLSession.shared.dataTask(with: url) { data, _, error in
DispatchQueue.main.async {
guard error == nil, let data = data else {
print("No data received:", error ?? URLError(.badServerResponse))
return
}
do {
let model = try JSONDecoder().decode([RecipeSearchElement].self, from: data)
DispatchQueue.main.async {
self.model = model
}
} catch let parseError {
print("Parsing error:", parseError, String(describing: String(data: data, encoding: .utf8)))
}
}
}
task.resume()
In a more advanced observation, I would never have a network call update the model directly. I would leave that to the caller. For example, you could use a completion handler pattern:
#discardableResult
func fetchIngredients(
_ ingredientString: String,
completion: #escaping (Result<[RecipeSearchElement], Error>) -> Void
) -> URLSessionTask? {
var components = URLComponents(string: "https://api.spoonacular.com/recipes/findByIngredients")
components?.queryItems = [
URLQueryItem(name: "ingredients", value: ingredientString),
URLQueryItem(name: "apiKey", value: "aaabbbccc111222333")
]
guard let url = components?.url else {
completion(.failure(URLError(.badURL)))
return nil
}
// Make the API call
let task = URLSession.shared.dataTask(with: url) { data, _, error in
print("Inside URLSession.shared.dataTask.....")
DispatchQueue.main.async {
guard error == nil, let data = data else {
DispatchQueue.main.async {
completion(.failure(error ?? URLError(.badServerResponse)))
}
return
}
do {
let model = try JSONDecoder().decode([RecipeSearchElement].self, from: data)
DispatchQueue.main.async {
completion(.success(model))
}
} catch let parseError {
DispatchQueue.main.async {
completion(.failure(parseError))
}
}
}
}
task.resume()
return task
}
And then the caller could do:
fetchIngredients(ingredientString) { [weak self] result in
switch result {
case .failure(let error): print(error)
case .success(let elements): self?.model = elements
}
}
This has two benefits:
The caller now knows when the model is updated, so you can update your UI at the appropriate point in time (if you want).
It maintains a better separation of responsibilities, architecturally avoiding the tight coupling of the network layer with that of the view or view model (or presenter or controller) layers.
Note, I am also returning the URLSessionTask object in case the caller would like to cancel it at a later time, but I made it an #discardableResult so that you do not have to worry about that if you are not tackling cancelation at this point.
If you (a) are reaching the “outside” message, but not seeing the “inside” message; and (b) are absolutely positive that you are reaching the resume statement, it is one of a few possibilities:
The app may be terminating before the asynchronous request has time to finish. This can happen, for example, if this is a command-line app and you are allowing the app to quit before the asynchronous request has a chance to finish. If you want a command-line app to wait for a network request to finish, you might run a RunLoop that does not exit until the network request is done.
It can also happen if you use a playground and neglect to set needsIndefiniteExecution:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
For the sake of completeness, there are a few other, less common, possibilities:
You have some other network request whose completion handler is blocked/deadlocked, thereby preventing anything else from running on the URLSession dedicated, serial, queue.
You have thread explosion somewhere else in your code, exhausting the limited pool of worker threads, preventing other tasks/operations from being able to get an available worker thread.

Alamofire Bad Network Connection | Swift

I would like to implement a way to check if the network is weak or disconnected during network calls I perform using Alamofire 4.9 - the following is what I am currently attempting to do, but I have noticed that if the network it off it never jumps to this line:
URLError.Code.notConnectedToInternet
why does this occur, is there a better way of attempting this?
//Fetch new data
guard let url = URL(string: "test.com")
else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = "value1=test1&value2=test2".data(using: .utf8)
URLSession.shared.dataTask(with: request) { [self] data, _, error in
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let res = try decoder.decode([Structure].self, from: data)
}
}
catch {
if let err = error as? URLError, err.code == URLError.Code.notConnectedToInternet {
// No internet
} else {
print(error)
}
}
}.resume()
There are a variety of errors that can be produced by poor or entirely broken connectivity. .notConnectedToInternet is simply one of them. There is no single, exhaustive list provided by Apple of which errors may be returned when there are connectivity issues, so you'll want to experiment and see what you can produce. Additionally, there are a variety of errors which you can probably throw into a "network connectivity" bucket, such as .dnsLookupFailed or .cannotConnectToHost. Ultimately it may not be worth it to differentiate different types of URLErrors.
By the way, you should use Alamofire 5 if you can, as Alamofire 4 is no longer supported.

Swift, URLSession, viewDidLoad, tableViewController

I've never really gotten the nuances of async operations so time and again, I get stymied. And I just can't figure it out.
I'm trying to do some very simple web scraping.
My local volleyball association has a page (verbose HTML, not responsive, not mobile-friendly, yaddah, yaddah, yaddah) which shows the refs assigned to each game of the season. I'm trying to write a silly little app which will scrape that page (no API, no direct access to db, etc.) and display the data in a grouped table. The first group will show today's matches (time, home team, away team). The second group will show tomorrow's matches. Third group shows the entire season's matches.
Using code I found elsewhere, my viewDidLoad loads the page, scrapes the data and parses it into an array. Once I've parsed the data, I have three arrays: today, tomorrow, and matches, all are [Match].
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: urlString)!
let request = NSMutableURLRequest(url: url)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if let error = error {
print (error)
} else {
if let unwrappedData = data {
// scrape, scrape, parse, parse
matchRow = ...
self.matches.append(matchRow)
if matchRow.date == todaysDate {
self.today.append(matchRow)
} else if matchRow.date == tomorrowsDate {
self.tomorrow.append(matchRow)
}
}
}
}
task.resume()
}
As I'm sure is no surprise to anyone who understands async operations, my table is empty. I've checked and I see the the data is there and properly parsed, etc. But I can't for the life of me figure out how get the data in my table. The way I have it now, the data is not ready when numberOfSections or numberOfRowsInSection is called.
I've found the Ray Wenderlich tutorial on URLSession and I also have a Udemy course (Rob Percival) that builds an app to get the weather using web scraping, but in both those instances, the app starts and waits for user input before going out to the web to get the data. I want my app to get the data immediately upon launch, without user interaction. But I just can't figure out what changes I need to make so that those examples work with my program.
Help, please.
You can simply reload the tableviews once the data arrays are getting populated from the URLSession completion block. Have you tried that. Sample snippet may be like the one follows.
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if let error = error {
print (error)
} else {
if let unwrappedData = data {
// scrape, scrape, parse, parse
matchRow = ...
self.matches.append(matchRow)
if matchRow.date == todaysDate {
self.today.append(matchRow)
} else if matchRow.date == tomorrowsDate {
self.tomorrow.append(matchRow)
}
}
DispatchQueue.main.async { [weak self] in
self?.todayTableView.reloadData()
self?.tomorrowTableView.reloadData()
}
}
}

How to Get Test Coverage for Guard Statement Fall-through

I started writing iOS unit tests today with the BDD approach. I have a question regarding guard statements and getting to 100% code coverage.
I have the following code, which handles the conversion of Data into Customer objects.
internal final class func customer(from data: Data) -> Customer? {
do {
guard let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: []) as? Dictionary<String, Any> else {
return nil
}
var customerFirstName: String? = nil
var customerLastName: String
if let firstName = jsonDictionary["first_name"] as? String {
customerFirstName = firstName
}
guard let lastName = jsonDictionary["last_name"] as? String else {
return nil
}
customerLastName = lastName
return Customer(firstName: customerFirstName, lastName: customerLastName)
} catch {
return nil
}
}
When our backend was created, some customers were given just a last name, which contained their first and last names. That is why the customer's first name is optional; their full name may be the value for last_name.
In my code, the customer's first name is optional while their last name is required. If their last name is not returned in the received JSON from a network request, then I do not create the customer. Also, if the Data cannot be serialized into a Dictionary, then the customer is not created.
I have two JSON files, both of which contain customer information that I am using to test both scenarios.
One contains no first name in the JSON:
{
"first_name": null,
"last_name": "Test Name",
}
The other contains a first name in the JSON:
{
"first_name": "Test",
"last_name": "Name",
}
In my unit test, using Quick and Nimble, I handle the creation of a Customer when the first name is not available and when it is:
override func spec() {
super.spec()
let bundle = Bundle(for: type(of: self))
describe("customer") {
context("whenAllDataAvailable") {
it("createsSuccessfully") {
let path = bundle.path(forResource: "CustomerValidFullName", ofType: "json", inDirectory: "ResponseStubs")!
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
let customer = DataTransformer.customer(from: data)
expect(customer).toNot(beNil())
}
}
context("whenMissingLastName") {
it("createsUnsuccessfully") {
let path = bundle.path(forResource: "CustomerMissingLastName", ofType: "json", inDirectory: "ResponseStubs")!
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
let customer = DataTransformer.customer(from: data)
expect(customer).to(beNil())
}
}
}
}
This ensures that I am creating a Customer when the first name is missing or present in the returned JSON.
How can I get to 100% code coverage of this method, using BDD, when my code does not hit the else clauses of the guard statements since the data is able to be turned into valid JSON objects? Should I just add another .json file with data that cannot be transformed into a JSON object to ensure that a Customer is not created as well as a .json file that contains a missing last_name to ensure that a Customer is not created?
Am I just over-thinking the "100% code coverage" concept? Do I even need to have the else clauses of the guard statements tested? Do I even have the appropriate approach using the BDD method?
Just write whatever JSON you want — malformed in every way you can think of. Examples:
You can hit your exception-handling with something that isn't correct JSON.
You can hit your very first guard with something that is a JSON array, not a dictionary.
As the saying goes, you only need to cover code that you want to be correct. 😉
TDD and BDD are related. In TDD, you'd write a failing test first. Then, you'd write code that passes that test as quickly as you can. Finally, you'd clean up your code to make it better. It looks like you're adding tests after-the-fact.
By the way, your tests would be much clearer if you didn't use external files, but put the JSON straight into your tests. Here's a screencast showing how I TDD the beginnings of JSON conversion. The screencast is in Objective-C but the principles are the same: https://qualitycoding.org/tdd-json-parsing/
100% code coverage with if let.
At times, its not feasible to forcefully prepare a malformed object to enforce the execution hitting return or return nil used in guard statements.
This is the case when you have 3rd party SDKs integrated and respective 3rd party objects get created runtime within the method.
For example :
func aMethod() {
guard let response = 3rdPartyResponse<3rdPartyInput>.createControl(with: .create(with: .someCase)) as? 3rdPartyResponse<3rdPartyInput> else { return }
}
In this case, its very hard, at times not possible to hit the return.
But If code coverage is the main criteria, you can go with using an if let for such cases If lets does give 100% code coverage

Unexpectedly unwrapping an optional to find a nil after an API call to Spotify

So I know this may be a bit specific but I've been staring at my code and am unable to resolve this issue. Basically, I'm making a network call to spotify to obtain a certain playlist and pass a number that will ultimately determine the number of songs I get back. The code is basically as follows:
// A network call is made just above to return somePlaylist
let playlist = somePlaylist as! SPTPartialPlaylist
var songs: [SPTPartialTrack] = []
// load in playlist to receive back songs
SPTPlaylistSnapshot.playlistWithURI(playlist.uri, session: someSession) { (error: NSError!, data: AnyObject!) in
// cast the data into a correct format
let playlistViewer = data as! SPTPlaylistSnapshot
let playlist = playlistViewer.firstTrackPage
// get the songs
for _ in 1...numberOfSongs {
let random = Int(arc4random_uniform(UInt32(playlist.items.count)))
songs.append(playlist.items[random] as! SPTPartialTrack)
}
}
The problem comes at the portion of code that initializes random. In maybe 1 in 20 calls to this function I, for whatever, reason unwrap a nil value for playlist.items.count and can't seem to figure out why. Maybe it's something I don't understand about API calls or something else I'm failing to see but I can't seem to make sense of it.
Anyone have any recommendations on addressing this issue or how to go about debugging this?
Ok, after sleeping on it and working on it some more I seem to have resolved the issue. Here's the error handling I implemented into my code.
if let actualPlaylist = playlist, actualItems = actualPlaylist.items {
if actualItems.count == 0 {
SongScraper.playlistHasSongs = false
print("Empty playlist, loading another playlist")
return
}
for _ in 1...numberOfSongs {
let random = Int(arc4random_uniform(UInt32(actualItems.count)))
songs.append(actualPlaylist.items[random] as! SPTPartialTrack)
}
completionHandler(songs: songs)
}
else {
print("Returned a nil playlist, loading another playlist")
SongScraper.playlistHasSongs = false
return
}