diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 81ad591..2e508ad 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ A565D15527CEA6E600816E0B /* TableInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A565D15427CEA6E600816E0B /* TableInterfaceController.swift */; }; A565D15727CF6C5F00816E0B /* ConnectivityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A565D15627CF6C5F00816E0B /* ConnectivityService.swift */; }; A565D15927CF6C8500816E0B /* TableDataPersistanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A565D15827CF6C8500816E0B /* TableDataPersistanceService.swift */; }; + A565D15C27D0BA1700816E0B /* WeatherService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A565D15B27D0BA1700816E0B /* WeatherService.swift */; }; + A565D15E27D0CE0000816E0B /* WeatherInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A565D15D27D0CE0000816E0B /* WeatherInterfaceController.swift */; }; A597401D27CE9D790080003C /* TableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A597401C27CE9D790080003C /* TableViewCell.swift */; }; A5C47DA427CCCC6400DBE1C2 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A5C47DA227CCCC6400DBE1C2 /* Interface.storyboard */; }; A5C47DA627CCCC6600DBE1C2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5C47DA527CCCC6600DBE1C2 /* Assets.xcassets */; }; @@ -97,6 +99,8 @@ A565D15427CEA6E600816E0B /* TableInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableInterfaceController.swift; sourceTree = ""; }; A565D15627CF6C5F00816E0B /* ConnectivityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityService.swift; sourceTree = ""; }; A565D15827CF6C8500816E0B /* TableDataPersistanceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableDataPersistanceService.swift; sourceTree = ""; }; + A565D15B27D0BA1700816E0B /* WeatherService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherService.swift; sourceTree = ""; }; + A565D15D27D0CE0000816E0B /* WeatherInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherInterfaceController.swift; sourceTree = ""; }; A597401C27CE9D790080003C /* TableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCell.swift; sourceTree = ""; }; A5C47DA027CCCC6400DBE1C2 /* watch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = watch.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5C47DA327CCCC6400DBE1C2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; }; @@ -181,6 +185,7 @@ children = ( A565D15627CF6C5F00816E0B /* ConnectivityService.swift */, A565D15827CF6C8500816E0B /* TableDataPersistanceService.swift */, + A565D15B27D0BA1700816E0B /* WeatherService.swift */, ); path = Services; sourceTree = ""; @@ -200,6 +205,7 @@ A565D15A27CF6C9100816E0B /* Services */, A565D15427CEA6E600816E0B /* TableInterfaceController.swift */, A597401C27CE9D790080003C /* TableViewCell.swift */, + A565D15D27D0CE0000816E0B /* WeatherInterfaceController.swift */, A5C47DB027CCCC6600DBE1C2 /* InterfaceController.swift */, A5C47DB227CCCC6600DBE1C2 /* ExtensionDelegate.swift */, A5C47DB427CCCC6600DBE1C2 /* NotificationController.swift */, @@ -389,10 +395,12 @@ buildActionMask = 2147483647; files = ( A565D15927CF6C8500816E0B /* TableDataPersistanceService.swift in Sources */, + A565D15C27D0BA1700816E0B /* WeatherService.swift in Sources */, A565D15527CEA6E600816E0B /* TableInterfaceController.swift in Sources */, A597401D27CE9D790080003C /* TableViewCell.swift in Sources */, A565D15727CF6C5F00816E0B /* ConnectivityService.swift in Sources */, A5C47DB527CCCC6600DBE1C2 /* NotificationController.swift in Sources */, + A565D15E27D0CE0000816E0B /* WeatherInterfaceController.swift in Sources */, A5C47DB727CCCC6600DBE1C2 /* ComplicationController.swift in Sources */, A5C47DB327CCCC6600DBE1C2 /* ExtensionDelegate.swift in Sources */, A5C47DB127CCCC6600DBE1C2 /* InterfaceController.swift in Sources */, @@ -807,8 +815,9 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "watch WatchKit Extension/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "watch WatchKit Extension"; - INFOPLIST_KEY_CLKComplicationPrincipalClass = watch_WatchKit_Extension.ComplicationController; + INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_MODULE_NAME).ComplicationController"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.nativeCommunication.demo.demo; INFOPLIST_KEY_WKExtensionDelegateClassName = watch_WatchKit_Extension.ExtensionDelegate; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -849,8 +858,9 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "watch WatchKit Extension/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "watch WatchKit Extension"; - INFOPLIST_KEY_CLKComplicationPrincipalClass = watch_WatchKit_Extension.ComplicationController; + INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_MODULE_NAME).ComplicationController"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.nativeCommunication.demo.demo; INFOPLIST_KEY_WKExtensionDelegateClassName = watch_WatchKit_Extension.ExtensionDelegate; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -888,8 +898,9 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "watch WatchKit Extension/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "watch WatchKit Extension"; - INFOPLIST_KEY_CLKComplicationPrincipalClass = watch_WatchKit_Extension.ComplicationController; + INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_MODULE_NAME).ComplicationController"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.nativeCommunication.demo.demo; INFOPLIST_KEY_WKExtensionDelegateClassName = watch_WatchKit_Extension.ExtensionDelegate; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 779bd02..70b3fe9 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,23 +1,18 @@ import UIKit import Flutter -import WatchConnectivity +import WatchConnectivity @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { var counter: Int = 0 var flutterEventSink: FlutterEventSink? - let wcSession = WCSession.default var timer: Timer? - var methodChannel: FlutterMethodChannel? + let wcSession = WCSession.default override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - // Activate session with watch - activateSession() - // Initializing FlutterViewController, he is needed for the binary messenger let controller : FlutterViewController = window?.rootViewController as! FlutterViewController methodChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery", @@ -25,22 +20,51 @@ import WatchConnectivity // Event channel - stream on Flutter side let eventChannel = FlutterEventChannel(name: "samples.flutter.dev/counter", binaryMessenger: controller.binaryMessenger) eventChannel.setStreamHandler(self) + + // Activating session for watch + if( WCSession.isSupported()) { + wcSession.delegate = self + wcSession.activate() + } else { + print("Watch not supported") + } + // Incomming method invocations from Flutter side methodChannel?.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in - // Check conditions for messaging + // Check conditions for ANY messaging guard let watchSession = self?.wcSession, watchSession.activationState == .activated, watchSession.isPaired == true, watchSession.isWatchAppInstalled == true else { + print("Conditions for messaging are not met.") result(false) return } switch call.method { + case "presentTableData": + // Get data from arguments of a call from Flutter + guard let tableData = call.arguments as? Array else { return } + let watchData: [String: Any] = ["method": "presentTableData", "data": tableData] + print("Sedinging table data: \n", tableData) + // If reachable, go with live messaging, if not reachable update application context + if watchSession.isReachable == true { + //print("Watch app is reachable! Going live... ") + watchSession.sendMessage(watchData, replyHandler: nil, errorHandler: nil) + } else { + //print("Watch app is not reachable, updating context... ") + do { + try watchSession.updateApplicationContext(watchData) + } catch(_) { + //print("Error occurred while updating application context: ", error) + } + } + result(true) + case "incrementWatchCounter": guard let methodData = call.arguments as? Int else { result("false") @@ -48,33 +72,13 @@ import WatchConnectivity } let watchData: [String: Any] = ["method": "incrementWatchCounter", "data": methodData] + print("Sedinging incrementWatchCounter: \n", watchData) watchSession.sendMessage(watchData, replyHandler: nil, errorHandler: nil) result(true) case "getBatteryLevel": self?.receiveBatteryLevel(result: result) - case "presentTableData": - // Get data from message - guard let tableData = call.arguments as? Array else { - print("Table data is NOT a list of strings: ", call.arguments) - return - } - let watchData: [String: Any] = ["method": "presentTableData", "data": tableData] - // If reachable, go with live messaging, if not reachable update application context - if watchSession.isReachable == true { - print("Watch app is reachable! Going live... ") - watchSession.sendMessage(watchData, replyHandler: nil, errorHandler: nil) - } else { - print("Watch app is not reachable, updating context... ") - do { - try watchSession.updateApplicationContext(watchData) - } catch(let error) { - print("Error occurred while updating application context: ", error) - } - } - result(true) - default: result(FlutterMethodNotImplemented) } @@ -96,36 +100,9 @@ import WatchConnectivity result(Int(device.batteryLevel * 100)) } } - - func activateSession() { - if( WCSession.isSupported()) { - wcSession.delegate = self - wcSession.activate() - } - } - - } -// MARK: - Flutter stream handler -extension AppDelegate: FlutterStreamHandler { - func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - self.flutterEventSink = events - timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(onTimerUp), userInfo: nil, repeats: true) - return nil - } - - func onCancel(withArguments arguments: Any?) -> FlutterError? { - timer?.invalidate() - flutterEventSink = nil - return nil - } - - @objc func onTimerUp() { - counter += 1 - flutterEventSink?(counter) - } -} +// MARK: - WCSessionDelegate methods - extension AppDelegate: WCSessionDelegate { func sessionDidBecomeInactive(_ session: WCSession) { @@ -137,8 +114,6 @@ extension AppDelegate: WCSessionDelegate { } func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { - print("Received message: ", message) - // Invoking method for to Flutter side, MUST BE ON MAIN THREAD! DispatchQueue.main.async { [weak self] in if @@ -152,13 +127,24 @@ extension AppDelegate: WCSessionDelegate { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { print("@session did complete with: acctivation state: ", activationState.rawValue) } - - func sessionWatchStateDidChange(_ session: WCSession) { - print("Watch state changed: ") - print(" Activation state: ", session.activationState) - print(" Is paired: ", session.isPaired) - print(" Is reachable: ", session.isReachable) +} + +// MARK: - Flutter stream handler - +extension AppDelegate: FlutterStreamHandler { + func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + self.flutterEventSink = events + timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(onTimerUp), userInfo: nil, repeats: true) + return nil } + func onCancel(withArguments arguments: Any?) -> FlutterError? { + timer?.invalidate() + flutterEventSink = nil + return nil + } + @objc func onTimerUp() { + counter += 1 + flutterEventSink?(counter) + } } diff --git a/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json b/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json index 26454ca..5cd67a9 100644 --- a/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json +++ b/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "ghost.png", "idiom" : "watch", "scale" : "2x" }, diff --git a/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/ghost.png b/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/ghost.png new file mode 100644 index 0000000..ed2c5e3 Binary files /dev/null and b/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/ghost.png differ diff --git a/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json b/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json index 26454ca..5cd67a9 100644 --- a/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json +++ b/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "ghost.png", "idiom" : "watch", "scale" : "2x" }, diff --git a/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/ghost.png b/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/ghost.png new file mode 100644 index 0000000..ed2c5e3 Binary files /dev/null and b/ios/watch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/ghost.png differ diff --git a/ios/watch WatchKit Extension/ComplicationController.swift b/ios/watch WatchKit Extension/ComplicationController.swift index 8d9cf5a..ee35cdc 100644 --- a/ios/watch WatchKit Extension/ComplicationController.swift +++ b/ios/watch WatchKit Extension/ComplicationController.swift @@ -10,11 +10,15 @@ import ClockKit class ComplicationController: NSObject, CLKComplicationDataSource { + // MARK: - Properties - + + private let tableDataPersistanceService = TableDataPersistanceService() + // MARK: - Complication Configuration - + func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) { let descriptors = [ - CLKComplicationDescriptor(identifier: "complication", displayName: "Runner", supportedFamilies: CLKComplicationFamily.allCases) + CLKComplicationDescriptor(identifier: "complication", displayName: "Runner", supportedFamilies: [CLKComplicationFamily.circularSmall, CLKComplicationFamily.graphicCircular, CLKComplicationFamily.graphicCorner]) // Multiple complication support can be added here with more descriptors ] @@ -25,7 +29,7 @@ class ComplicationController: NSObject, CLKComplicationDataSource { func handleSharedComplicationDescriptors(_ complicationDescriptors: [CLKComplicationDescriptor]) { // Do any necessary work to support these newly shared complication descriptors } - + // MARK: - Timeline Configuration func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { @@ -37,23 +41,50 @@ class ComplicationController: NSObject, CLKComplicationDataSource { // Call the handler with your desired behavior when the device is locked handler(.showOnLockScreen) } - + // MARK: - Timeline Population func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { - // Call the handler with the current timeline entry - handler(nil) + if let template = getComplicationTemplate(for: complication, using: Date()) { + let entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template) + handler(entry) + } else { + handler(nil) + } } func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) { // Call the handler with the timeline entries after the given date handler(nil) } - + // MARK: - Sample Templates func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) { // This method will be called once per supported complication, and the results will be cached - handler(nil) + let template = getComplicationTemplate(for: complication, using: Date()) + if let t = template { + handler(t) + } else { + handler(nil) + } + } +} + + +// MARK: - Private methods - +private extension ComplicationController { + func getComplicationTemplate(for complication: CLKComplication, using date: Date) -> CLKComplicationTemplate? { + let text = tableDataPersistanceService.getTableData().first ?? "n/a" + switch complication.family { + case .graphicCorner: + return CLKComplicationTemplateGraphicCornerGaugeText(gaugeProvider: CLKSimpleGaugeProvider(style: .fill, gaugeColor: .purple, fillFraction: 0.44), outerTextProvider: CLKSimpleTextProvider(text: text)) + case .graphicCircular: + return CLKComplicationTemplateGraphicCircularStackText(line1TextProvider: CLKSimpleTextProvider(text: "\(text)1"), line2TextProvider: CLKSimpleTextProvider(text: "\(text)2")) + case .circularSmall: + return CLKComplicationTemplateCircularSmallSimpleText(textProvider: CLKSimpleTextProvider(text: text)) + default: + return nil + } } } diff --git a/ios/watch WatchKit Extension/ExtensionDelegate.swift b/ios/watch WatchKit Extension/ExtensionDelegate.swift index 729ccc0..80da632 100644 --- a/ios/watch WatchKit Extension/ExtensionDelegate.swift +++ b/ios/watch WatchKit Extension/ExtensionDelegate.swift @@ -6,11 +6,17 @@ // import WatchKit +import ClockKit class ExtensionDelegate: NSObject, WKExtensionDelegate { + + private let weatherService = WeatherService.shared func applicationDidFinishLaunching() { // Perform any final initialization of your application. + CommunicationService.instance.setupService() + weatherService.fetchWeatherBackground(isFirst: true) + updateComplications() } func applicationDidBecomeActive() { @@ -21,14 +27,17 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate { // 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, etc. } - + func handle(_ backgroundTasks: Set) { // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one. + print("HANDLE CALLED for some background tasks: \n", backgroundTasks) for task in backgroundTasks { // Use a switch statement to check the task type switch task { case let backgroundTask as WKApplicationRefreshBackgroundTask: // Be sure to complete the background task once you’re done. + print("APP REFRESH BACKGROUND TASK!: \n", backgroundTasks) + //WeatherService.shared.fetchWeatherBackground() backgroundTask.setTaskCompletedWithSnapshot(false) case let snapshotTask as WKSnapshotRefreshBackgroundTask: // Snapshot tasks have a unique completion call, make sure to set your expiration date @@ -38,7 +47,17 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate { connectivityTask.setTaskCompletedWithSnapshot(false) case let urlSessionTask as WKURLSessionRefreshBackgroundTask: // Be sure to complete the URL session task once you’re done. - urlSessionTask.setTaskCompletedWithSnapshot(false) + print("URL SESSION TASK!: \n", urlSessionTask) + weatherService.onUrlSessionBackgroundTaskCompleted = { [weak self] shouldUpdate in + //weatherService.fetchWeatherBackground(isFirst: false) // schedule anotheer refresh + if (shouldUpdate) { + print("UPDATING COMPLICATIONS") + self?.updateComplications() + } else { + print("Did not update any complications") + } + urlSessionTask.setTaskCompletedWithSnapshot(false) + } case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask: // Be sure to complete the relevant-shortcut task once you're done. relevantShortcutTask.setTaskCompletedWithSnapshot(false) @@ -53,3 +72,47 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate { } } + + +// MARK: - Private methods - + +private extension ExtensionDelegate { + func updateComplications() { + Task { + let complicationServer = CLKComplicationServer.sharedInstance() + let activeComplications = await complicationServer.getActiveComplications() + print("Active complications: ", activeComplications) + for complication in activeComplications { + complicationServer.reloadTimeline(for: complication) + } + } + } +} + + + +// This is used to get active complciations safely, check apple documetation +extension CLKComplicationServer { + + // Safely access the server's active complications, main actor enabels the code torun on the main thread + @MainActor + func getActiveComplications() async -> [CLKComplication] { + return await withCheckedContinuation { continuation in + + // First, set up the notification. + let center = NotificationCenter.default + let mainQueue = OperationQueue.main + var token: NSObjectProtocol? + token = center.addObserver(forName: .CLKComplicationServerActiveComplicationsDidChange, object: nil, queue: mainQueue) { _ in + center.removeObserver(token!) + continuation.resume(returning: self.activeComplications!) + } + + // Then check to see if we have a valid active complications array. + if activeComplications != nil { + center.removeObserver(token!) + continuation.resume(returning: self.activeComplications!) + } + } + } +} diff --git a/ios/watch WatchKit Extension/Info.plist b/ios/watch WatchKit Extension/Info.plist index 552fed0..9396ace 100644 --- a/ios/watch WatchKit Extension/Info.plist +++ b/ios/watch WatchKit Extension/Info.plist @@ -2,8 +2,6 @@ - WKCompanionAppBundleIdentifier - com.nativeCommunication.demo.demo NSExtension NSExtensionAttributes diff --git a/ios/watch WatchKit Extension/InterfaceController.swift b/ios/watch WatchKit Extension/InterfaceController.swift index a3af0ff..81bd2e1 100644 --- a/ios/watch WatchKit Extension/InterfaceController.swift +++ b/ios/watch WatchKit Extension/InterfaceController.swift @@ -7,20 +7,15 @@ import WatchKit import Foundation -import WatchConnectivity class InterfaceController: WKInterfaceController { @IBOutlet weak var label: WKInterfaceLabel! @IBOutlet weak var button: WKInterfaceButton! - var wcSession: WCSession? private var counter = 0 private let communicationService = CommunicationService.instance - override func awake(withContext context: Any?) { - communicationService.setupService() - //communicationService.addDelegate(self) } override func willActivate() { @@ -38,15 +33,14 @@ class InterfaceController: WKInterfaceController { extension InterfaceController: CommunicationServiceDelegate { var subscriptionTheme: WatchReceiveMethod { - .incrementWatchCounter + .incrementWatchCounter } var id: String { - "interfaceId" + "interfaceId" } func onDataReceived(data: Any?) { - print("Receieved data for counter: ", data) self.counter = (data as? Int) ?? 0 print("Receieved counter: ", counter) self.label.setText("Counter: \(self.counter)") diff --git a/ios/watch WatchKit Extension/Services/ConnectivityService.swift b/ios/watch WatchKit Extension/Services/ConnectivityService.swift index aa73c6b..ffb8479 100644 --- a/ios/watch WatchKit Extension/Services/ConnectivityService.swift +++ b/ios/watch WatchKit Extension/Services/ConnectivityService.swift @@ -23,15 +23,39 @@ enum WatchSendMethod: String { case sendCounterToFlutter } -final class CommunicationService: NSObject, WCSessionDelegate { +final class CommunicationService: NSObject { + + // MARK: - Properties - + static let instance = CommunicationService() + private var delegates = [CommunicationServiceDelegate]() private let tableDataPersistanceService = TableDataPersistanceService() private let wcSession = WCSession.default - // TODO: Add removal from delegates, so no calles are made when not needed. - private var delegates = [CommunicationServiceDelegate]() + + // MARK: - Int - private override init() {} +} + +// MARK: - WCSessionDelegate methods +extension CommunicationService: WCSessionDelegate { + func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + print("@session did complete with: acctivation state: ", activationState.rawValue) + print("Is reachable: ", wcSession.isReachable) + } + func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { + handleIncommingMessages(message: message, replyHandler: nil) + } + + func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { + handleIncommingMessages(message: applicationContext, replyHandler: nil) + } +} + +// MARK: - Public methods - + +extension CommunicationService { func setupService() { if(WCSession.isSupported()) { wcSession.delegate = self @@ -48,9 +72,6 @@ final class CommunicationService: NSObject, WCSessionDelegate { delegates.removeAll { delegate in delegate.id == id } - delegates.forEach { delegate in - print(delegate.id) - } print("Removed delegates, now list: ", delegates) } @@ -65,11 +86,15 @@ final class CommunicationService: NSObject, WCSessionDelegate { let messageData: [String: Any] = ["method": method, "data": data] wcSession.sendMessage(messageData, replyHandler: nil, errorHandler: nil) } - +} + +// MARK: - Private methods - + +private extension CommunicationService { func handleIncommingMessages(message: [String : Any], replyHandler: (([String : Any]) -> Void)?) { print("Watch received message: ", message) guard let method = message["method"] as? String, let subscriptionTheme = WatchReceiveMethod(rawValue: method) else { - print("No such method for watch: ", message["method"]) + print("No such method for watch: ", message["method"] ?? "n/a") return } let data = message["data"] @@ -77,8 +102,7 @@ final class CommunicationService: NSObject, WCSessionDelegate { if (subscriptionTheme == .presentTableData) { handleTableData(data: data) } - - print("Notifiy for delegates with theme: ", subscriptionTheme) + delegates.forEach { delegate in if (delegate.subscriptionTheme == subscriptionTheme) { delegate.onDataReceived(data: data) @@ -92,19 +116,4 @@ final class CommunicationService: NSObject, WCSessionDelegate { } tableDataPersistanceService.saveTableData(tableData) } - - func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { - print("@session did complete with: acctivation state: ", activationState.rawValue) - print("Activated state...") - print("Is reachable: ", wcSession.isReachable) - } - - func session(_ session: WCSession, didReceiveMessage message: [String : Any]) { - handleIncommingMessages(message: message, replyHandler: nil) - } - - func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { - handleIncommingMessages(message: applicationContext, replyHandler: nil) - } } - diff --git a/ios/watch WatchKit Extension/Services/WeatherService.swift b/ios/watch WatchKit Extension/Services/WeatherService.swift new file mode 100644 index 0000000..2c54ff2 --- /dev/null +++ b/ios/watch WatchKit Extension/Services/WeatherService.swift @@ -0,0 +1,181 @@ +// +// WeatherService.swift +// watch WatchKit Extension +// +// Created by Ivan Stajcer on 03.03.2022.. +// + +import Foundation +import WatchKit + +// api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API key} + +typealias WeatherCallback = (WeatherData) -> Void + +struct WeatherData: Codable { + let temperature: String + let windSpeed: String + let pressure: String + + static func mock() -> WeatherData { + WeatherData(temperature: "mock", windSpeed: "mock", pressure: "mock") + } + + static func createFromData(with data: Data) -> WeatherData { + guard let weatherDataJSON = try? JSONSerialization.jsonObject(with: data, options: .topLevelDictionaryAssumed) as? [String : Any] else { + print("JSON object failed to be created.") + return WeatherData.mock() + } + print("Created JSON object: \n", weatherDataJSON) + guard + let temp = (weatherDataJSON["main"] as? [String : Any])?["temp"] as? Double, + let wind = (weatherDataJSON["wind"] as? [String : Any])?["speed"] as? Double, + let pressure = (weatherDataJSON["main"] as? [String : Any])?["pressure"] as? Int + else { + print("Converting to data from JSON failed") + return WeatherData.mock() + } + print("Success!") + let weatherData = WeatherData(temperature: String(temp), windSpeed: String(wind), pressure: String(pressure)) + return weatherData + } +} + +protocol WeatherServiceProtocol { + func fetchWeatherForeground(completion: WeatherCallback?) + func fetchWeatherBackground(isFirst: Bool) +} + +final class WeatherService: NSObject { + + // MARK: - Properties - + + static let shared = WeatherService() + var onUrlSessionBackgroundTaskCompleted: ((_ shouldUpdate: Bool) -> Void)? + private let tableDataPersistanceService = TableDataPersistanceService() + private let apiKey = "33a595b1052037a58ebbd6503b0303ac" + private var backgroundTask: URLSessionTask? // Store task in order to complete it when finished + + // MARK: - Comupted properties - + + private var backgroundSession: URLSession { + let sessionIdentifier = "backgroundConfigurationIdentifier" + let config = URLSessionConfiguration.background(withIdentifier: sessionIdentifier) + config.isDiscretionary = false + config.sessionSendsLaunchEvents = true + + return URLSession(configuration: config, + delegate: self, + delegateQueue: nil) + + } + + // MARK: - Init - + + private override init() {} +} + +// MARK: - Public methods - + +extension WeatherService: WeatherServiceProtocol { + /* SCHEDULE A BACKGROOUND REFRESH TASK -> tis task DOES NOT ALLOW NETWORKING! + + Shedule task -> afte tim is up, system decides whento call and calles the WKExtension delegate handle method + + Can only have about 4 of them in 1h, when you have complications. Only 1 if not. + */ + func scheduleBackgroundRefresh() { + WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate: Date(timeIntervalSinceNow: 8), userInfo: nil) { error in + print("Error when shcedualing background refresh task: ", error) + } + } + + /* SCHEDULE A URL SESSION BACKGROUND TASK -> ALLOWS networking! + + Schedule task -> time passes -> WKEExtension delegategate 'handle' method called -> you set completion handler -> when task completed + URL session delegate method 'didFinishDownloadingTo' gets called -> you call your completion ahndler there + + Can only have about 4 of them in 1h, when you have complications. Only 1 if not. + */ + func fetchWeatherBackground(isFirst: Bool) { + if (backgroundTask == nil) { + let task = backgroundSession.downloadTask(with: getUrlRequest()) + // if not first task, schedule in 15 minutees + task.earliestBeginDate = Date().addingTimeInterval(isFirst ? 10 : 15*60) + backgroundTask = task + task.resume() + } + } + + // Can only be called while app in foreground + func fetchWeatherForeground(completion: WeatherCallback?) { + let task = URLSession.shared.dataTask(with: getUrlRequest()) { [weak self] data, response, error in + guard + let data = data, + let response = response as? HTTPURLResponse, + 200..<300 ~= response.statusCode + else { + return + } + let weatherData = WeatherData.createFromData(with: data) + completion?(weatherData) + } + task.resume() + } +} + +// MARK: - URLSessionDownloadDelegate methods - + +extension WeatherService: URLSessionDownloadDelegate { + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + processFile(at: location) + } + + private func processFile(at url: URL){ + if let data = try? Data(contentsOf: url) { + let weatherData = WeatherData.createFromData(with: data) + tableDataPersistanceService.saveTableData(["Bok", "From", "Background", "WOW"]) + onUrlSessionBackgroundTaskCompleted?(true) + print("Weather data from file downlaoded in background: \n", weatherData) + } else { + print("Can not get 'Data' from file at loaction: \n", url) + } + } +} + + +// MARK: - Private methods - + +private extension WeatherService { + func getUrlRequest() -> URLRequest { + var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/weather")! + urlComponents.queryItems = [URLQueryItem(name: "lat", value: "45.5550"), URLQueryItem(name: "lon", value: "18.6955"), URLQueryItem(name: "appid", value: "33a595b1052037a58ebbd6503b0303ac")] + var request = URLRequest(url: urlComponents.url!) + request.httpMethod = "GET" + return request + } +} + + +//extension WeatherService: URLSessionDownloadDelegate { +// func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { +// processFile(at: location) +// +// self.pendingBackgroundTasks.forEach { task in +// task.setTaskCompletedWithSnapshot(false) +// } +// } +// +// +// private func processFile(at url: URL){ +// if let data = try? Data(contentsOf: url) { +// let weatherData = WeatherData.createFromData(with: data) +// print("Weather data from file downlaoded in background: \n", weatherData) +// weatherLabel.setText("Temp: \(weatherData.temperature)") +// windLabel.setText("Wind: \(weatherData.windSpeed)") +// pressureLabel.setText("Pressure: \(weatherData.pressure)") +// } else { +// print("Can not get 'Data' from file at loaction: \n", url) +// } +// } +//} diff --git a/ios/watch WatchKit Extension/WeatherInterfaceController.swift b/ios/watch WatchKit Extension/WeatherInterfaceController.swift new file mode 100644 index 0000000..d7c5e0e --- /dev/null +++ b/ios/watch WatchKit Extension/WeatherInterfaceController.swift @@ -0,0 +1,44 @@ +// +// WeatherInterface.swift +// watch WatchKit Extension +// +// Created by Ivan Stajcer on 03.03.2022.. +// + +import Foundation +import WatchKit + +final class WeatherInterfaceController: WKInterfaceController { + @IBOutlet weak var weatherLabel: WKInterfaceLabel! + @IBOutlet weak var windLabel: WKInterfaceLabel! + @IBOutlet weak var pressureLabel: WKInterfaceLabel! + private var counter = 0 + private let weatherDataService = WeatherService.shared + + override func awake(withContext context: Any?) { + + } + + override func willActivate() { + print("WILL ACTIVATE") + //weatherDataService.fetchWeatherBackground(delegate: self) +// { [weak self] weatherData in + +// } + } + + override func willDisappear() { + } + + @IBAction func onFetchButtonPressed() { + WeatherService.shared.fetchWeatherForeground { [weak self] weatherData in + self?.updateUI(with: weatherData) + } + } + + func updateUI(with weatherData: WeatherData) { + weatherLabel.setText("Temp: \(weatherData.temperature)") + windLabel.setText("Wind: \(weatherData.windSpeed)") + pressureLabel.setText("Pressure: \(weatherData.pressure)") + } +} diff --git a/ios/watch/Base.lproj/Interface.storyboard b/ios/watch/Base.lproj/Interface.storyboard index 05c4497..9c68803 100644 --- a/ios/watch/Base.lproj/Interface.storyboard +++ b/ios/watch/Base.lproj/Interface.storyboard @@ -11,7 +11,7 @@ - @@ -29,7 +34,30 @@ - + + + + + + + + + + + + + + + + + @@ -56,7 +84,7 @@ - + @@ -73,21 +101,21 @@ - + - + - +