AVMetadataMachineReadableCodeObject stringValue returns inconsistent results - ios

I'm experiencing a weird issue when scanning barcodes on iOS. The data in the barcodes are railway tickets encoded using the UIC-918-3 standard.
To decode the tickets I need to get the contents of stringValue in AVMetadataMachineReadableCodeObject for further processing.
It works perfectly on iOS 16.x (tested on both iPhone 13 and 14), but when scanning the same barcode using an iPhone 6s running iOS 15.7.1 I get a truncated string in stringValue.
Any thoughts as to why this happens, and how i can fix this?
extension TicketScannerViewController: AVCaptureMetadataOutputObjectsDelegate {
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first, !shouldHaltTicketDecoding {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
let payload = readableObject.stringValue
// ...
}
}
}

Related

Scanning QR code crashes often - AVCaptureSession [duplicate]

This question already has an answer here:
Swift: Unable to detect linear type Barcodes
(1 answer)
Closed 4 years ago.
Am trying to scan QR code with the below code and it works fine. But sometimes it crashes with the following error.
Could not cast value of type 'AVMetadataFaceObject' (0x1b245bd28) to 'AVMetadataMachineReadableCodeObject' (0x1b245be68). Help much appreciated.
P.S: Am showing camera inside tabbar controller
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
// Check if the metadataObjects array is not nil and it contains at least one object.
if metadataObjects.count == 0 {
qrCodeFrameView?.frame = CGRect.zero
messageLabel.text = "No QR code is detected"
return
}
// Get the metadata object.
let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject
if supportedCodeTypes.contains(metadataObj.type) {
// If the found metadata is equal to the QR code metadata (or barcode) then update the status label's text and set the bounds
let barCodeObject = videoPreviewLayer?.transformedMetadataObject(for: metadataObj)
qrCodeFrameView?.frame = barCodeObject!.bounds
if metadataObj.stringValue != nil {
launchApp(decodedURL: metadataObj.stringValue!)
messageLabel.text = metadataObj.stringValue
captureSession?.stopRunning()
captureSession = nil
}
}
}
The use cases of a force cast are very limited, it is almost never a good idea. You should use:
guard let metadataObj = metadataObjects[0] as? AVMetadataMachineReadableCodeObject else{
// Display some sort of error message or handle it
return
}
Make sure you are detecting QR codes and not faces:
captureMetadataOutput.metadataObjectTypes = [AVMetadataObject.ObjectType.qr]

I'm uploading data from my Swift app to Amazon S3 and it drains battery like nothing else. How can this be avoided?

In my 'Swift' app I have a feature of uploading photos to my Amazon S3 bucket. When the user is connected to WiFi or LTE, there's no problem, but when the connection is a little slower (e.g. 3G), then the upload takes a lot of time (up to one minute) and iphone can lose 15-20% of battery! I resize photos down to around 200-300kb, so that should not be a problem. The code that I use for that is:
func awsS3PhotoUploader(_ ext: String, pathToFile: String, contentType: String, automaticUpload: Bool){
let credentialsProvider = AWSCognitoCredentialsProvider(regionType:CognitoRegionType,
identityPoolId:CognitoIdentityPoolId)
let configuration = AWSServiceConfiguration(region:CognitoRegionType, credentialsProvider:credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = configuration
let uploadRequest = AWSS3TransferManagerUploadRequest()
uploadRequest?.body = URL(string: "file://"+pathToFile)
uploadRequest?.key = ProcessInfo.processInfo.globallyUniqueString + "." + ext
uploadRequest?.bucket = S3BucketName
uploadRequest?.contentType = contentType + ext
uploadRequest?.uploadProgress = { (bytesSent, totalBytesSent, totalBytesExpectedToSend) -> Void in
DispatchQueue.main.async(execute: { () -> Void in
if totalBytesExpectedToSend > 1 {
print(totalBytesSent)
print(totalBytesExpectedToSend)
}
})
}
let transferManager = AWSS3TransferManager.default()
transferManager?.upload(uploadRequest).continue({ (task) -> AnyObject! in
if (task.isCompleted) {
print("task completed")
}
if let error = task.error {
print("Upload failed ❌ (\(error))")
}
if let exception = task.exception {
print("Upload failed ❌ (\(exception))")
}
if task.result != nil {
let s3URL: String = "https://myAlias.cloudfront.net/\((uploadRequest?.key!)!)"
print("Uploaded to:\n\(s3URL)")
}
else {
print("Unexpected empty result.")
}
return nil
}
)
}
Is there anything that comes up to your mind of what am I doing wrong here and how could this huge battery consumption be avoided?
The following answer was inspired by https://stackoverflow.com/a/20690088/3549695
I believed what you need to do is the ability to detect the Type of the Radio Network. Be it WiFi, LTE, 3G, 2G, or No Network.
Then the app will need to make the decision, based on the result.
I created a test Xcode project to test this concept on my iPhone 6.
It seems to work, but I could only test 'Air Plane Mode', WiFi and LTE. I can't get myself into 2G or 3G network.
In case of either WiFi or LTE, I will get the value:
'CTRadioAccessTechnologyLTE'
While in 'Air Plane Mode', the Optional value will be nil. So it's up to me what text I replace it with. And I choose to output 'Not able to detect'
Here is what my ViewController.swift looks like:
import UIKit
import CoreTelephony
class ViewController: UIViewController {
#IBOutlet weak var currentRAN: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func detect(_ sender: UIButton) {
if case let telephonyInfo = CTTelephonyNetworkInfo(),
let currentRadioAccessTech = telephonyInfo.currentRadioAccessTechnology {
currentRAN.text = currentRadioAccessTech
print("Current Radio Access Technology: \(currentRadioAccessTech)")
} else {
currentRAN.text = "Not able to detect"
print("Not able to detect")
}
}
}
Where the possible values for .currentRadioAccessTechnology are:
/*
* Radio Access Technology values
*/
#available(iOS 7.0, *)
public let CTRadioAccessTechnologyGPRS: String
#available(iOS 7.0, *)
public let CTRadioAccessTechnologyEdge: String
#available(iOS 7.0, *)
public let CTRadioAccessTechnologyWCDMA: String
#available(iOS 7.0, *)
public let CTRadioAccessTechnologyHSDPA: String
#available(iOS 7.0, *)
public let CTRadioAccessTechnologyHSUPA: String
#available(iOS 7.0, *)
public let CTRadioAccessTechnologyCDMA1x: String
#available(iOS 7.0, *)
public let CTRadioAccessTechnologyCDMAEVDORev0: String
#available(iOS 7.0, *)
public let CTRadioAccessTechnologyCDMAEVDORevA: String
#available(iOS 7.0, *)
public let CTRadioAccessTechnologyCDMAEVDORevB: String
#available(iOS 7.0, *)
public let CTRadioAccessTechnologyeHRPD: String
#available(iOS 7.0, *)
public let CTRadioAccessTechnologyLTE: String

Unresolved Identifier "itemID"

How can I Globally define (barcode scanned) so that it can be accessible by all my functions. In order words how can I define “metadataObj” globally?
class ScanController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
var captureSession: AVCaptureSession?
var videoPreviewLayer: AVCaptureVideoPreviewLayer?
var qrCodeFrameView: UIView?
override func viewDidLoad() {
super.viewDidLoad()
//Get an instance of the AVCaptureDevice class a device object and provide the video as the media type parameter
let captureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
do {
// Get an instance of the AVCaptureDeviceInput class using the previous device object.
let input = try AVCaptureDeviceInput(device: captureDevice)
// Initialize the captureSession object.
captureSession = AVCaptureSession()
// Set the input device on the capture session.
captureSession?.addInput(input)
let captureMetadataOutput = AVCaptureMetadataOutput()
captureSession?.addOutput(captureMetadataOutput)
// Set delegate and use the default dispatch queue to execute the call back
captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
captureMetadataOutput.metadataObjectTypes = supportedCodeTypes
// Initialize the video preview layer and add it as a sublayer to the viewPreview view's layer.
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
videoPreviewLayer?.frame = view.layer.bounds
view.layer.addSublayer(videoPreviewLayer!)
// Start video capture.
captureSession?.startRunning()
//initialize QR Code Frame to highlight the QR Code
qrCodeFrameView = UIView()
if let qrCodeFrameView = qrCodeFrameView {
qrCodeFrameView.layer.borderColor = UIColor.green.cgColor
qrCodeFrameView.layer.borderWidth = 2
view.addSubview(qrCodeFrameView)
}
} catch {
// If any error occurs, simply print it out and don't continue any more.
print(error)
return
}
}
func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) {
// Check if the metadataObjects array is not nil and it contains at least one object.
if metadataObjects == nil || metadataObjects.count == 0 {
qrCodeFrameView?.frame = CGRect.zero
messageLabel.text = "No QR/barcode is detected"
return
}
//Get metadata object
let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject
if supportedCodeTypes.contains(metadataObj.type) {
//if the found metadata is equal to the QR code metadata then update the status label's text and set the the bounds
let barCodeObject = videoPreviewLayer?.transformedMetadataObject(for: metadataObj)
qrCodeFrameView?.frame = barCodeObject!.bounds
if metadataObj.stringValue != nil {
messageLabel.text = metadataObj.stringValue
//Searches firebase for existing barcode
}
let itemToSearchFor = metadataObj.stringValue
let itemID = metadataObj.stringValue
guard let Description = productDescriptionTextField.text,
let price = priceTextField.text,
let location = productLocationTextField.text
else{
print("Fill basic product information")
return
}
let ref = FIRDatabase.database().reference(fromURL: " /")
// creating an item child node
let values = ["Item Description": Description, "Image": price, "Location": location, "Price": price ]
let items = ref.child("Items").child(itemID!)
items.updateChildValues(values, withCompletionBlock: { (err, ref) in
if err != nil {
print(err)
return
} })
FIRDatabase.database().reference().child("Items").child(itemToSearchFor!).observeSingleEvent(of: .value, with:{(snap) in
print(snap)
})
self.setupNewProductEntry()
self.setupenterNewProductButton()
}
}
I have one error when I tried to use ItemID within another function. I think this is because it is not globally defined, it is only defined within func captureOutput. Any ideas on how to globally define the barcode string values that I get from my barcode scanner?
You can move it beyond your class scopes, so it'll become global and visible through all your module
Declare it up here (where the comment is) to make it accessible.
class ScanController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
var captureSession: AVCaptureSession?
var videoPreviewLayer: AVCaptureVideoPreviewLayer?
var qrCodeFrameView: UIView?
//DECLARE itemID here
override func viewDidLoad() {
super.viewDidLoad()
//Get an instance of the AVCaptureDevice class a device object and provide the video as the media type parameter
let captureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
Just put "metadataObj" outside the class in the global scope (beginning of the file after the import section, I mean, outside the class declaration), so that it isn't a member of the class anymore (and initialize it with a value of your choice). If you declare it in a framework and want to use it in your app, then add the public access modifier, or you won't be able to access it.
If you want to put it in the scope of the class but accessible from everywhere in your code, add the static modifier (or static public if you declare it in a framework). Using static you'll create a single instance of the variable, not related to any particular instance of your class (one and only one instance of metadataObj shared among all instances of your class). Be careful if you access metadataObj from different threads... you could face very annoing faults, and difficult to fix. In this latter case consider thread synchronization (not exactly an easy subject to cope with)... I would discourage that if you haven't a very clear idea about what you need to do. If you, instead, need it only within the class scope (I mean, to be used by all the member functions... and only them), put it where Alec told you.

Swift CNCopySupportedInterfaces not valid

Trying to get the SSID of current device. I have found plenty of examples on how to do it however I am struggling with getting the CNCopySupportedInterfaces to autocomplete. I have 'import SystemConfiguration' at the top of my swift file but no success. Can't seem to figure out what I am doing wrong.
iOS 12
You must enable Access WiFi Information from capabilities.
Important
To use this function in iOS 12 and later, enable the Access WiFi Information capability for your app in Xcode. When you enable this capability, Xcode automatically adds the Access WiFi Information entitlement to your entitlements file and App ID. Documentation link
You need: import SystemConfiguration.CaptiveNetwork
Underneath the covers, CaptiveNetwork is a C header file (.h) that is within the SystemConfiguration framework:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SystemConfiguration.framework/Headers/CaptiveNetwork.h
If you know Objective-C, this goes into more depth:
iPhone get SSID without private library
You have to use the awkward syntax to bridge from any pure C API, so the following is required:
for interface in CNCopySupportedInterfaces().takeRetainedValue() as! [String] {
println("Looking up SSID info for \(interface)") // en0
let SSIDDict = CNCopyCurrentNetworkInfo(interface).takeRetainedValue() as! [String : AnyObject]
for d in SSIDDict.keys {
println("\(d): \(SSIDDict[d]!)")
}
}
ADDENDUM FOR SWIFT 2.2 and 3.0
The CFxxx datatypes are now bridged to native Objective-C runtime, eliminating the head-scratching retain calls. However, nullable pointers give rise to Optionals, so things don't get any shorter. At least, it's fairly clear what's going on, plus the nil helps us identify the simulator. The other answer uses an awful lot of bit-casting and unsafe operations which seems non-Swiftian, so I offer this.
func getInterfaces() -> Bool {
guard let unwrappedCFArrayInterfaces = CNCopySupportedInterfaces() else {
print("this must be a simulator, no interfaces found")
return false
}
guard let swiftInterfaces = (unwrappedCFArrayInterfaces as NSArray) as? [String] else {
print("System error: did not come back as array of Strings")
return false
}
for interface in swiftInterfaces {
print("Looking up SSID info for \(interface)") // en0
guard let unwrappedCFDictionaryForInterface = CNCopyCurrentNetworkInfo(interface) else {
print("System error: \(interface) has no information")
return false
}
guard let SSIDDict = (unwrappedCFDictionaryForInterface as NSDictionary) as? [String: AnyObject] else {
print("System error: interface information is not a string-keyed dictionary")
return false
}
for d in SSIDDict.keys {
print("\(d): \(SSIDDict[d]!)")
}
}
return true
}
Output on success:
SSIDDATA: <57696c6d 79>
BSSID: 12:34:56:78:9a:bc
SSID: YourSSIDHere
In Swift 2.0 / iOS 9 the API CaptiveNetwork is (nearly) gone or depreciated. I contacted Apple regarding this problem and I thought we could (or should) use the NEHotspotHelper instead. I got a respond from Apple today: One should continue to use CaptiveNetwork and the two relevant APIs (even tough there marked depreciated):
CNCopySupportedInterfaces
CNCopyCurrentNetworkInfo
The user braime posted an updated code-snippet for this problem on Ray Wenderlich forums:
let interfaces:CFArray! = CNCopySupportedInterfaces()
for i in 0..<CFArrayGetCount(interfaces){
let interfaceName: UnsafePointer<Void>
= CFArrayGetValueAtIndex(interfaces, i)
let rec = unsafeBitCast(interfaceName, AnyObject.self)
let unsafeInterfaceData = CNCopyCurrentNetworkInfo("\(rec)")
if unsafeInterfaceData != nil {
let interfaceData = unsafeInterfaceData! as Dictionary!
currentSSID = interfaceData["SSID"] as! String
} else {
currentSSID = ""
}
}
Works perfect for me.
Swift:
import SystemConfiguration.CaptiveNetwork
func currentSSIDs() -> [String] {
guard let interfaceNames = CNCopySupportedInterfaces() as? [String] else {
return []
}
return interfaceNames.flatMap { name in
guard let info = CNCopyCurrentNetworkInfo(name as CFString) as? [String:AnyObject] else {
return nil
}
guard let ssid = info[kCNNetworkInfoKeySSID as String] as? String else {
return nil
}
return ssid
}
}
Then print(currentSSIDs()), not working on simulator, only real devices.
Taken from https://forums.developer.apple.com/thread/50302
func getInterfaces() -> String? {
var ssid: String?
if let interfaces = CNCopySupportedInterfaces() as NSArray? {
for interface in interfaces {
if let interfaceInfo = CNCopyCurrentNetworkInfo(interface as! CFString) as NSDictionary? {
ssid = interfaceInfo[kCNNetworkInfoKeySSID as String] as? String
break
}
}
}
return ssid
}
In iOS 12 and up you will need to enable the Access WiFi Information capability for your app in order to get the ssid

iOS Simulator Custom Location issues

I am having issues with the iOS simulator and specifically the custom location setting for the iPhone. When I run the app the first time the simulator is opened it finds the location of the user without issues, however if I then change the custom location, and run the app again it gives the same location as the first time, despite having changed the custom location. If instead I set the Debug> Location > none in the simulator, and change the location in Product > Schemes > Edit Schemes in xCode itself, I have no issues. However every time I change the location this way I have to first set the location to none in the simulator. Is it a problem with my code, or just a quirk of the simulator that I wouldn't find with a real iPhone?
import UIKit
import CoreLocation
import MapKit
var userLocationCity : String!
var userLocationDate : String!
var safeUsername : String!
class TinderViewController: UIViewController, CLLocationManagerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
PFGeoPoint.geoPointForCurrentLocationInBackground { (geopoint: PFGeoPoint!, error: NSError!) -> Void in
if error == nil {
println(geopoint)
var longitude :CLLocationDegrees = geopoint.longitude
var latitude :CLLocationDegrees = geopoint.latitude
var location = CLLocation(latitude: latitude, longitude: longitude) //changed!!!
println(location)
var formatter: NSDateFormatter = NSDateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
let stringDate: String = formatter.stringFromDate(NSDate())
userLocationDate = stringDate
println(userLocationDate)
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
if error != nil {
println("Reverse geocoder failed with error" + error.localizedDescription)
return
}
if placemarks.count > 0 {
println(userLocationCity)
let pm = placemarks[0] as CLPlacemark
println(pm.locality)
println(userLocationCity)
userLocationCity = pm.locality
println(userLocationCity)
user["location"] = userLocationCity
user.save()
let string1 = PFUser.currentUser().objectId
let string2 = "ID_"
safeUsername = string2 + string1
var locate = PFObject(className: safeUsername)
locate.setObject(userLocationCity, forKey: "location")
locate.setObject(userLocationDate, forKey: "date")
locate.saveInBackgroundWithBlock {
(success: Bool!, error: NSError!) -> Void in
if success == true {
println("Score created with ID: \(locate.objectId)")
} else {
println(error)
}
}
}
else {
println("Problem with the data received from geocoder")
}
})
// user["location"] = geopoint
// user.save()
}
}
}
Yes, it sounds like the issue is that you are using two different methods to simulate location. You should choose either to simulate location via schemes or via the debug menu in XCode, but not through both. It sounds like you're doing both, and the setting in the debug menu is overriding the setting in your scheme.
I would strongly advise you, however, to test any location based code on an actual device. Most of the problems that you will find with location services will not appear on the simulator; you really need to deal with the actual peculiarities of real-world GPS hardware.

Resources