Firebase runTransactionBlock() in iOS share extension - ios

My share extension has the following code as part of the didSelectPost() segment:
override func didSelectPost() {
if self.sharedURL != nil {
// Send data to Firebase
self.myRootRef.runTransactionBlock({
(currentData:FMutableData!) in
var value = currentData.value as? String
// Getting the current value
// and checking whether it's null
if value == nil {
value = ""
}
// Setting the new value to the clipboard
// content
currentData.value = self.sharedURL?.absoluteString
// Finalizing the transaction
return FTransactionResult.successWithValue(currentData)
}, andCompletionBlock: {
// Completion Check
(error:NSError!, success:Bool, data:FDataSnapshot!) in
print("DEBUG- We're done:\(success) and \(error)")
}
)
}
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
}
I'm getting the following error at runtime:
host connection <NSXPCConnection: 0x7fb84af2e8c0> connection from pid 16743 invalidated
I believe this error is due to the andCompletionBlock and related to the following issue: Debug info when run today extension
How can I cleanly and successfully deal with the completion status of the above transaction?

Like the answer you linked to stated, the NSXPCConnection error doesn't matter here.
The issue is that .runTransactionBlock() is asynchronous and .completeRequestReturningItems() will get called and exit the extension before you ever get a value from your Firebase database.
Try running .completeRequestReturningItems() in the andCompletionBlock.
override func didSelectPost() {
if self.sharedURL != nil {
// Send data to Firebase
self.myRootRef.runTransactionBlock({
(currentData:FMutableData!) in
var value = currentData.value as? String
// Getting the current value
// and checking whether it's null
if value == nil {
value = ""
}
// Setting the new value to the clipboard
// content
currentData.value = self.sharedURL?.absoluteString
// Finalizing the transaction
return FTransactionResult.successWithValue(currentData)
}, andCompletionBlock: {
// Completion Check
(error:NSError!, success:Bool, data:FDataSnapshot!) in
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
}
)
}
}

Related

How to check if user revoked HealthKit permissions through Settings and act in response to it?

I am using Apple's HealthKit to build an app, and am requesting permission from the user to read and write Sleep Data as soon as a View loads using SwiftUI's .onAppear modifier.
It all works fine and I get the data I need using this method.
However, if a user revokes the read and write permissions for my app through Settings, instead of requesting permission again, the app crashes. This is how I have set things up.
#State var authorized: Bool = false
var body: some View {
Text("\(initTextContent)")
.onAppear {
healthstore.getHealthAuthorization(completionAuth: {success in
authorized = success
if self.authorized == true {
healthstore.getSleepMetrics(startDate: sevendaysAgo, endDate: sevendaysAfter, completion: sleepDictAssign)
}
else {
initTextContent = "Required Authorization Not Provided"
}
})
}
}
I've created a class called healthstore and am simply using HealthKit's requestAuthorization method as follows:
var healthStore: HKHealthStore()
func getHealthAuthorization(completionAuth: #escaping (Bool) -> Void) {
//we will call this inside our view
healthStore.requestAuthorization(toShare: [sleepSampleType], read: [sleepSampleType]) { (success, error) in
if let error = error {
// Handle the error here.
fatalError("*** An error occurred \(error.localizedDescription) ***")
}
else {
completion(success)
}
}
}
If the user revoked the permisions they are revoked. You cannot ask again. If you want to handle this scenario you would need to throw the error and handle it outside of the function by showing a message to the user asking them to enable it again.
Or simply return the success boolean ignoring the error.
func getHealthAuthorization(completionAuth: #escaping (Bool) -> Void) {
//we will call this inside our view
healthStore.requestAuthorization(toShare: [sleepSampleType], read: [sleepSampleType]) { (success, error) in
//ignore error since your func can just return a boolean
completion(success)
}
}

Unable to attach debugger to Network Extension iOS ("Waiting to Attach")

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).

UI Update Delegate Method Not Called

Working on a practice project (solution found here: https://github.com/appbrewery/ByteCoin-iOS13-Completed) where you swipe with a picker view to see the value of 1 Bitcoin in the selected currency.
Right now, I'm successfully retrieving and parsing the data from coinapi.io, but my delegate method to update the text labels isn't activating and I can't figure out why, even comparing it to the solution code. I'm not getting any errors and it runs fine, but this update method just isn't calling. Why not?
ViewController.swift
// CoinManager Delegate Extension Functionality
extension ViewController: CoinManagerDelegate {
func didUpdateCoin(currency: String, value: String) {
// Test to see if it's being called
print("didUpdateCoin Called")
DispatchQueue.main.async {
// Change the information presented to the user
self.currencyLabel.text = currency
self.bitcoinLabel.text = String(value)
}
}
// If it fails, print the error that occurred
func didFailWithError(error: Error) {
print(error)
}
}
This is where I'm calling it.
CoinManager.swift
var delegate: CoinManagerDelegate?
func performRequest(with urlString: String, currency: String) {
// If the url is valid
if let url = URL(string: urlString) {
// Create the URLSession to request the data
let session = URLSession(configuration: .default)
// Create the task and session with the url
let task = session.dataTask(with: url) { (data, response, error) in
// If there's an error
if error != nil {
// Call the error-handling function
self.delegate?.didFailWithError(error: error!)
// Return without any request being performed
return
}
// If the data is retrieved successfully
if let safeData = data {
// Parse the data
if let value = self.parseJSON(safeData) {
print("Got to just before didUpdateCoin")
// The value is being passed correctly
print(value)
// Not calling, just skipping passed didUpdateCoin
self.delegate?.didUpdateCoin(currency: currency, value: value)
print("Passed didUpdateCoin")
}
}
}
// Continue running
task.resume()
}
}
Output
Got to just before didUpdateCoin
9768.79
Passed didUpdateCoin
The problem is - you have not connected your delegate with second view where you want to update content
For make it works follow next steps:
1) Create Protocol
2) Create delegate variable with your protocol type, you can make it optional
3) Chose where you want to call method from your delegate
4) In related VC assign it itself
5) Subscribe to protocol
6) Add protocol stubs
7) Configure your functionality
For better understanding: https://www.youtube.com/watch?v=DBWu6TnhLeY

Polling with RxSwift and Parse-Server

I'm working on an Apple TV app that uses Parse-Server as a backend and RxSwift and I'm trying to set up an authentication system similar to those on the tv streaming apps.
Right now I have an AuthenticationCode object in the parse database that has a code, device id, and session token column. I'm trying to use RxSwift's interval to perform a fetch on the object every 5 seconds, and am checking if the session token column has been filled out.
Here is the code:
func poll(authorizationCode: AuthorizationCode) -> Observable<AuthorizationCode> {
return Observable<Int>.interval(5, scheduler: MainScheduler.instance).flatMap({ _ in
return Observable<AuthorizationCode>.create { observer -> Disposable in
authorizationCode.fetchInBackground(block: { (authorizationCode, error) in
if let authorizationCode = authorizationCode as? AuthorizationCode {
observer.onNext(authorizationCode)
if authorizationCode.sessionToken != nil {
observer.onCompleted()
}
} else if let error = error {
observer.onError(error)
}
})
return Disposables.create()
}
})
}
I'm emitting on onNext event every time I fetch the object, and I want to terminate the sequence when the session code exists.
The problem I'm having with this code is that even after the session token is filled out and the onCompleted is called, the timer still fires and the subscriber never gets the onCompleted event.
Any help with this is appreciated.
Also, if I'm way off on how I should be doing this, let me know.
I would use Parse-Server live queries but they currently don't support tvOS.
Thanks.
UPDATED:
Try this:
func poll(authorizationCode: AuthorizationCode) -> Observable<AuthorizationCode> {
// 1. Return the Observable
return Observable<AuthorizationCode>.create { observer -> Disposable in
// 2. We create the interval here
let interval = Observable<Int>.interval(.seconds(5), scheduler: MainScheduler.instance)
// 3. Interval subscription
let subscription =
interval.subscribe(onNext: { _ in
// 4. Fetch
authorizationCode.fetchInBackground(block: { (authorizationCode, error) in
// 5. onNext, onCompleted, onError
if let authorizationCode = authorizationCode as? AuthorizationCode {
observer.onNext(authorizationCode)
if authorizationCode.sessionToken != nil {
observer.onCompleted()
}
} else if let error = error {
observer.onError(error)
}
})
})
return Disposables.create{
subscription.dispose()
}
}
}

Firebase runTransactionBlock() deletes node

I am using Firebase SDK (2.4.2) for iOS. The following code is part of the didSelectPost() function used in SLComposeServiceViewController object. It's very important to indicate that this behavior is not the same when used in a regular UIViewController (weird I know).
Assuming the following basic code:
override func presentationAnimationDidFinish() {
// Retrieving content and identifying type here
}
override func didSelectPost() {
self.myRootRef.runTransactionBlock({
(currentData:FMutableData!) in
currentData.value = "test"
// Finalizing the transaction
return FTransactionResult.successWithValue(currentData)
}
)
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
}
When the transaction is complete node_value disappears completely from Firebase. The value is not set and the node is deleted. This is a very weird and unexpected behavior!
On the other hand, the following code works as expected.
override func didSelectPost() {
self.myRootRef.setValue("test", withCompletionBlock: {
(error:NSError?, ref:Firebase!) in
if (error != nil) {
print("Data could not be saved.")
} else {
print("Data saved successfully!")
}
})
self.extensionContext!.completeRequestReturningItems([], completionHandler: nil)
}
This problem did not exist a couple of days ago, last I ran the code above. Any insights as to what the problem is?
If you set nil to a path in a Firebase database, it will do the same as a .removeValue().
When running a transaction you should check for nil. Since transactions can be called with nil if no default value was written.
myRootRef.runTransactionBlock({
(currentData:FMutableData!) in
var value = currentData.value as? Int
if value == nil {
value = 0
}
currentData.value = value! + 1
return FTransactionResult.successWithValue(currentData)
})
Read the Firebase Docs for more information.

Resources