How to fix IUnityGraphicsMetal error when I run Xcode project - ios

I work to integrate an amazing AR model from Unity into Swift 4.
I finally did that using Unity 2018 (version 2018.2.4) and Swift 4.1 (Xcode 9.4.1) but when I run the project on my device (iPhone 7 Plus) is crushing and the error is coming from the path MyProjectName/Unity/Classes/Unity/IUnityGraphicsMetal.h
The error is: Thread 1: EXC_BAD_ACCESS (code=1, address=0x8).
I exported the project from Unity selecting Graphics API as Metal and also as OpenGLES3, none of this helped me.
Also in XCode -> Edit Scheme -> Run -> Options Tab -> Metal API Validation -> I set this one to Disabled, and I still get the same error.
I also update ARKit Plugin (in Unity) to the latest version (1.5) hoping that will fix the problem with UnityGraphicsMetal but apparently not.
Can anyone help me please to fix this error or to guide me to the wright way ?
Here I have 3 screenshots with the errors which maybe can help you more.
Thank you if you are reading this !
EDIT:
Here is the source code for my ViewController which is managing the bridge between Swift and Unity:
import UIKit
class ViewController: UIViewController {
#IBOutlet var rotateSwitch: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.startUnity()
NotificationCenter.default.addObserver(self, selector: #selector(handleUnityReady), name: NSNotification.Name("UnityReady"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleUnityToggleRotation(_:)), name: NSNotification.Name("UnityToggleRotation"), object: nil)
}
}
#objc func handleUnityReady() {
showUnitySubView()
}
#objc func handleUnityToggleRotation(_ n: NSNotification) {
if let isOn = n.userInfo?["isOn"] as? NSNumber {
rotateSwitch.isOn = isOn.boolValue
}
}
#IBAction func handleSwitchValueChanged(sender: UISwitch) {
UnityPostMessage("NATIVE_BRIDGE", "PlayHologram", sender.isOn ? "start" : "stop")
}
func showUnitySubView() {
if let unityView = UnityGetGLView() {
// insert subview at index 0 ensures unity view is behind current UI view
view?.insertSubview(unityView, at: 0)
unityView.translatesAutoresizingMaskIntoConstraints = false
let views = ["view": unityView]
let w = NSLayoutConstraint.constraints(withVisualFormat: "|-0-[view]-0-|", options: [], metrics: nil, views: views)
let h = NSLayoutConstraint.constraints(withVisualFormat: "V:|-75-[view]-0-|", options: [], metrics: nil, views: views)
view.addConstraints(w + h)
}
}
}
Here is the AppDelegate file:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var application: UIApplication?
#objc var currentUnityController: UnityAppController!
var isUnityRunning = false
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
self.application = application
unity_init(CommandLine.argc, CommandLine.unsafeArgv)
currentUnityController = UnityAppController()
currentUnityController.application(application, didFinishLaunchingWithOptions: launchOptions)
// first call to startUnity will do some init stuff, so just call it here and directly stop it again
startUnity()
stopUnity()
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// 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, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
if isUnityRunning {
currentUnityController.applicationWillResignActive(application)
}
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
if isUnityRunning {
currentUnityController.applicationDidEnterBackground(application)
}
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
if isUnityRunning {
currentUnityController.applicationWillEnterForeground(application)
}
}
func applicationDidBecomeActive(_ application: UIApplication) {
// 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.
if isUnityRunning {
currentUnityController.applicationDidBecomeActive(application)
}
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
if isUnityRunning {
currentUnityController.applicationWillTerminate(application)
}
}
func startUnity() {
if !isUnityRunning {
isUnityRunning = true
currentUnityController.applicationDidBecomeActive(application!)
}
}
func stopUnity() {
if isUnityRunning {
currentUnityController.applicationWillResignActive(application!)
isUnityRunning = false
}
}
}

This seems to be a tricky problem, here's a few things which may help, please try them:
In the Unity Editor -> Project Settings -> Player, there is also a checkbox option called "Metal API Validation", you said that you only set the Xcode option to disabled, perhaps this one needs to be disabled as well. Or a combination of on/off for the two.
Instead of selecting Metal and OpenGLES3, try ticking the checkbox "Auto Graphics API". In one of my projects, I left it on manual with Metal selected, but it wouldn't render at all on iOS, fixed it by turning on automatic.
In Color Gamut, have both sRGB and P3 Wide-colour selected. The iPhone 7 and newer all have displays rated for P3 wide-colour.
Hope this helps!

Related

Check when a system alert is shown in iOS

I have the following usecase.
I want to show an overlay over the current window when the application moves to the task manager.
Currently I am doing the following:
func applicationWillResignActive(_ application: UIApplication) {
guard let window = application.keyWindow else { return }
ApplicationPrivacyProtector.showOverlay(inKeyWindow: window)
}
func applicationDidBecomeActive(_ application: UIApplication) {
guard let window = application.keyWindow else { return }
ApplicationPrivacyProtector.hideOverlay(inKeyWindow: window)
}
This works fine but
applicationWillResignActive is also called when a system alert (eg. faceID, touchID, etc.) is shown which triggers this overlay view as well.
Is there a way to distinguish between system alert and application moves to taskmanager?

HomeKit - How to update accessory isReachable value, when i come back from background to foreground?

Situation: i am developing an app, which manages HomeKit accessories. For example i do that:
Accessory is power on, and i see it via app. Also in foreground mode HMAccessoryDelegate method:
func accessoryDidUpdateReachability(HMAccessory)
works fine and i can handle status of my accessory.
I switch app to background mode.
I turn off accessory (i mean completely power off) so it must be unreachable.
I switch app to foreground mode, but accessory is still reachable.
method func accessoryDidUpdateReachability(HMAccessory) — not called.
value accessory.isReachable not updated.
Example of code when i go to foreground:
func applicationDidBecomeActive(_ application: UIApplication) {
if let home = HomeStore.sharedStore.home {
for accessory in home.accessories {
print(accessory.isReachable) //not updated
for service in accessory.services {
for characteristic in service.characteristics {
characteristic.readValue { (error) in //updated
if error == nil {
let notification = Notification(name: Notification.Name(rawValue: "UpdatedCharacteristic"))
NotificationCenter.default.post(notification)
}
}
}
}
}
}
}
Question: how to update isReachable values of accessories, when i come back from background mode to foreground?
You can create a function in the ViewController that implements HMHomeManagerDelegate:
func startHomeManager() {
manager = HMHomeManager()
manager.delegate = self
// do something here
}
and add a call to startHomeManager() to your viewDidLoad(). That will refresh your HMHome object. Then call this func in your AppDelegate:
func applicationWillEnterForeground(_ application: UIApplication) {
viewController.startHomeManager()
viewController.didRestartHomeManager()
}
A bonus is that you can call startHomeManager() for pull to refresh, etc.

iOS applicationWillResignActive task not performed until app becomes active again

I'm trying to use applicationWillResignActive() in order to sync some data to my Firestore database before the application enters the background.
func applicationWillResignActive(_ application: UIApplication) {
self.uploadWantToPlay()
}
When I call my upload function from applicationWillResignActive() it runs but no data is added to Firestore before the next time the application becomes active.
When I for testing purposes instead run the same function from one of my ViewControllers the data is added instantly to Firestore.
I've also tried calling the function from applicationDidEnterBackground(), I've tried running it in it's own DispatchQueue. But it's had the same result.
How can I run this function as the user is about to leave the app and have it perform the database sync properly?
The functions handling the database sync;
func uploadWantToPlay() {
print ("Inside uploadWantToPlay")
if let wantToPlay = User.active.wantToPlayList {
if let listEntries = wantToPlay.list_entries {
let cleanedEntries = listEntries.compactMap({ (entry: ListEntry) -> ListEntry? in
if entry.game?.first_release_date != nil {
return entry
} else {
return nil
}
})
let gamesToUpload = cleanedEntries.filter {
$0.game!.first_release_date! > Int64(NSDate().timeIntervalSince1970 * 1000)
}
DatabaseConnection().writeWantToPlayToDatabase(user: User.active,wantToPlay: gamesToUpload)
}
}
}
func writeWantToPlayToDatabase(user: User, wantToPlay: [ListEntry]) {
firebaseSignIn()
let deviceId = ["\(user.deviceId)": "Device ID"]
for entry in wantToPlay {
let wantToPlayGameRef = fireStore.collection(WANTTOPLAY).document("\(entry.game!.id!)")
wantToPlayGameRef.updateData(deviceId) {(err) in
if err != nil {
wantToPlayGameRef.setData(deviceId) {(err) in
if let err = err {
Events().logException(withError: err, withMsg: "DatabaseConnection-writeWantToPlayToDatabase(user, [ListEntry]) Failed to write to database")
} else {
print("Document successfully written to WantToPlayGames")
}
}
} else {
print("Document successfully updated in WantToPlayGames")
}
}
}
}
According to the Apple documentation
Apps moving to the background are expected to put themselves into a
quiescent state as quickly as possible so that they can be suspended
by the system. If your app is in the middle of a task and needs a
little extra time to complete that task, it can call the
beginBackgroundTaskWithName:expirationHandler: or
beginBackgroundTaskWithExpirationHandler: method of the UIApplication
object to request some additional execution time. Calling either of
these methods delays the suspension of your app temporarily, giving it
a little extra time to finish its work. Upon completion of that work,
your app must call the endBackgroundTask: method to let the system
know that it is finished and can be suspended.
So, what you need to do here is to perform a finite length task while your app is being suspended. This will buy your app enough time to sync your records to the server.
An example snippet:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var backgroundTask: UIBackgroundTaskIdentifier!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
self.registerBackgroundTask()
// Do your background work here
print("Do your background work here")
// end the task when work is completed
self.endBackgroundTask()
}
func registerBackgroundTask() {
self.backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
self?.endBackgroundTask()
}
assert(self.backgroundTask != UIBackgroundTaskInvalid)
}
func endBackgroundTask() {
print("Background task ended.")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = UIBackgroundTaskInvalid
}
}
For further information refer to this article.

iOS Swift download lots of small files in background

In my app I need to download files with the following requirements:
download lots (say 3000) of small PNG files (say 5KB)
one by one
continue the download if the app in background
if an image download fails (typically because the internet connection has been lost), wait for X seconds and retry. If it fails Y times, then consider that the download failed.
be able to set a delay between each download to reduce the server load
Is iOS able to do that? I'm trying to use NSURLSession and NSURLSessionDownloadTask, without success (I'd like to avoid starting the 3000 download tasks at the same time).
EDIT: some code as requested by MwcsMac:
ViewController:
class ViewController: UIViewController, URLSessionDelegate, URLSessionDownloadDelegate {
// --------------------------------------------------------------------------------
// MARK: Attributes
lazy var downloadsSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier:"bgSessionConfigurationTest");
let session = URLSession(configuration: configuration, delegate: self, delegateQueue:self.queue);
return session;
}()
lazy var queue:OperationQueue = {
let queue = OperationQueue();
queue.name = "download";
queue.maxConcurrentOperationCount = 1;
return queue;
}()
var activeDownloads = [String: Download]();
var downloadedFilesCount:Int64 = 0;
var failedFilesCount:Int64 = 0;
var totalFilesCount:Int64 = 0;
// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
startButton.addTarget(self, action:#selector(onStartButtonClick), for:UIControlEvents.touchUpInside);
_ = self.downloadsSession
_ = self.queue
}
// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------
// MARK: User interaction
#objc
private func onStartButtonClick() {
startDownload();
}
// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------
// MARK: Utils
func startDownload() {
downloadedFilesCount = 0;
totalFilesCount = 0;
for i in 0 ..< 3000 {
let urlString:String = "http://server.url/\(i).png";
let url:URL = URL(string: urlString)!;
let download = Download(url:urlString);
download.downloadTask = downloadsSession.downloadTask(with: url);
download.downloadTask!.resume();
download.isDownloading = true;
activeDownloads[download.url] = download;
totalFilesCount += 1;
}
}
// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------
// MARK: URLSessionDownloadDelegate
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if(error != nil) { print("didCompleteWithError \(error)"); }
failedFilesCount += 1;
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
if let url = downloadTask.originalRequest?.url?.absoluteString {
activeDownloads[url] = nil
}
downloadedFilesCount += 1;
[eventually do something with the file]
DispatchQueue.main.async {
[update UI]
}
if(failedFilesCount + downloadedFilesCount == totalFilesCount) {
[all files have been downloaded]
}
}
// --------------------------------------------------------------------------------
// --------------------------------------------------------------------------------
// MARK: URLSessionDelegate
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
if let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
DispatchQueue.main.async { completionHandler() }
}
}
}
// --------------------------------------------------------------------------------
}
AppDelegate:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var backgroundSessionCompletionHandler: (() -> Void)?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// 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, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// 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.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
backgroundSessionCompletionHandler = completionHandler
}
}
Download:
class Download: NSObject {
var url: String
var isDownloading = false
var progress: Float = 0.0
var downloadTask: URLSessionDownloadTask?
var resumeData: Data?
init(url: String) {
self.url = url
}
}
What's wrong with this code:
I'm not sure that the background part is working. I followed this tutorial: https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started. It says that the app screenshot should be updated if I press home and then double tap home to show the app switcher. Does not seems to work reliably. It is updated when I re-open the app though. Having an iPhone since yesterday, I don't know if this is the normal behavior?
the 3000 downloads are started in the startDownload method. The maxConcurrentOperationCount of the queue does not seem to be respected: downloads are running concurrently
the downloadsSession.downloadTask(with: url); call takes 30ms. Multiplied by 3000, it takes 1mn30, that's a big problem :/ . Waiting a few seconds (2-3) would be ok.
I can't set a delay between two downloads (that's not a big problem. Would be nice though, but if I can't it will be OK)
Ideally, I would run the startDownload method asynchronously, and download the files synchronously in the for loop. But I guess I can't do that in background with iOS?
So here is what I finally did:
start the download in a thread, allowed to run for a few minutes in background (with UIApplication.shared.beginBackgroundTask)
download files one by one in a loop with a custom download method allowing to set a timeout
before downloading each file, check if UIApplication.shared.backgroundTimeRemaining is greater than 15
if yes, download the file with a timeout of min(60, UIApplication.shared.backgroundTimeRemaining - 5)
if no, stop downloading and save the download progress in the user defaults, in order to be able to resume it when the user navigates back to the app
when the user navigates back to the app, check if a state has been saved, and if so resume the download.
This way the download continues for a few minutes (3 on iOS 10) when the user leaves the app, and is pause just before these 3 minutes are elapsed. If the user leaves the app in background for more than 3 minutes, he must come back to finish the download.

CLLocationManager startUpdatingLocation() doesn't work when called in background

I have a particular use case where I want to be able to start location updates when the app is already in the background. Specifically, I want to be able to press a button on my Pebble watchapp, and that causes my companion iOS app to begin location updates.
I am able to successfully start the location updates from my Pebble only if the iOS app is either in the foreground or has just entered the background within the past few seconds (like 5?). After a few seconds has passed with the app in the background, I can no longer start the updates. I know that the updates haven't started, since the backgroundTimeRemaining starts counting down to 0 (and if the updates did start, it stays at the max value). I also have the blue bar at the top that shows when location updates are on ("_____ is Using Your Location") and that also fails to show up after this weird ~5 second threshold has passed.
I've looked at almost all of the related questions on SO, and I've already tried basically everything that's been suggested, such as starting a background task right before starting the location updates. I've also tried delaying the start of the location updates by a couple seconds after starting the background task, and that doesn't work either.
The strange thing is, I also have an Apple Watch app that does the same things as the Pebble app, and the Apple Watch is able to start the location updates without fail all the time. Maybe the Apple Watch can do something special where it unsuspends the iOS app?
This is on iOS 9.
Any kind of help is appreciated, I'll try anything that's suggested.
Update: I put together a quick sample app that demonstrates the issue.
The code is posted below, or you can download the project from GitHub:
https://github.com/JessicaYeh/BackgroundLocationTest
If you try it out, you can see that setting the delay to 2 seconds works, but when the delay is longer (like 10 seconds), the updates don't start.
//
// AppDelegate.swift
// BackgroundLocationTest
//
// Created by Jessica Yeh on 2/18/16.
// Copyright © 2016 Yeh. All rights reserved.
//
import UIKit
import CoreLocation
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let locationManager = CLLocationManager()
var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
var timer: NSTimer?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
debugPrint(__FUNCTION__)
// Setup location manager
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.allowsBackgroundLocationUpdates = true
return true
}
#objc func checkBackgroundTimeRemaining() {
debugPrint(UIApplication.sharedApplication().backgroundTimeRemaining)
}
#objc func endBackgroundTask() {
if self.backgroundTask != UIBackgroundTaskInvalid {
UIApplication.sharedApplication().endBackgroundTask(self.backgroundTask)
self.backgroundTask = UIBackgroundTaskInvalid
}
}
func applicationWillResignActive(application: UIApplication) {
debugPrint(__FUNCTION__)
}
func applicationDidEnterBackground(application: UIApplication) {
debugPrint(__FUNCTION__)
// Cancel old timer and background task
timer?.invalidate()
endBackgroundTask()
// Start a new background task
backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler {
self.endBackgroundTask()
}
// Start updating location
// let delaySec = 2.0 // successfully starts location update
let delaySec = 10.0 // doesn't start location update
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delaySec * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue(), {
debugPrint("Attempting to start updating location")
self.locationManager.startUpdatingLocation()
})
// Keep checking every 3 sec how much background time app has left
timer = NSTimer.scheduledTimerWithTimeInterval(3.0, target: self, selector: Selector("checkBackgroundTimeRemaining"), userInfo: nil, repeats: true)
}
func applicationWillEnterForeground(application: UIApplication) {
debugPrint(__FUNCTION__)
}
func applicationDidBecomeActive(application: UIApplication) {
debugPrint(__FUNCTION__)
}
func applicationWillTerminate(application: UIApplication) {
debugPrint(__FUNCTION__)
}
}
extension AppDelegate: CLLocationManagerDelegate {
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
debugPrint(__FUNCTION__)
}
}
Well, I don't exactly what you tried, but make sure you've turned on the Location updates on your target's Capabilities (Capabilities -> Background Modes) and also, for iOS 9.0, add this:
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"9.0")) {
self.locationManager.allowsBackgroundLocationUpdates = YES;
}
and change the line:
locationManager.requestWhenInUseAuthorization()
to:
locationManager.requestAlwaysAuthorization().
Good luck!

Resources