I wrote two applications, first using transferUserInfo, which caused too much lag (I believe because it sends stuff in background). I switched to sendMessage and was very happy with the results (faster response time). When attempted to run the application on my real iPhone and Apple Watch, I received Transfer timed out. Here's the full code and example of one of the debugs:
iPhone:
// ViewController.swift
import UIKit
import Foundation
import WatchConnectivity
class WatchManager: UIViewController, WCSessionDelegate {
var counter = 0
var watchSession: WCSession? {
didSet {
if let session = watchSession {
session.delegate = self
session.activate()
}
}
}
override func viewDidLoad(){
super.viewDidLoad()
watchSession = WCSession.default
}
private func sendDict(_ dict: [String: Any]) {
self.watchSession?.sendMessage(dict, replyHandler: nil, errorHandler: {error in print(error.localizedDescription)})
}
#IBOutlet weak var transferButton: UIButton!
#IBOutlet weak var label: UILabel!
#IBAction func dataTransfer(_ sender: Any) {
sendDict(["DataKey": counter])
counter+=1
print("sent")
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("Session activation did complete")
}
public func sessionDidBecomeInactive(_ session: WCSession) {
print("session did become inactive")
}
public func sessionDidDeactivate(_ session: WCSession) {
print("session did deactivate")
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print("phone received app context: ", message)
if let temperature = message["DataKey"] as? String {
DispatchQueue.main.async {
self.transferButton.setTitle(temperature, for: .normal)
self.label.text=temperature
}
}
}
}
Apple Watch:
// InterfaceController.swift
import WatchKit
import Foundation
import WatchConnectivity
class InterfaceController: WKInterfaceController {
var watchSession: WCSession? {
didSet {
if let session = watchSession {
session.delegate = self
session.activate()
}
}
}
#IBOutlet weak var temperatureLabel: WKInterfaceButton!
private func sendDict(_ dict: [String: Any]) {
self.watchSession?.sendMessage(dict, replyHandler: nil, errorHandler: {error in print(error.localizedDescription)})
}
#IBAction func button() {
let urg = ["DataKey":UUID().uuidString]
sendDict(urg)
print("watch sent app context \(urg)")
}
}
extension InterfaceController: WCSessionDelegate {
#if os(iOS)
public func sessionDidBecomeInactive(_ session: WCSession) { }
public func sessionDidDeactivate(_ session: WCSession) {
session.activate()
}
#endif
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("Session activation did complete")
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print("watch received app context: ", message)
if let temperature = message["DataKey"] as? Int {
self.temperatureLabel.setTitle(String(temperature))
}
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
watchSession = WCSession.default
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
}
Debug Example:
Session activation did complete
watch sent app context ["DataKey": "AF793FC6-7A16-4D7D-9A3B-D3BB960EC9D9"]
2019-01-13 21:07:43.524717-0800 testApp WatchKit Extension[1240:1178401] [WC] -[WCSession onqueue_handleMessageCompletionWithError:withMessageID:] C385FF5F-5EA1-478B-A930-54066C2F0B0F due to WCErrorCodeTransferTimedOut -> IDSErrorTypeTimedOut -> IDSResponseTimedOut
2019-01-13 21:07:43.525001-0800 testApp WatchKit Extension[1240:1178401] [WC] -[WCSession _onqueue_notifyOfMessageError:messageID:withErrorHandler:] C385FF5F-5EA1-478B-A930-54066C2F0B0F errorHandler: YES with WCErrorCodeTransferTimedOut -> IDSErrorTypeTimedOut -> IDSResponseTimedOut
Transfer timed out.
Related
This is my code.
When I click the captureButton it executes without error.
The purpose is to save a CIImage in captureProcessor.image
import UIKit
import AVFoundation
class ViewController: UIViewController {
let captureProcessor = captureProcess()
let session = AVCaptureSession()
let capturedPhotoOutPut = AVCapturePhotoOutput()
#IBOutlet weak var previewOfCamera: UIView!
#IBOutlet weak var imageShow: UIImageView!
#IBAction func captureButton(_ sender: Any) {
captureProcessor.capturePhoto(capturedPhotoOutPut)
// getCGRectInfo()
// code above is to get CGRect info from captureProcessor.image
}
override func viewDidLoad() {
super.viewDidLoad()
configurePreview()
}
func configureTheSession () -> Void {
var videoInput:AVCaptureDeviceInput!
let videoDevice = AVCaptureDevice.DiscoverySession(deviceType:[.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first
videoInput = try! AVCaptureDeviceInput(device: videoDevice!)
session.beginConfiguration()
session.sessionPreset = .hd1280x720
session.addInput(videoInput)
session.addOutput(capturedPhotoOutPut)
session.commitConfiguration()
DispatchQueue.global(qos: .userInitiated).async {
self.session.startRunning()
}
}
And here is another class mentioned above
import Foundation
import AVFoundation
class captureProcess: NSObject, AVCapturePhotoCaptureDelegate{
var image:CIImage!
func capturePhoto (_ photoOutput: AVCapturePhotoOutput) {
let settings = AVCapturePhotoSettings()
settings.flashMode = .off
photoOutput.capturePhoto(with: settings, delegate: self)
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
let imageData = photo.fileDataRepresentation()
image = CIImage(data: imageData!)
}
But if I add an action in #IBAction, e.g. getInfo (comment in first chunk of code). Program will omit capturePhoto (it runs this line but doesn't execute the delegate) but executes getCGRectInfo first. The image variable is still empty. So the program will crash.
Is there any way to execute delegate method once the program run to capturePhoto line so that I can get the CIImage info properly
I want to implement downloading functionality which can show completed status of downloading task with the percentage. And I'm able to do that but the problem is when the app is moving to the background and come back to the foreground at that time the delegate method didWriteData is not called in iOS12. Can anyone please help me? Here is my code
protocol DownloadDelagate {
func downloadingProgress(value:Float)
func downloadCompleted(identifier: Int,url: URL)
}
class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
static var shared = DownloadManager()
var delegate: DownloadDelagate?
var backgroundSessionCompletionHandler: (() -> Void)?
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
config.isDiscretionary = true
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
}
private override init() {
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
if let completionHandler = self.backgroundSessionCompletionHandler {
self.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
delegate?.downloadCompleted(identifier: downloadTask.taskIdentifier, url: location)
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
let progressPercentage = progress * 100
delegate?.downloadingProgress(value: progressPercentage)
print("Download with task identifier: \(downloadTask.taskIdentifier) is \(progressPercentage)% complete...")
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Task failed with error: \(error)")
} else {
print("Task completed successfully.")
}
}
}
Based on this thread this is a bug in NSURLSesstion. Currently there are known workaround for this (approved by Apple Engineers):
var session: URLSession?
...
func applicationDidBecomeActive(_ application: UIApplication) {
session?.getAllTasks { tasks in
tasks.first?.resume() // It is enough to call resume() on only one task
// If it didn't work, you can try to resume all
// tasks.forEach { $0.resume() }
}
}
Please try your code in AppDelegate's applicationWillEnterForeground(). You can make changes here when the app makes transition from Background to Active state.
I'm trying to make Google social helper is NSObject outside of ViewController. I'm present SignIn using UIApplication extension in root ViewController, but I still have an error.
'uiDelegate must either be a |UIViewController| or implement the |signIn:presentViewController:| and |signIn:dismissViewController:| methods from |GIDSignInUIDelegate|.'
This my social helper object
import GoogleSignIn
class GidHelper: NSObject, GIDSignInUIDelegate, GIDSignInDelegate {
private let succesAuth: (String, String, String, String) -> ()
private let failedAuth: (Error) -> ()
init(succesAuth: #escaping (String, String, String, String) -> (), failedAuth: #escaping (Error) -> ()) {
self.succesAuth = succesAuth
self.failedAuth = failedAuth
super.init()
GIDSignIn.sharedInstance().uiDelegate = self
GIDSignIn.sharedInstance().delegate = self
}
func openGidAuthorization() {
GIDSignIn.sharedInstance().signIn()
}
func gidLogout() {
GIDSignIn.sharedInstance().signOut()
}
// Present a view that prompts the user to sign in with Google
private func signIn(signIn: GIDSignIn!,
presentViewController viewController: UIViewController!) {
UIApplication.topViewController()?.present(viewController, animated: true, completion: nil)
}
private func signIn(signIn: GIDSignIn!,
dismissViewController viewController: UIViewController!) {
UIApplication.topViewController()?.dismiss(animated: true, completion: nil)
}
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
if let error = error {
print("\(error.localizedDescription)")
self.failedAuth(error)
} else {
let userId = user.userID
let accessToken = user.authentication.accessToken
let userAvatarUrl = user.profile.imageURL(withDimension: 100)?.absoluteString
let email = user.profile.email
self.succesAuth(accessToken!, userId!, email!, userAvatarUrl!)
}
}
}
My UIApplication extension:
import Foundation
import UIKit
extension UIApplication {
class func topViewController(controller: UIViewController? =
UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}
Your implementation of GidHelper class look very weird starting from the init and finishing with this extension:(. I recommend you to create a service (let it be your GoogleLoginService), make it a singleton and create also a NavigationService instead of this mess with extension. Here is some ideas how to implement this:
import GoogleSignIn
final class GoogleLoginService: NSObject {
typealias SignInResponse = (_ user: User?, _ error: Error?) -> ()
static let sharedInstance = GoogleLoginService()
private var presenter: UIViewController?
private var singInCompletion: SignInResponse?
//Call next function in appDelegate: didFinishLaunchingWithOptions
#discardableResult func registerInApplication(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if let url = Bundle.main.url(forResource: "GoogleService-Info", withExtension: "plist"),
let data = try? Data(contentsOf: url) {
let dictionary = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String : AnyObject]
if let clientID = dictionary??["CLIENT_ID"] {
GIDSignIn.sharedInstance().clientID = clientID as? String
}
}
GIDSignIn.sharedInstance().delegate = self
GIDSignIn.sharedInstance().uiDelegate = self
return true
}
// Call this function in AppDelegate: open url
#discardableResult func handleURLIn(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool {
return GIDSignIn.sharedInstance().handle(url, sourceApplication: options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String, annotation: options[UIApplication.OpenURLOptionsKey.annotation])
}
// MARK: - UserManagement
func signIn(_ controller: UIViewController, completion: SignInResponse?) {
singInCompletion = completion
presenter = controller
GIDSignIn.sharedInstance().signIn()
}
func signOut() {
GIDSignIn.sharedInstance().signOut()
}
func isLoggedIn() -> Bool {
return GIDSignIn.sharedInstance().hasAuthInKeychain()
}
}
extension GoogleLoginService: GIDSignInDelegate {
// MARK: - GIDSignInDelegate
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
if let error = error {
self.singInCompletion?(nil, error)
return
}
guard let authentication = user.authentication else {
self.singInCompletion?(nil, error)
return
}
let googleUserObj = User(name: user.profile.name) // <-- You can get your user data
}
}
extension GoogleLoginService: GIDSignInUIDelegate {
// MARK: - GIDSignInUIDelegate
func sign(_ signIn: GIDSignIn!, present viewController: UIViewController!) {
presenter?.present(viewController, animated: true, completion: nil)
}
func sign(_ signIn: GIDSignIn!, dismiss viewController: UIViewController!) {
presenter = nil
viewController.dismiss(animated: true, completion: nil)
}
}
Now using isLoggedIn method you can save a result in lets say UserDefaults and using NavigationService check if user is logged in or not and go to a proper view controller (as example have a look on the following method of NavigavionService:
func presentCurrentUserUI() {
// next line is extension on UserDefaults which keep Bool value - result of logging procedure
if UserDefaults().isLoggedIn {
let homeViewController = UIStoryboard(name: StoryboardName.main, bundle: nil).instantiateInitialViewController()
self.window?.rootViewController = homeViewController
} else {
let loginViewController = UIStoryboard(name: StoryboardName.login, bundle: nil).instantiateInitialViewController()
self.window?.rootViewController = loginViewController
}
self.window?.makeKeyAndVisible()
}
Hope it will help you to refactor your code and avoid this error:) Good luck!
I've been trying to send a variable to the watch from the iPhone. I've managed to send it with watchConnectivity but I can't get the picker in the watch app to update with the new variable I sent through.
Here's the code for the watch app:
import WatchKit
import Foundation
import WatchConnectivity
var bigDict = ["":""]
class InterfaceController: WKInterfaceController, WCSessionDelegate {
lazy var keys = Array(bigDict.keys)
lazy var values = Array(bigDict.values)
var pickerItems: [WKPickerItem] = []
#IBOutlet var pickerW: WKInterfacePicker!
#IBAction func pickerDidChange(_ value: Int) {
}
#IBAction func updateButton() {
for item in keys{
let pickerItem = WKPickerItem()
pickerItem.title = item
pickerItem.caption = bigDict[item]
pickerItems += [pickerItem]
}
pickerW.setItems(pickerItems)
}
//func refreshPickerItems() {
//for item in keys{
//let pickerItem = WKPickerItem()
//pickerItem.title = item
//pickerItem.caption = bigDict[item]
//pickerItems += [pickerItem]
// }
//pickerW.setItems(pickerItems)
// }
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Configure interface objects here.
//refreshPickerItems()
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print(message)
bigDict = message as! [String : String]
print(bigDict)
}
}
Shouldn't you call "updateButton()" when you receive the data?
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print(message)
bigDict = message as! [String : String]
updateButton()
print(bigDict)
}
This question already has answers here:
Type CCC doesnt conform to protocol 'NSObjectProtocol'
(3 answers)
Closed 6 years ago.
I get an error that my class doesn't conform the NSObjectProtocol, I don't know what this means. I have implemented all the function from the WCSessionDelegate so that is not the problem. Does somebody know what the issue is? Thanks!
import Foundation
import WatchConnectivity
class BatteryLevel: WCSessionDelegate {
var session: WCSession? {
didSet {
if let session = session {
session.delegate = self
session.activate()
}
}
}
var batteryStatus = 0.0;
func getBatteryLevel(){
if WCSession.isSupported() {
// 2
session = WCSession.default()
// 3
session!.sendMessage(["getBatteryLevel": ""], replyHandler: { (response) -> Void in
if (response["batteryLevel"] as? String) != nil {
self.batteryStatus = (response["batteryLevel"] as? Double)! * 100
}
}, errorHandler: { (error) -> Void in
// 6
print(error)
})
}}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
}
}
See Why in swift we cannot adopt a protocol without inheritance a class from NSObject?
In short, WCSessionDelegate itself inherits from NSObjectProtocol therefore you need to implement methods in that protocol, too. The easiest way to implement those methods is to subclass NSObject:
class BatteryLevel: NSObject, WCSessionDelegate
Note that you are dealing with Obj-C APIs here.