iOS: UIWindow.rootViewController must be used from main thread only - ios

I have a problem by using
if let wd = UIApplication.shared.delegate?.window {
var vc = wd!.rootViewController
If I put this piece of code in a Dispatch, the warning message disappear, but the application doesn't display correctly.
If I remove the dispatch, I have warning message.
UIWindow.rootViewController must be used from main thread only
AND
UIApplication.delegate must be used from main thread only
That class is specially for downloading with a progressBar.
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("Download finished: \(location)")
...
do {
let result = try FileManager.default.replaceItemAt(URL(fileURLWithPath: Constants.Path.temp.stringByAppendingPathComponent(path: "temp.zip")), withItemAt: URL(fileURLWithPath: location.path))
let source = Constants.Path.tempZipFile
let destination = Constants.Path.temp.stringByAppendingPathComponent(path: "dezipped")
var myDict = [String:Any]()
myDict["source"] = source
myDict["destination"] = destination
DispatchQueue.main.async { //IF I REMOVE THIS => PB OR THREAD IN MAIN
if let wd = UIApplication.shared.delegate?.window {
var vc = wd!.rootViewController
if(vc is UINavigationController){
vc = (vc as! UINavigationController).visibleViewController
}
if(vc is WebViewController){
NotificationCenter.default.post(name: .DeflatSynchroFilesWebView, object: myDict, userInfo: nil)
}
else
{
NotificationCenter.default.post(name: .DeflatSynchroFiles, object: myDict, userInfo: nil)
}
}
}
} catch let writeError as NSError {
print("error writing file temp.zip to temp folder")
}
How to remove the warning without bugging my app?
Thanks in advance.

I am not sure if this can help, but to get the rootViewController I always use this:
if let window = UIApplication.shared.keyWindow?.rootViewController {
}
without the delegate

Related

How to update state from Phone view on WatchOS view?

I'm using a companion app to authorize a user with a 3rd party service. Once authorized, I update a UserDefaults variable to true. On the companion app side, the view updates correctly and shows that the user has been authenticated. However, on the watch OS side the view does not update. Would I need to use the Watch Connectivity API and send a message to the watch to update the state? Or is there a simple way?
Phone App
struct AuthenticationView: View {
#State private var startingWebAuthenticationSession = false
#AppStorage("authorized") private var authorized = false
var body: some View {
Group {
if !authorized {
VStack {
Button("Connect", action: { self.startingWebAuthenticationSession = true })
.webAuthenticationSession(isPresented: $startingWebAuthenticationSession) {
WebAuthenticationSession(
url: URL(string: "https://service.com/oauth/authorize?scope=email%2Cread_stats&response_type=code&redirect_uri=watch%3A%2F%2Foauth-callback&client_id=\(clientId)")!,
callbackURLScheme: callbackURLScheme
) { callbackURL, error in
guard error == nil, let successURL = callbackURL else {
return
}
let oAuthCode = NSURLComponents(string: (successURL.absoluteString))?.queryItems?.filter({$0.name == "code"}).first
guard let authorizationCode = oAuthCode?.value else { return }
let url = URL(string: "https://service.com/oauth/token")
var request = URLRequest(url: url!)
request.httpMethod = "POST"
let params = "client_id=\(clientId)&client_secret=\(clientSecret)&grant_type=authorization_code&code=\(authorizationCode)&redirect_uri=\(callbackURLScheme)://oauth-callback";
request.httpBody = params.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print("Error took place \(error)")
return
}
if let data = data, let response = String(data: data, encoding: .utf8) {
let accessTokenResponse: AccessTokenResponse = try! JSONDecoder().decode(AccessTokenResponse.self, from: response.data(using: .utf8)!)
let defaults = UserDefaults.standard
authorized = true
startingWebAuthenticationSession = false
defaults.set(accessTokenResponse.access_token, forKey: DefaultsKeys.accessToken) //TODO: Store securely
ConnectivityService.shared.send(authorized: true)
}
}
task.resume()
}
.prefersEphemeralWebBrowserSession(false)
}
}
}
else {
VStack {
Text("Authenticated!")
}
}
}
}
}
WatchOS
import SwiftUI
struct ConnectView: View {
#ObservedObject var connectivityService: ConnectivityService
var body: some View {
if !$connectivityService.authorized.wrappedValue {
VStack {
Text("Open the app on your primary device to connect.")
}
}
else {
//Some other view
}
}
}
EDIT:
Trying with Watch Connectivity API but the issue I'm experiencing is that when I authenticate from the phone, it'll take some time for the ConnectView to update the authorized variable. I know Watch Connectivity API doesn't update right away but at minimum I'd need some way for the watch to pick up that a secret access token has been retrieved and it can transition to the next view; whether that's through a shared state variable, UserDefaults, or whatever other mechanism.
Here is the ConnectivityService class I'm using:
import Foundation
import Combine
import WatchConnectivity
final class ConnectivityService: NSObject, ObservableObject {
static let shared = ConnectivityService()
#Published var authorized: Bool = false
override private init() {
super.init()
#if !os(watchOS)
guard WCSession.isSupported() else {
return
}
#endif
WCSession.default.delegate = self
WCSession.default.activate()
}
public func send(authorized: Bool, errorHandler: ((Error) -> Void)? = nil) {
guard WCSession.default.activationState == .activated else {
return
}
#if os(watchOS)
guard WCSession.default.isCompanionAppInstalled else {
return
}
#else
guard WCSession.default.isWatchAppInstalled else {
return
}
#endif
let authorizationInfo: [String: Bool] = [
DefaultsKeys.authorized: authorized
]
WCSession.default.sendMessage(authorizationInfo, replyHandler: nil)
WCSession.default.transferUserInfo(authorizationInfo)
}
}
extension ConnectivityService: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { }
func session(
_ session: WCSession,
didReceiveUserInfo userInfo: [String: Any] = [:]
) {
let key = DefaultsKeys.authorized
guard let authorized = userInfo[key] as? Bool else {
return
}
self.authorized = authorized
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
self.authorized = true
}
#if os(iOS)
func sessionDidBecomeInactive(_ session: WCSession) {
}
func sessionDidDeactivate(_ session: WCSession) {
WCSession.default.activate()
}
#endif
}
I tried doing these two lines but they have varying results:
WCSession.default.sendMessage(authorizationInfo, replyHandler: nil)
WCSession.default.transferUserInfo(authorizationInfo)
In the first line, XCode will say that no watch app could be found, even though I'm connected to both physical devices through XCode; launch phone first then watch. I believe the first one is immediate and the second is more of when the queue feels like it. Sometimes if I hard close the watch app, it'll pick up the state change in the authorized variable, sometimes it won't. Very frustrating inter-device communication.
UserDefaults doesn't pick up the access token value on the watch side. Maybe I have to use App Groups?
I do see this error on the Watch side:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
So I thought to try and encapsulate the self.authorized = authorized call into something like:
DispatchQueue.main.async {
self.authorized = authorized
}
But it didn't do anything as far as solving the immediate state change issue.

failed for URL: "snapchat://" - error: "This app is not allowed to query for scheme snapchat" [duplicate]

This question already has answers here:
canOpenURL: failed for URL: "instagram://app" - error: "This app is not allowed to query for scheme instagram"
(4 answers)
Closed 2 years ago.
I'm trying to use SnapKit, specifically Login Kit, and when I try to send the user to Snapchat to log in, I get the message in the debugger :
-canOpenURL: failed for URL: "snapchat://" - error: "This app is not allowed to query for scheme snapchat".
I also get in the debugger:
Warning: Attempt to present <SFSafariViewController: 0x7fb65485e800> on <App.LoginViewController: 0x7fb653f1f5f0> whose view is not in the window hierarchy!
This is the code I have to try and log in:
import UIKit
import SCSDKLoginKit
class LoginViewController: UIViewController {
#IBOutlet weak var loginButton: UIButton!
#IBOutlet weak var messageLabel: UILabel!
#IBAction func Loginn(_ sender: Any) {
SCSDKLoginClient.login(from: LoginViewController()) { (success: Bool, error: Error?) in
if success {
// Needs to be on the main thread to control the UI.
self.displayForLoginState()
}
if let error = error {
// Needs to be on the main thread to control the UI.
DispatchQueue.main.async {
self.messageLabel?.text = String.init(format: "Login failed. Details: %#", error.localizedDescription)
}
}
}
// loginButtonDidTap()
}
}
extension LoginViewController {
fileprivate func displayForLogoutState() {
// Needs to be on the main thread to control the UI.
DispatchQueue.main.async {
self.logoutButton?.isEnabled = false
//self.loginView.isHidden = false
//self.profileView.isHidden = true
self.messageLabel?.text = LoginViewController.DefaultMessage
}
}
fileprivate func displayForLoginState() {
// Needs to be on the main thread to control the UI.
DispatchQueue.main.async {
self.logoutButton?.isEnabled = true
//self.loginView?.isHidden = true
// self.profileView?.isHidden = false
self.messageLabel?.text = LoginViewController.DefaultMessage
}
displayProfile()
}
fileprivate func displayProfile() {
let successBlock = { (response: [AnyHashable: Any]?) in
guard let response = response as? [String: Any],
let data = response["data"] as? [String: Any],
let me = data["me"] as? [String: Any],
let displayName = me["displayName"] as? String,
let bitmoji = me["bitmoji"] as? [String: Any],
let avatar = bitmoji["avatar"] as? String else {
return
}
// Needs to be on the main thread to control the UI.
DispatchQueue.main.async {
self.loadAndDisplayAvatar(url: URL(string: avatar))
self.nameLabel?.text = displayName
}
}
let failureBlock = { (error: Error?, success: Bool) in
if let error = error {
print(String.init(format: "Failed to fetch user data. Details: %#", error.localizedDescription))
}
}
let queryString = "{me{externalId, displayName, bitmoji{avatar}}}"
SCSDKLoginClient.fetchUserData(withQuery: queryString,
variables: nil,
success: successBlock,
failure: failureBlock)
}
Not sure what the next steps are. If you have any ideas, please let me know. Thanks in advance.
You need to add the schemes you try to open in your app in the info.plist file. Add these lines in your info.plist right after the first <dict> keyword you see when you open it as source code.
<key>LSApplicationQueriesSchemes</key>
<array>
<string>snapchat</string>
</array>

How can I call a function from scene delegate which downloads an URL in a view controller?

I am downloading an image in my view controller. I want to update my image every time the app enters the foreground. I tried to call the function to download the image from the scene delegate, but unfortunately, I get the error "Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value", when I try that.
This is my code to download the image which works fine except when I call it from the scene delegate.
let urlSession = URLSession(configuration: .default)
let url = URL(string: "https://jarisstoriesphotographyphoto.files.wordpress.com/2020/06/menu1.png")!
// Create Data Task
let dataTask = urlSession.dataTask(with: url) { [weak self] (data, _, error) in
if let error = error {
print(error)
}
if let data = data {
DispatchQueue.main.async {
// Create Image and Update Image View
// self?.imageView.image
self?.imageView.image = UIImage(data: data)
}
}
}
// Start Data Task
dataTask.resume()
This is the code I used in my scene delegate. I also tried to call the download function in the "willConnectTo" but that gave me the same error.
let viewController = ViewController()
func sceneWillEnterForeground(_ scene: UIScene) {
viewController.downloadImage()
}
Help is very appreciated.
If you want to start a download task every time the app enters foreground, within a view controller then you should do the task in viewWillAppear of the view controller. Here's an example:
class ViewController: UIViewController {
// ...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let urlSession = URLSession(configuration: .default)
let url = URL(string: "https://jarisstoriesphotographyphoto.files.wordpress.com/2020/06/menu1.png")!
// Create Data Task
let dataTask = urlSession.dataTask(with: url) { [weak self] (data, _, error) in
if let error = error {
print(error)
}
if let data = data {
DispatchQueue.main.async {
// Create Image and Update Image View
// self?.imageView.image
self?.imageView.image = UIImage(data: data)
}
}
}
// Start Data Task
dataTask.resume()
}
}

Error while implementing AVAssetDownloadURLSession to download HLS stream

I'm trying to implement an offline mode to a streaming application.
The goal is to be able to download an HLS stream on the device of the user to make it possible to watch the stream even while the user is offline.
I have recently stumble on this tutorial.
It seems to answer the exact requirements of what I was trying to implement but I'm facing a problem while trying to make it work.
I've created a little DownloadManager to apply the logic of the tutorial.
Here is my singleton class:
import AVFoundation
class DownloadManager:NSObject {
static var shared = DownloadManager()
private var config: URLSessionConfiguration!
private var downloadSession: AVAssetDownloadURLSession!
override private init() {
super.init()
config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
downloadSession = AVAssetDownloadURLSession(configuration: config, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
}
func setupAssetDownload(_ url: URL) {
let options = [AVURLAssetAllowsCellularAccessKey: false]
let asset = AVURLAsset(url: url, options: options)
// Create new AVAssetDownloadTask for the desired asset
let downloadTask = downloadSession.makeAssetDownloadTask(asset: asset,
assetTitle: "Test Download",
assetArtworkData: nil,
options: nil)
// Start task and begin download
downloadTask?.resume()
}
func restorePendingDownloads() {
// Grab all the pending tasks associated with the downloadSession
downloadSession.getAllTasks { tasksArray in
// For each task, restore the state in the app
for task in tasksArray {
guard let downloadTask = task as? AVAssetDownloadTask else { break }
// Restore asset, progress indicators, state, etc...
let asset = downloadTask.urlAsset
downloadTask.resume()
}
}
}
func playOfflineAsset() -> AVURLAsset? {
guard let assetPath = UserDefaults.standard.value(forKey: "assetPath") as? String else {
// Present Error: No offline version of this asset available
return nil
}
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(assetPath)
let asset = AVURLAsset(url: assetURL)
if let cache = asset.assetCache, cache.isPlayableOffline {
return asset
// Set up player item and player and begin playback
} else {
return nil
// Present Error: No playable version of this asset exists offline
}
}
func getPath() -> String {
return UserDefaults.standard.value(forKey: "assetPath") as? String ?? ""
}
func deleteOfflineAsset() {
do {
let userDefaults = UserDefaults.standard
if let assetPath = userDefaults.value(forKey: "assetPath") as? String {
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(assetPath)
try FileManager.default.removeItem(at: assetURL)
userDefaults.removeObject(forKey: "assetPath")
}
} catch {
print("An error occured deleting offline asset: \(error)")
}
}
}
extension DownloadManager: AVAssetDownloadDelegate {
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
var percentComplete = 0.0
// Iterate through the loaded time ranges
for value in loadedTimeRanges {
// Unwrap the CMTimeRange from the NSValue
let loadedTimeRange = value.timeRangeValue
// Calculate the percentage of the total expected asset duration
percentComplete += loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds
}
percentComplete *= 100
debugPrint("Progress \( assetDownloadTask) \(percentComplete)")
let params = ["percent": percentComplete]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "completion"), object: nil, userInfo: params)
// Update UI state: post notification, update KVO state, invoke callback, etc.
}
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
// Do not move the asset from the download location
UserDefaults.standard.set(location.relativePath, forKey: "assetPath")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
debugPrint("Download finished: \(location)")
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
debugPrint("Task completed: \(task), error: \(String(describing: error))")
guard error == nil else { return }
guard let task = task as? AVAssetDownloadTask else { return }
print("DOWNLOAD: FINISHED")
}
}
My problem comes when I try to call my setupAssetDownload function.
Everytime time I try to resume a downloadTask I get an error message in the urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) delegate function.
The log of the message is:
Task completed: <__NSCFBackgroundAVAssetDownloadTask:
0x7ff57fc024a0>{ taskIdentifier: 1 }, error: Optional(Error
Domain=AVFoundationErrorDomain Code=-11800 \"The operation could not
be completed\" UserInfo={NSLocalizedFailureReason=An unknown error
occurred (-12780), NSLocalizedDescription=The operation could not be
completed})
To give you all the relevant information the URL I past to my setupAssetDownload function is of type
URL(string: "https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8")!
I been looking for a cause and solution for this error but I don't seem to be able to find one for the time being.
I would be very grateful for any tips or any clues on how resolve this issue or any indication of errors in my singleton implementation that could explain this behaviour.
Thank you in advance.
Martin
EDIT:
It seems that this bug occurs on a simulator. I launch my app on a real device and the download started without any problem. Hope this helps. Still don't understand why I cannot try this behaviour on a simulator.

Native AVPlayerViewController called from JavaScript causing autolayout modification from background thread

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!

Resources