Native AVPlayerViewController called from JavaScript causing autolayout modification from background thread - ios

I am building a TVML/TVJS Apple TV app, but i need to be able to get some native functionality with the player, so I am using evaluateAppJavaScriptInContext to create a JavaScript function that will push a custom view controller to the screen when called. The problem is that it causes a warning in the console:
This application is modifying the autolayout engine from a background thread, which can lead to engine corruption and weird crashes. This will cause an exception in a future release.
The code looks like this:
import TVMLKit
import AVKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, TVApplicationControllerDelegate {
var window: UIWindow?
var appController: TVApplicationController?
var workoutViewController = WorkoutViewController()
static let TVBaseURL = "http://localhost:3000/"
static let TVBootURL = "\(AppDelegate.TVBaseURL)assets/tv.js"
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
window = UIWindow(frame: UIScreen.mainScreen().bounds)
// 1
let appControllerContext = TVApplicationControllerContext()
// 2
guard let javaScriptURL = NSURL(string: AppDelegate.TVBootURL) else {
fatalError("unable to create NSURL")
}
appControllerContext.javaScriptApplicationURL = javaScriptURL
appControllerContext.launchOptions["BASEURL"] = AppDelegate.TVBaseURL
// 3
appController = TVApplicationController(context: appControllerContext, window: window, delegate: self)
do {
guard let audioURL = NSURL(string: self.workoutViewController.audioURL) else {
fatalError("unable to create NSURL")
}
let audioPlayer = try AVAudioPlayer(contentsOfURL: audioURL)
if (audioPlayer.prepareToPlay()) {
audioPlayer.play()
}
} catch {
print("Error: \(error)")
}
return true
}
func appController(appController: TVApplicationController, evaluateAppJavaScriptInContext jsContext: JSContext) {
let presentWorkoutViewController : #convention(block) (String) -> Void = { (string : String) -> Void in
self.workoutViewController.jsonString = string
// dispatch_async(dispatch_get_main_queue(), {
self.appController?.navigationController.pushViewController(self.workoutViewController, animated: true)
// })
}
jsContext.setObject(unsafeBitCast(presentWorkoutViewController, AnyObject.self), forKeyedSubscript: "presentWorkoutViewController")
}
}
I tried to wrap it in a dispatch_async and that fixes the error, but when i try to push the native view controller back in view, it still contains its old content, and not the new content that i am trying to display.
That looked like this:
dispatch_async(dispatch_get_main_queue(), {
self.appController?.navigationController.pushViewController(self.workoutViewController, animated: true)
})
Thanks in advance!

Related

Forwarding user's location to server when application is in the background (Swift/iOS)

I need to keep my server updated with user's location even when the app is in the background or terminated.
The location updating is working just fine and seems to wake the application as wanted.
My problem is regarding the forwarding of the user's location via a PUT request to the server.
I was able to go through the code with breakpoints and it goes well except that when I check with Charles if requests are going though, nothing appears.
Here is what I have so far:
API Client
final class BackgroundNetwork: NSObject, BackgroundNetworkInterface, URLSessionDelegate {
private let keychainStorage: Storage
private var backgroundURLSession: URLSession?
init(keychainStorage: Storage) {
self.keychainStorage = keychainStorage
super.init()
defer {
let sessionConfiguration = URLSessionConfiguration.background(withIdentifier: "backgroundURLSession")
sessionConfiguration.sessionSendsLaunchEvents = true
sessionConfiguration.allowsCellularAccess = true
backgroundURLSession = URLSession(configuration: sessionConfiguration,
delegate: self,
delegateQueue: nil)
}
}
func put<T: Encodable>(url: URL, headers: Headers, body: T) {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "PUT"
let authenticationToken: String? = try? keychainStorage.get(forKey: StorageKeys.authenticationToken)
if let authenticationToken = authenticationToken {
urlRequest.setValue(String(format: "Bearer %#", authenticationToken), forHTTPHeaderField: "Authorization")
}
headers.forEach { (key, value) in
if let value = value as? String {
urlRequest.setValue(value, forHTTPHeaderField: key)
}
}
do {
let jsonData = try JSONEncoder().encode(body)
urlRequest.httpBody = jsonData
} catch {
#if DEBUG
print("\(error.localizedDescription)")
#endif
}
backgroundURLSession?.dataTask(with: urlRequest)
}
}
AppDelegate
// ...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if launchOptions?[UIApplication.LaunchOptionsKey.location] != nil {
environment.locationInteractor.backgroundDelegate = self
_ = environment.locationInteractor.start()
}
return true
}
// ...
extension AppDelegate: LocationInteractorBackgroundDelegate {
func locationDidUpdate(location: CLLocation) {
taskId = UIApplication.shared.beginBackgroundTask {
UIApplication.shared.endBackgroundTask(self.taskId)
self.taskId = .invalid
}
environment.tourInteractor.updateLocationFromBackground(latitude: Float(location.coordinate.latitude),
longitude: Float(location.coordinate.longitude))
UIApplication.shared.endBackgroundTask(taskId)
taskId = .invalid
}
}
SceneDelegate (yes, the application is using SwiftUI and Combine and I target iOS 13 or later)
func sceneWillEnterForeground(_ scene: UIScene) {
if let environment = (UIApplication.shared.delegate as? AppDelegate)?.environment {
environment.locationInteractor.backgroundDelegate = nil
}
}
func sceneDidEnterBackground(_ scene: UIScene) {
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
appDelegate.environment.locationInteractor.backgroundDelegate = appDelegate
_ = appDelegate.environment.locationInteractor.start()
}
}
So basically, whenever my app goes in background, I set my delegate, restart the location updates and whenever an update comes, my interactor is called and a request is triggered.
According to breakpoints, eveything just works fine up to backgroundURLSession?.dataTask(with: urlRequest). But for some reason the request never gets fired.
I obviously checked Background Modes capabilities Location updates and Background fetch.
Any idea why ?
That’s correct, the line
backgroundURLSession?.dataTask(with: urlRequest)
does nothing. The way to do networking with a session task is to say resume, and you never say that. Your task is created and just thrown away. (I’m surprised the compiler doesn’t warn about this.)

AVSystemController_SystemVolumeDidChangeNotification not giving callback for the first time

I want to check if both the volume buttons are working fine. So I set the observer AVSystemController_SystemVolumeDidChangeNotification to check that.
NotificationCenter.default.addObserver(self, selector: #selector(volumeCallback(notification:)), name: NSNotification.Name("AVSystemController_SystemVolumeDidChangeNotification"), object: nil)
Given is volumeCallback method:
#objc private func volumeCallback(notification: NSNotification) {
// check if app is in forground
guard UIApplication.shared.applicationState == .active else {
return
}
//get volume level
if let userInfo = notification.userInfo {
if let volumeChangeType = userInfo["AVSystemController_AudioVolumeChangeReasonNotificationParameter"] as? String {
if volumeChangeType == "ExplicitVolumeChange" {
print("value changed")
let level = userInfo["AVSystemController_AudioVolumeNotificationParameter"] as? Float
guard let volLevel = level else {
return
}
// my work here
}
}
}
}
Now the problem is, I am not getting callback in volumeCallback for the first installation of the app. The weird thing is, this method is being called when the app is in background, but not being called in foreground.
I am using iPhone 5s (iOS 10.3.3).
I don't understand what is the problem in this code. Any help will be appreciated.
This can be easily done with key-value observer as AVAudioSession provides outputVolume property. Check here.
You can just add observer on this property and get callbacks.
Here's a simple way of doing this in Swift 5:
// Audio session object
private let session = AVAudioSession.sharedInstance()
// Observer
private var progressObserver: NSKeyValueObservation!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
try session.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print("cannot activate session")
}
progressObserver = session.observe(\.outputVolume) { [weak self] (session, value) in
print(session.outputVolume)
}
return true
}

Got an "configurationError" when sending a transaction in Xcode

When I try to send a transaction on the Aion network, I keep getting the "configurationError". I am using Xcode to create my IOS dApp.
The code i have to send a transaction is:
#IBAction func sendTxButton(_ sender: Any) {
//deleted my address and pk
let address = "0x0"
let privateKey = "0x0"
let nonce = BigInt.init(3)
let to = "0xa0d969df9232b45239b577c3790887081b5a22ffd5a46a8d82584ee560485624"
let value = BigInt.init(10000000)
let nrgPrice = BigInt.init(10000000000)
let nrg = BigInt.init(21000)
var txParams = [AnyHashable: Any]()
txParams["nonce"] = HexStringUtil.prependZeroX(hex: nonce.toString(radix: 16))
txParams["to"] = to
txParams["data"] = ""
txParams["value"] = HexStringUtil.prependZeroX(hex: value.toString(radix: 16))
txParams["nrgPrice"] = HexStringUtil.prependZeroX(hex: nrgPrice.toString(radix: 16))
txParams["nrg"] = HexStringUtil.prependZeroX(hex: nrg.toString(radix: 16))
do {
let importWallet = try PocketAion.importWallet(privateKey: privateKey, subnetwork: "32", address: address, data: nil)
try PocketAion.eth.sendTransaction(wallet: importWallet, nonce: nonce, to: to, data: "", value: value, nrgPrice: nrgPrice, nrg: nrg, handler: { (result, error) in
if error != nil {
print(error!)
return
} else {
print("the hash:", result!)
}
})
} catch{
print(error)
}
}
I have met all of the requirements on sending a transaction, but cant figure out what is wrong.(this is for sending test tokens on the Aion test net "32").
check your AppDelegates class. make sure you have added "configuration" and point it to the right URL and under the application function you make sure you have the configuration is set to "self".
class AppDelegate: UIResponder, UIApplicationDelegate, Configuration {
public var nodeURL: URL{
get {
return URL.init(string: "https://aion.pokt.network")!
}
}
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
PocketAion.shared.setConfiguration(config: self)
return true
}

Show Facebook Events in Swift 3

How can I store Facebook events in an array using Swift 3? I have the following code which I pretty much copied from The Swift Guy but it doesn't work for this code. The following is in my viewDidLoad() function:
let url = URL(string: "https://www.facebook.com/events/upcoming")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("ERROR")
} else {
if let content = data {
do {
let myJson = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
print(myJson)
} catch {
print("error")
}
}
}
}
task.resume()
How can I get the information from the Facebook Events page? I should mention that I'm good at coding, but I'm a beginner when it comes to Swift so some explanation as to what each line does would be very helpful. Thanks!
The URL you're requesting returns an HTML page that you're trying to parse as if it was a JSON resource. You'll have to use the Facebook Graph API and/or the Facebook Swift SDK to get the information as JSON.
Try reading the Facebook developer documentation for more information:
https://developers.facebook.com/docs/swift
https://developers.facebook.com/docs/graph-api
Also, Swift Error objects contain information that can help you understand what went wrong. You can print them to the console. Try this:
if error != nil {
print(error)
} else {
// ...
The catch statement also sets its own error variable inside its block, so you can use:
} catch {
print(error)
}
Make sure you understand the steps involved in creating a Facebook app: registering the app with Facebook, downloading the SDK, adding the SDK to your project, configuring the SDK for your app, logging in to Facebook in your app and then calling the Facebook Graph API to get the information. These steps are all described in the Facebook documentation mentioned above. I'd start with the iOS SDK (Objective-C) instructions to setup your project and then change your app delegate and view controller to the following:
AppDelegate.swift:
import UIKit
import FacebookCore
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
SDKApplicationDelegate.shared.application(application, didFinishLaunchingWithOptions: launchOptions)
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
return SDKApplicationDelegate.shared.application(app, open: url, options: options)
}
}
ViewController.swift:
import UIKit
import FacebookCore
import FacebookLogin
class ViewController: UIViewController, LoginButtonDelegate {
override func viewDidLoad() {
super.viewDidLoad()
if let _ = AccessToken.current {
loadEvents()
}
let loginButton = LoginButton(readPermissions: [ .publicProfile, .userEvents ])
loginButton.center = view.center
loginButton.delegate = self
view.addSubview(loginButton)
}
func loginButtonDidCompleteLogin(_ loginButton: LoginButton, result: LoginResult) {
if let _ = AccessToken.current {
loadEvents()
}
}
func loginButtonDidLogOut(_ loginButton: LoginButton) {
// Logout handling code here
}
func loadEvents() {
let connection = GraphRequestConnection()
connection.add(GraphRequest(graphPath: "/me/events")) { httpResponse, result in
switch result {
case .success(let response):
print("Graph Request Succeeded: \(response)")
case .failed(let error):
print("Graph Request Failed: \(error)")
}
}
connection.start()
}
}
The response object will contain the Facebook Events information, already parsed.

Complication Timeline update with data from iPhone

I am trying to write a complication for watchOS 2 GM that displays a value it gets from my iPhone (iOS 9 GM) using WCSession.
Unfortunately I get the following error when sending a message:
Error Domain=WCErrorDomain Code=7014 "Payload could not be delivered." UserInfo={NSLocalizedDescription=Payload could not be delivered.}
This is what my code looks like in ComplicationController.swift:
import ClockKit
import WatchConnectivity
class ComplicationController: NSObject, CLKComplicationDataSource,WCSessionDelegate {
// MARK: - Timeline Configuration
var session : WCSession.defaultSession()
var myValue : Int?
...
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
getInfo()
if self.myValue != nil {
if complication.family == .CircularSmall {
let template = CLKComplicationTemplateCircularSmallRingText()
template.textProvider = CLKSimpleTextProvider(text: "\(self.myValue)")
template.fillFraction = Float(self.myValue!) / 100
template.ringStyle = CLKComplicationRingStyle.Closed
let timelineEntry = CLKComplicationTimelineEntry(date: NSDate(), complicationTemplate: template)
handler(timelineEntry)
} else {
handler(nil)
}
}
}
func requestedUpdateDidBegin(){
getInfo()
}
// MARK: - Update Scheduling
func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
// Call the handler with the date when you would next like to be given the opportunity to update your complication content
handler(NSDate(timeIntervalSinceNow: 5)); // only that low for debugging
}
func getInfo(){
if (WCSession.defaultSession().reachable) {
let messageToSend = ["Value":"Info"]
session.sendMessage(messageToSend, replyHandler: { replyMessage in
//handle and present the message on screen
let value:[String:AnyObject] = replyMessage
if value.indexForKey("myValue") != nil{
self.myValue = value["myValue"]! as? Int
print("Value: \(self.myValue)")
}
}, errorHandler: {error in
// catch any errors here
print(error)
})
}
}
This is my ExtensionDelegate.swift:
import WatchKit
import WatchConnectivity
class ExtensionDelegate: NSObject, WKExtensionDelegate,WCSessionDelegate {
var session:WCSession!
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
if (WCSession.isSupported()) {
session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
...
And finally my iOS AppDelegate:
import UIKit
import WatchConnectivity
class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate {
var window: UIWindow?
var myDevice: UIDevice?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self // conforms to WCSessionDelegate
session.activateSession()
}
application.statusBarStyle = UIStatusBarStyle.LightContent
return true
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
var reply = [String:AnyObject]()
// some logic
let value = //some Int value
reply.updateValue(value, forKey: "myValue")
replyHandler(reply)
}
Any ideas?
Thanks in advance!
A few things that will help you set things up so you can update your complications.
Generally, you'd want to have your timeline data already available for those points when the CLKComplicationDataSource methods are called. (Not always easy to do).
It looks like both your ComplicationController and ExtensionDelegate are being used as WCSessionDelegates. Use it in one place (probably ExtensionDelegate) and not the other, on the watch.
You have set up your AppDelegate to respond to a message, but any message handled by that didReceiveMessage method will only be coming from your Watch.
Determine where your message is originally coming from (maybe an external notification?), and send that info to the watch as a dictionary via WCSession 'send' methods.
Have your ExtensionDelegate (or whomever is responding to WCSessionDelegate methods) respond to the corresponding 'receive' methods to capture that sent info.
THEN: Kick off a refresh of your timeline by having the CLKComplicationServer reload your timeline.

Resources