From f8b4d4252d6dce22715aabe1a603b176de67e520 Mon Sep 17 00:00:00 2001 From: SpiralArm Consulting Ltd Date: Tue, 29 Jan 2019 15:27:49 +0000 Subject: [PATCH] Watch app can now send data to Flutter app --- README.md | 15 +++- ios/Runner.xcodeproj/project.pbxproj | 10 +++ ios/Runner/AppDelegate.h | 11 ++- ios/Runner/AppDelegate.m | 105 +++++++++++++++++++++++++++ ios/Runner/Info.plist | 5 ++ lib/homescreen.dart | 47 ++++++++++++ 6 files changed, 190 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6f95896..e7bf9d8 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,22 @@ My plan is to: - *1.1* Add a blank watch project and get it working - *1.2* Modify the watch app functionally into WatchTips - *1.3* Update the Flutter app to WatchTips -- *1.4* **See if I can bridge between the 2 app (as the Titanium version does)** - +- *1.4* See if I can bridge between the 2 app (as the Titanium version does) +- **1.5 Tidy up both the Flutter code and the iOS project and use it to submit the app into the App Store** ## Versions +### 1.4 - Rough and Ready but Working! +I have now managed to add the required code into the iOS project to allow it to communicate with Flutter and send data updates from the watch so they are reflected in the app. +Basically if you do a calculation for a Tip on the watch then the data is transferred to the associated device. + +You need to forgive the (bad?) Objective C coding as it is not really my thing - who in their right mind thought of it in the first place anyway :-) . A lot of this was just brute force trial and error as well as a lot of searching and testing. + +Apart from that the difficult issue for me was that although **MethodChannel** is quite well documented, **EventChannel** is not, the only examples I found were to do with Plugin extensions, but again probably due to my dislike (due to not understanding it very well) of Objective C. + +I will try to create a generic plug-in later, but that will be done in Swift. + + ### 1.3 So after a bit of a hair pulling out session I have managed to get this working. I now have a iWatch app as well as a Flutter iOS app to build and run. I have tested this by building and distribuing to a physical phone via HockeyApp. Now the bad news...... diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e179190..b7df815 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -300,10 +300,20 @@ CreatedOnToolsVersion = 10.1; DevelopmentTeam = QJ23459J94; ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.BackgroundModes.watchos.extension = { + enabled = 0; + }; + }; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; DevelopmentTeam = QJ23459J94; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; + }; }; }; }; diff --git a/ios/Runner/AppDelegate.h b/ios/Runner/AppDelegate.h index 36e21bb..bfeff4a 100644 --- a/ios/Runner/AppDelegate.h +++ b/ios/Runner/AppDelegate.h @@ -1,6 +1,15 @@ #import #import +#import -@interface AppDelegate : FlutterAppDelegate +@interface WTUserInfoHandler : NSObject +@end + +// Modified to add the Watch Session Delegate +@interface AppDelegate : FlutterAppDelegate { + @private + WCSession *watchSession; + WTUserInfoHandler* userInfoStreamHandler; +} @end diff --git a/ios/Runner/AppDelegate.m b/ios/Runner/AppDelegate.m index 59a72e9..c0cd28b 100644 --- a/ios/Runner/AppDelegate.m +++ b/ios/Runner/AppDelegate.m @@ -1,11 +1,116 @@ #include "AppDelegate.h" #include "GeneratedPluginRegistrant.h" + +@implementation WTUserInfoHandler + +FlutterEventSink sink; +NSDictionary *info; + +- (void) updateUserInfo:(nonnull NSDictionary *)userInfo{ + NSLog(@"Update Recieved"); + if(info != userInfo){ + info = userInfo; + if(sink!=nil){ + sink(@[userInfo]); + } + } +} + +- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { + NSLog(@"Adding Flutter Listener"); + sink = eventSink; + return nil; +} + +- (FlutterError*)onCancelWithArguments:(id)arguments { + NSLog(@"Removing Flutter Listener"); + if(sink!=nil){ + sink = nil; + } + return nil; +} + +@end + + @implementation AppDelegate +#pragma mark watch session delegates + +// Event triggered when userInfo Rxd +- (void)session:(nonnull WCSession *)session didReceiveUserInfo:(nonnull NSDictionary *)userInfo +{ + if(userInfoStreamHandler != nil){ + [userInfoStreamHandler updateUserInfo:userInfo]; + } +} + + + + +#pragma mark watch session methods + +// Method used to enable the Watch session +- (void)activateSession +{ + [self activateChannel]; + [self watchSession]; +} + +// create our eventChannel +-(FlutterEventChannel *) activateChannel { + NSLog(@"activateChannel"); + userInfoStreamHandler = [[WTUserInfoHandler alloc] init]; + FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController; + FlutterEventChannel *eventChannel = [FlutterEventChannel eventChannelWithName:@"uk.spiralarm.watchtips/tipinfo/watchdata" binaryMessenger:controller]; + [eventChannel setStreamHandler:(NSObject * _Nullable) userInfoStreamHandler ]; + return eventChannel; +} + + +// activate session +- (WCSession *)watchSession +{ + if (watchSession == nil) { + if ([WCSession isSupported]) { + watchSession = [WCSession defaultSession]; + [watchSession setDelegate:self]; + [watchSession activateSession]; + } else { + NSLog(@"Error - Watch Connectivity NOT supported"); + } + } + return watchSession; +} +- (void)dealloc +{ + if (watchSession != nil) { + [watchSession setDelegate:nil]; + } +} + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + + FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController; + FlutterMethodChannel *tipinfoChannel = [FlutterMethodChannel + methodChannelWithName:@"uk.spiralarm.watchtips/tipinfo" + binaryMessenger:controller]; + + //__weak typeof(self) weakSelf = self; + [tipinfoChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { + + // activate Session + if([@"activateSession" isEqualToString:call.method]){ + [self activateSession]; + result(@"WatchTips Activated"); + } + + }]; + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 63b8434..79faec8 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -22,6 +22,11 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIBackgroundModes + + fetch + remote-notification + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/lib/homescreen.dart b/lib/homescreen.dart index 55a106e..725f362 100644 --- a/lib/homescreen.dart +++ b/lib/homescreen.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; class HomeScreen extends StatefulWidget { @@ -8,12 +10,35 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { + static const platform = const MethodChannel('uk.spiralarm.watchtips/tipinfo'); + static const stream = const EventChannel('uk.spiralarm.watchtips/tipinfo/watchdata'); + StreamSubscription tipSubscription; + final formatter = new NumberFormat("##0.00"); TextEditingController billTotalController = TextEditingController(text: "0.00"); int tipPercent = 10, tipSplit = 1; double billTotal = 0.0; double totalWithTip = 0.0; double totalEach = 0.0; + String buttonName = "Activate"; + + + @override + void initState() { + super.initState(); + activateWatchConnection(); + } + + + @override + void dispose() { + if(tipSubscription!=null){ + tipSubscription.cancel(); + tipSubscription = null; + } + super.dispose(); + } + @override Widget build(BuildContext context) { @@ -159,9 +184,31 @@ class _HomeScreenState extends State { total = (total ?? billTotal); setState(() { billTotal = total; + billTotalController.text = "${formatter.format(billTotal)}"; double tip = (total/100) * tipPercent; totalWithTip = total +tip; totalEach = (totalWithTip/tipSplit); }); } + + /// Active and start the connection to the Watch App + activateWatchConnection() async { + + // Start initial Session to allow watch an iOS to swap user data + await platform.invokeMethod("activateSession"); + + // Connect up our stream so we can monitor for watch updates + stream.receiveBroadcastStream().listen((value){ + List result = value; + debugPrint("${result[0]}"); + if(result[0]!=null){ + tipPercent = int.tryParse(result[0]['tip']); + tipSplit = int.tryParse(result[0]['split']); + billTotal = double.tryParse(result[0]['bill']); + calculateBill(null); + } + }); + + } + } \ No newline at end of file