I've created a MacOS app project which contains System Network Extension (DNS proxy). With help of https://developer.apple.com/forums/thread/81103?answerId=246229022 and How to use NEDNSProxyProvider in iOS 11. I enabled DNSProxyProvider. When I built the app for the first time a prompt asked to allow the system extension. I allowed it and Under network in system preference, I can see DNS is not running (yellow dot).
Not running
I added OSSystemextensionRequest from the container App. This is my code.
import Cocoa
import NetworkExtension
import SystemExtensions
import OSLog
class ViewController: NSViewController {
let log=OSLog(subsystem:"com.example.applesamplecode.DNSTestBed", category: "app")
let manager = NEDNSProxyManager.shared()
private func installSystemExtension() {
os_log("DNSFProxy: installing system extension")
let request = OSSystemExtensionRequest.activationRequest(
forExtensionWithIdentifier: "my system extension's bundle indentifier",
queue: .main
)
request.delegate = self
OSSystemExtensionManager.shared.submitRequest(request)
}
private func enable() {
NSLog("enabled already ",self.manager.isEnabled )
NSLog(self.manager.isEnabled ? "Yes" : "No")
self.update {
self.manager.localizedDescription = "DNS"
let proto = NEDNSProxyProviderProtocol()
proto.serverAddress = "localhost"
proto.providerBundleIdentifier = "my system extension's bundle indentifier"
self.manager.providerProtocol = proto
self.manager.isEnabled = true
}
NSLog(self.manager.isEnabled ? "Yes" : "No")
}
private func disable() {
self.update {
self.manager.isEnabled = false
}
}
private func update(_ body: #escaping () -> Void) {
self.manager.loadFromPreferences { (error) in
guard error == nil else {
NSLog("DNS Test App: load error")
return
}
body()
self.manager.saveToPreferences { (error) in
guard error == nil else {
NSLog("DNS Test App: save error")
return
}
NSLog("DNS Test App: saved")
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.enable()
self.installSystemExtension()
}
}
extension ViewController: OSSystemExtensionRequestDelegate {
func request(_ request: OSSystemExtensionRequest, actionForReplacingExtension existing: OSSystemExtensionProperties, withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
os_log("DNSProxy: Replacing extension %# version %# with version %#", request.identifier, existing.bundleShortVersion, ext.bundleShortVersion)
return .replace
}
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
os_log("DNSProxy: Extension %# requires user approval", request.identifier)
}
func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) {
os_log("DNSProxy: System extension request failed: %#", error.localizedDescription)
}
/* Other delegate methods here */
func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) {
switch result {
case .completed:
var manager = NEDNSProxyManager.shared()
case .willCompleteAfterReboot:
os_log("DNSProxy: willCompleteAfterReboot")
#unknown default:
os_log("DNSProxy: default")
}
}
}
Info.plist of system extension:
info.plist
I'm getting an error when the activation request happens: The operation couldn’t be completed. (OSSystemExtensionErrorDomain error 1.). Any help would be great. Thanks.
P.S: I also posted this in apple forums https://developer.apple.com/forums/thread/717647
Related
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.
I'm new to iOS development, and am writing a VPN app, using the OpenVPN framework. I'm making use of the Network Extension target to tunnel network traffic to our OpenVPN server. However, when trying to debug, I can't attach the debugger to the Network Extension - it's stuck at "Waiting to Attach" in the Debug Navigator view.
In this blog post by Alex Grebenyuk he gives some recommendations for troubleshooting when you fail to attach the debugger to the extension. I've meticulously checked all of them, and my app correctly uses everything there: Both the app and the network extension have the Packet Tunnel Network Extension capability and bundle names all work out.
Some more info on the bundle names in case it might be relevant:
Bundle name for the app: com.example.Example-App
Bundle name for the Network Extension: com.example.Example-App.Example-VPN
Group name: group.com.example.Example-App
I'm out of ideas here. Could the problem be in the Network Extension never being started? It doesn't seem to be the case, because the startTunnel() function is called. The code for starting the VPN through the extension:
func configureVPN(serverAddress: String) {
guard let configData = readTestFile() else { return }
// Test file with ovpn configuration seems is read in correctly - contents are shown if printed to console.
self.providerManager?.loadFromPreferences { error in
if error == nil {
let tunnelProtocol = NETunnelProviderProtocol()
tunnelProtocol.serverAddress = serverAddress
tunnelProtocol.providerBundleIdentifier = "com.example.Example-App.Example-VPN" // bundle id of the network extension target
tunnelProtocol.providerConfiguration = ["ovpn": configData]
self.providerManager.protocolConfiguration = tunnelProtocol
self.providerManager.localizedDescription = "Example VPN"
self.providerManager.isEnabled = true
self.providerManager.saveToPreferences(completionHandler: { (error) in
if error == nil {
self.providerManager.loadFromPreferences(completionHandler: { (error) in
do {
print("Trying to start connection now") // This line is printed to the Console
try self.providerManager.connection.startVPNTunnel() // start the VPN tunnel.
} catch let error {
print(error.localizedDescription)
}
})
}
})
}
}
}
Not sure if this is relevant information, but I am using the OpenVPNAdapter in the PacketTunnelProvider, following this tutorial:
import NetworkExtension
import OpenVPNAdapter
class PacketTunnelProvider: NEPacketTunnelProvider {
lazy var vpnAdapter: OpenVPNAdapter = {
let adapter = OpenVPNAdapter()
adapter.delegate = self
return adapter
}()
let vpnReachability = OpenVPNReachability()
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
override func startTunnel(
options: [String : NSObject]?,
completionHandler: #escaping (Error?) -> Void
) {
// There are many ways to provide OpenVPN settings to the tunnel provider. For instance,
// you can use `options` argument of `startTunnel(options:completionHandler:)` method or get
// settings from `protocolConfiguration.providerConfiguration` property of `NEPacketTunnelProvider`
// class. Also you may provide just content of a ovpn file or use key:value pairs
// that may be provided exclusively or in addition to file content.
// In our case we need providerConfiguration dictionary to retrieve content
// of the OpenVPN configuration file. Other options related to the tunnel
// provider also can be stored there.
guard
let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration
else {
fatalError()
}
guard let ovpnFileContent: Data = providerConfiguration["ovpn"] as? Data else {
fatalError()
}
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnFileContent
configuration.settings = [ :
// Additional parameters as key:value pairs may be provided here
]
// Uncomment this line if you want to keep TUN interface active during pauses or reconnections
// configuration.tunPersist = true
// Apply OpenVPN configuration
let evaluation: OpenVPNConfigurationEvaluation
do {
evaluation = try vpnAdapter.apply(configuration: configuration)
} catch {
completionHandler(error)
return
}
// Provide credentials if needed
if !evaluation.autologin {
// If your VPN configuration requires user credentials you can provide them by
// `protocolConfiguration.username` and `protocolConfiguration.passwordReference`
// properties. It is recommended to use persistent keychain reference to a keychain
// item containing the password.
guard let username: String = protocolConfiguration.username else {
fatalError()
}
// Retrieve a password from the keychain
let password: String = "Test"
let credentials = OpenVPNCredentials()
credentials.username = username
credentials.password = password
do {
try vpnAdapter.provide(credentials: credentials)
} catch {
completionHandler(error)
return
}
}
// Checking reachability. In some cases after switching from cellular to
// WiFi the adapter still uses cellular data. Changing reachability forces
// reconnection so the adapter will use actual connection.
vpnReachability.startTracking { [weak self] status in
guard status == .reachableViaWiFi else { return }
self?.vpnAdapter.reconnect(afterTimeInterval: 5)
}
// Establish connection and wait for .connected event
startHandler = completionHandler
vpnAdapter.connect(using: packetFlow as! OpenVPNAdapterPacketFlow)
}
override func stopTunnel(
with reason: NEProviderStopReason,
completionHandler: #escaping () -> Void
) {
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
vpnAdapter.disconnect()
}
}
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
// `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
// protocol if the tunnel is configured without errors. Otherwise send nil.
// `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
// you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
// send `self.packetFlow` to `completionHandler` callback.
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
completionHandler: #escaping (Error?) -> Void
) {
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
// send empty string to NEDNSSettings.matchDomains
networkSettings?.dnsSettings?.matchDomains = [""]
// Set the network settings for the current tunneling session.
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
}
// Process events returned by the OpenVPN library
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
handleEvent event:
OpenVPNAdapterEvent, message: String?
) {
switch event {
case .connected:
if reasserting {
reasserting = false
}
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
}
// Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
// Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
fatal == true else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
if let startHandler = startHandler {
startHandler(error)
self.startHandler = nil
} else {
cancelTunnelWithError(error)
}
}
}
Any help with this would be hugely appreciated! Thanks.
You need to attache the bundle name and not the bundle identifier and the bundle name should not be an identifier. The PRODUCT_BUNDLE_IDENTIFIER of your network extension would be something like com.example.my-network-extension-id (it cannot be com.example.Example-App.Example-VPN, as a third period is not allowed for network extension IDs) and the PRODUCT_NAME would be something like My Network Extension, and you would then attach to My Network Extension (yes, with spaces and no extension).
I am making an app in which I stream data from native android and iOS side to flutter side and there I display the data in the UI.
I already did the android part. The android part is sending the data to flutter side and displays them in UI. But the problem is how to achieve same for iOS swift side.
Android code that works for me:
new EventChannel(getFlutterView(), "Eventchannelname").setStreamHandler(
new EventChannel.StreamHandler() {
#Override
public void onListen(Object args, EventChannel.EventSink events) {
Log.w(TAG, "adding listener");
mEventSink = events; // I use mEventsink.success(data) to pass the data to flutter side
#Override
public void onCancel(Object args) {
Log.w(TAG, "cancelling listener");
}
}
);
How can I achieve the same in Swift native code. I googled it and did not find anything that can help me.
I want same in swift as what I did in android java: I want to capture the events in local variable and then use that where I need to send data to flutter.
just call mEventSink as a function
mEventSink(data)
Use FlutterEndOfEventStream constant to signal end of stream
mEventSink(FlutterEndOfEventStream)
if you going to send error to flutter side use
mEventSink(FlutterError(code: "ERROR_CODE",
message: "Detailed message",
details: nil))
Reference to API DOC
Complete swift example
let eventChannel = FlutterEventChannel(name: "your.channel.id", binaryMessenger: messenger!)
eventChannel.setStreamHandler(SwiftStreamHandler())
....
class SwiftStreamHandler: NSObject, FlutterStreamHandler {
public func onListen(withArguments arguments: Any?, eventSink events: #escaping FlutterEventSink) -> FlutterError? {
events(true) // any generic type or more compex dictionary of [String:Any]
events(FlutterError(code: "ERROR_CODE",
message: "Detailed message",
details: nil)) // in case of errors
events(FlutterEndOfEventStream) // when stream is over
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
return nil
}
}
I also find an example in the official Flutter repo: https://github.com/flutter/flutter/blob/master/examples/platform_channel_swift
The code in Swift looks like the following:
import UIKit
import Flutter
enum ChannelName {
static let battery = "samples.flutter.io/battery"
static let charging = "samples.flutter.io/charging"
}
enum BatteryState {
static let charging = "charging"
static let discharging = "discharging"
}
enum MyFlutterErrorCode {
static let unavailable = "UNAVAILABLE"
}
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
GeneratedPluginRegistrant.register(with: self)
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("rootViewController is not type FlutterViewController")
}
let batteryChannel = FlutterMethodChannel(name: ChannelName.battery,
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
guard call.method == "getBatteryLevel" else {
result(FlutterMethodNotImplemented)
return
}
self?.receiveBatteryLevel(result: result)
})
let chargingChannel = FlutterEventChannel(name: ChannelName.charging,
binaryMessenger: controller.binaryMessenger)
chargingChannel.setStreamHandler(self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func receiveBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
guard device.batteryState != .unknown else {
result(FlutterError(code: MyFlutterErrorCode.unavailable,
message: "Battery info unavailable",
details: nil))
return
}
result(Int(device.batteryLevel * 100))
}
public func onListen(withArguments arguments: Any?,
eventSink: #escaping FlutterEventSink) -> FlutterError? {
self.eventSink = eventSink
UIDevice.current.isBatteryMonitoringEnabled = true
sendBatteryStateEvent()
NotificationCenter.default.addObserver(
self,
selector: #selector(AppDelegate.onBatteryStateDidChange),
name: UIDevice.batteryStateDidChangeNotification,
object: nil)
return nil
}
#objc private func onBatteryStateDidChange(notification: NSNotification) {
sendBatteryStateEvent()
}
private func sendBatteryStateEvent() {
guard let eventSink = eventSink else {
return
}
switch UIDevice.current.batteryState {
case .full:
eventSink(BatteryState.charging)
case .charging:
eventSink(BatteryState.charging)
case .unplugged:
eventSink(BatteryState.discharging)
default:
eventSink(FlutterError(code: MyFlutterErrorCode.unavailable,
message: "Charging status unavailable",
details: nil))
}
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
NotificationCenter.default.removeObserver(self)
eventSink = nil
return nil
}
}
I want to connect an iPhone to a web server running on my Macbook using MAMP and Starscream Library https://github.com/daltoniam/Starscream
Installed using Cocoapods and set up the web server with this URL:localhost:8888
But I'm having a lot of headaches because the code doesn't work... I followed all the guides online, the official documentation and so on but none of them helped me...
Whenever I run the code, the console shows this message: websocket is disconnected: Invalid HTTP upgrade
So maybe it's my server that's broken? I tried using instead ws://echo.websocket.org/ and it worked!!! So I thought... Because it finally showed me: websocket is connected
But at the same time it wasn't!!! In fact the method socket.isConnected gave me false... After that if I called socket.disconnect() it doesn't
work either, no messages show up.
import UIKit
import Starscream
class ViewController: UIViewController, WebSocketDelegate {
let socket = WebSocket(url: URL(string:"ws://echo.websocket.org/")!)//websocket is connected
/*
let socket = WebSocket(url: URL(string: "ws://localhost:8888/")!) //websocket is disconnected: Invalid HTTP upgrade
*/
override func viewDidLoad() {
super.viewDidLoad()
socket.delegate = self
socket.connect()
if socket.isConnected {
print("hi1") //it doesn't get printed
}
if !socket.isConnected {
print("hi2") //it gets printed
}
}
func websocketDidConnect(socket: WebSocketClient) {
print("websocket is connected")
}
func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
if let e = error as? WSError {
print("websocket is disconnected: \(e.message)")
} else if let e = error {
print("websocket is disconnected: \(e.localizedDescription)")
} else {
print("websocket disconnected")
}
}
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
print("Received text: \(text)")
}
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
print("Received data: \(data.count)")
}
}
So I finally understood the problem: Apache doesn't support Web Sockets by default... that's why I got the error message. Then the code works with "ws://echo.websocket.org/". You just have to put all socket.x functions outside viewDidLoad() to make them work, for example socket.disconnect in a Disconnect Button, socket.write in a sendButton etc.
For example:
let socket = WebSocket(url: URL(string: "ws://echo.websocket.org/")!)
override func viewDidLoad() {
super.viewDidLoad()
socket.delegate = self
socket.connect()
}
#IBAction func sendButtonPressed(_ sender: UIButton) {
socket.write(string: textField.text!)
}
I am trying to pass data from my app into my Apple Watch app. Basically, I am using the same method as I used for creating the today widget and so I am passing data through NSUserDefaults.
The problem is, that when I run my app, the data does not update the labels in the Watch app as I would expect it to.
Here is what I have...
override init(context: AnyObject?) {
// Initialize variables here.
super.init(context: context)
// Configure interface objects here.
NSLog("%# init", self)
var defaults = NSUserDefaults(suiteName: "group.AffordIt")
var totalBudgetCalculation = ""
if (defaults!.stringForKey("totalBudgetWidget") != nil) {
println("Worked")
totalBudgetCalculation = defaults!.stringForKey("totalBudgetWidget")!
initialBudgetLabel.setText("Initial: \(totalBudgetCalculation)")
}
var currentBudgetCalculation = ""
if (defaults!.stringForKey("currentBudgetWidget") != nil) {
currentBudgetCalculation = defaults!.stringForKey("currentBudgetWidget")!
currentBudgetLabel.setText("Current: \(currentBudgetCalculation)")
}
}
I tried putting this code in willActivate(), however that doesn't seem to make a difference.
Anyone know where I am going wrong?
This applies to OS 1 only. See below for better answers.
I got it working using your method. I guess there's a couple of things you can check:
1) Are you synchronising the defaults after you set the value:
defaults?.synchronize();
NSLog("%# ", defaults?.dictionaryRepresentation())
2) Have you enabled the App Group in both your app and your extension?
3) Are you using the correctly named app group when constructing the NSDefaults? For example, I use:
NSUserDefaults(suiteName: "group.com.brindysoft.MyWatch");
Once all that's set up I run the app, set the value in the defaults, then run the glance target which reads the value from the default and that seems to work!
Still stuck? check your app groups in your apple account
The accepted answer applies to apple watch os 1. See NSUserDefaults not working on Xcode beta with Watch OS2
For OS2 - you will need to use the WatchConnectivity frameworks and implement the WCSessionDelegate.
import WatchConnectivity
import WatchKit
#available(iOS 9.0, *)
var alertDelegate:HomeIC? = nil
public class WatchData: NSObject,WCSessionDelegate {
var session = WCSession.defaultSession()
//
class var shared: WatchData {
struct Static {
static var onceToken: dispatch_once_t = 0
static var instance: WatchData? = nil
}
dispatch_once(&Static.onceToken) {
Static.instance = WatchData()
}
return Static.instance!
}
public func session(session: WCSession, didReceiveFile file: WCSessionFile){
print(__FUNCTION__)
print(session)
}
public func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
print(__FUNCTION__)
print(session)
alertDelegate?.showMessage("didReceiveApplicationContext")
}
public func sessionReachabilityDidChange(session: WCSession){
print(__FUNCTION__)
print(session)
print("reachability changed:\(session.reachable)")
let text = session.reachable ? "reachable" : "unreachable"
alertDelegate?.showMessage(text)
}
public func sessionWatchStateDidChange(session: WCSession) {
print(__FUNCTION__)
print(session)
print("reachable:\(session.reachable)")
// alertDelegate?.showMessage("sessionWatchStateDidChange")
if !session.receivedApplicationContext.keys.isEmpty {
alertDelegate?.showMessage(session.receivedApplicationContext.description)
}
}
public func session(session: WCSession, didReceiveMessageData messageData: NSData){
if !session.receivedApplicationContext.keys.isEmpty {
alertDelegate?.showMessage(session.receivedApplicationContext.description)
}
}
public func session(session: WCSession, didReceiveMessage message: [String : AnyObject]){
print(__FUNCTION__)
if let data = message["data"] {
alertDelegate?.showMessage(data as! String)
return
}
}
public func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
print(__FUNCTION__)
if let data = message["data"] {
alertDelegate?.showMessage(data as! String)
return
}
guard message["request"] as? String == "showAlert" else {return}
}
public func activate(){
if WCSession.isSupported() { // it is supported
session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
print("watch activating WCSession")
} else {
print("watch does not support WCSession")
}
if(!session.reachable){
print("not reachable")
return
}else{
print("watch is reachable")
}
}
}
Sample Usage
class HomeIC: WKInterfaceController {
// MARK: Properties
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
// Initialize the `WCSession`.
WatchData.shared.activate()
alertDelegate = self
}
internal func showMessage(msg:String){
let defaultAction = WKAlertAction(title: msg, style: WKAlertActionStyle.Default) { () -> Void in }
let actions = [defaultAction]
self.presentAlertControllerWithTitle( "Info", message: "", preferredStyle: WKAlertControllerStyle.Alert, actions: actions)
}
}
in my iphone code / I can invoke sharing data here
if #available(iOS 9.0, *) {
WatchData.shared.sendInbox()
} else {
// Fallback on earlier versions
}
And somewhere else I have another discrete singleton for watch data session.
#available(iOS 9.0, *)
public class WatchData: NSObject,WCSessionDelegate {
var session = WCSession.defaultSession()
var payload:String = ""
class var shared: WatchData {
struct Static {
static var onceToken: dispatch_once_t = 0
static var instance: WatchData? = nil
}
dispatch_once(&Static.onceToken) {
Static.instance = WatchData()
}
return Static.instance!
}
public func sessionReachabilityDidChange(session: WCSession){
print(__FUNCTION__)
print(session)
print("reachability changed:\(session.reachable)")
if (session.reachable){
}
}
public func sessionWatchStateDidChange(session: WCSession) {
print(__FUNCTION__)
print(session)
print("reachable:\(session.reachable)")
}
public func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
print(__FUNCTION__)
guard message["request"] as? String == "showAlert" else {return}
guard let m = message["m"] as? String else { return }
print("msg:",m)
}
public func sendInbox(){
if (!session.reachable){
if WCSession.isSupported() { // it is supported
session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
print("iphone activating WCSession")
} else {
print("iphone does not support WCSession")
}
session.activateSession()
}
if(session.paired){
if(session.watchAppInstalled){
print("paired | watchAppInstalled")
}
}else{
print("not paired | or no watchAppInstalled")
}
if(!session.reachable){
print("not reachable")
return
}else{
/*let transfer:WCSessionUserInfoTransfer = (session.transferUserInfo(["data" : "Test2"]) as WCSessionUserInfoTransfer?)!
if(transfer.transferring){
print("-> iphone")
}else{
print("!-> iphone")
}*/
session.sendMessage(["data" :"test"],
replyHandler: { reply in
},
errorHandler: { error in
print(error)
})
}
}
}
Refer to sample watch os2 app
https://github.com/shu223/watchOS-2-Sampler/tree/20eeebeed66764d0814603e97d3aca5933236299
As #johndpope said, shared NSUserDefaults no longer work on WatchOS2.
I'm posting a simplified solution that's not as full featured as john's but will get the job done in most cases.
In your iPhone App, follow these steps:
Pick find the view controller that you want to push data to the Apple Watch from and add the framework at the top.
import WatchConnectivity
Now, establish a WatchConnectivity session with the watch and send some data.
if WCSession.isSupported() { //makes sure it's not an iPad or iPod
let watchSession = WCSession.defaultSession()
watchSession.delegate = self
watchSession.activateSession()
if watchSession.paired && watchSession.watchAppInstalled {
do {
try watchSession.updateApplicationContext(["foo": "bar"])
} catch let error as NSError {
print(error.description)
}
}
}
Please note, this will NOT work if you skip setting the delegate, so even if you never use it you must set it and add this extension:
extension MyViewController: WCSessionDelegate {
}
Now, in your watch app (this exact code works for Glances and other watch kit app types as well) you add the framework:
import WatchConnectivity
Then you set up the connectivity session:
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
let watchSession = WCSession.defaultSession()
watchSession.delegate = self
watchSession.activateSession()
}
and you simply listen and handle the messages from the iOS app:
extension InterfaceController: WCSessionDelegate {
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
print("\(applicationContext)")
dispatch_async(dispatch_get_main_queue(), {
//update UI here
})
}
}
That's all there is to it.
Items of note:
You can send a new applicationContext as often as you like and it
doesn't matter if the watch is nearby and connected or if the watch
app is running. This delivers the data in the background in an
intelligent way and that data is sitting there waiting when the
watch app is launched.
If your watch app is actually active and running, it should receive
the message immediately in most cases.
You can reverse this code to have the watch send messages to the
iPhone app the same way.
applicationContext that your watch app receives when it is viewed will ONLY be the last message you sent. If you sent 20 messages before the watch app is viewed, it will ignore the first 19 and handle the 20th one.
For doing a direct/hard connection between the 2 apps or for background file transfers or queued messaging, check out the WWDC video.
Another way to communicate between the app and the watch is via wormhole:
https://github.com/mutualmobile/MMWormhole
Send:
[self.wormhole passMessageObject:#{#"titleString" : title}
identifier:#"messageIdentifier"];
id messageObject = [self.wormhole messageWithIdentifier:#"messageIdentifier"];
Recieve:
[self.wormhole listenForMessageWithIdentifier:#"messageIdentifier"
listener:^(id messageObject) {
// Do Something
}];
Just use watch connectivity for communicate between these two platform you can read more about this in apple document
https://developer.apple.com/documentation/watchconnectivity