I am passing summary message in MSMessage , but when try to get when message received at other end, it returns nil.
Below is code for create Message.
fileprivate func composeMessage(with url: String, andEventInfo eventInfo: NSDictionary?) -> MSMessage {
let message = MSMessage(session:MSSession())
message.url = URL(string: url)
message.layout = createTemplateForEvent(eventInfo: eventInfo!)
message.summaryText = "SAMPLE MESSAGE"
return message
}
To Send Message in Current Conversation
let message = composeMessage(with: url!,andEventInfo: eventInfo)
activeConversation?.insert(message, completionHandler: { (error) in
print(error)
})
Now, At receiving end
Here summaryText returns nil.
override func didReceive(_ message: MSMessage, conversation: MSConversation) {
print("DID RECEIVE MESSAGE: \(message.summaryText)")
}
Also when user tap on message, then also it returns nil
override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
guard let conversation = activeConversation else { fatalError("Expected an active converstation") }
// Present the view controller appropriate for the conversation and presentation style.
if presentationStyle == .expanded {
if conversation.selectedMessage != nil {
print(conversation.selectedMessage?.summaryText)
presentViewController(for: conversation, with: presentationStyle)
}
}
}
Any one have idea, why this happens or any thing is going wrong ?
May this will be helpful
override func didReceive(_ message: MSMessage, conversation: MSConversation) {
// Called when a message arrives that was generated by another instance of this
// extension on a remote device.
// Use this method to trigger UI updates in response to the message.
guard let messageURL = message.url else { return }
guard let urlComponents = NSURLComponents(url: messageURL, resolvingAgainstBaseURL: false), let queryItems = urlComponents.queryItems else { return }
print("URL Components", urlComponents)
print("queryItems", queryItems)
for item in queryItems {
print("Received \(item.name) with value \(item.value)")
}
}
Reference & helped Source: https://www.hackingwithswift.com/ios10
And Also Refer : iOS10 iMessage : Unable to insert data into iMessage using MSConversation
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 trying and failing badly to implement the cool Firebase email link login feature. I successfully setup sending an email link. However, I can't get the email link to open up the app. It just opens up the preview page like it can't open the app.
I've tested the dynamic link I setup and I can get it to open up the app in a device. I just can't get the email link to do the same.
Code in my app:
func sendFirebaseEmailLink() {
let actionCodeSettings = ActionCodeSettings.init()
// userEmail comes from a textField
let email = userEmail
actionCodeSettings.url = URL.init(string: String(format: "https://<myappname>.firebaseapp.com/?email=%#", email))
// The sign-in operation has to always be completed in the app.
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
Auth.auth().sendSignInLink(toEmail: email,
actionCodeSettings: actionCodeSettings) { error in
if let error = error {
print(error.localizedDescription)
return
}
else {
UserDefaults.standard.set(email, forKey: "Email")
print("email sent to user")
}
}
}
When I say I've successfully gotten my dynamic link to open the app what I mean is when I follow the link I created (mylinkname.page.link/emaillogin) on a device that has the app installed, it opens the app. Because of that and [this helpful Firebase video][1] on setting up a dynamic link it seems like I've got those details correct and the issue is with the code, but I'm new to this so I'm not sure.
I've spend few days going around in circles to figure this out, and trying to parse the dense Firebase documentation, so any ideas are greatly appreciated.
I finally figured it out. The code was fine. It was an issue related to the dynamic link. I had a couple links setup in Firebase because I had to create a new Bundle ID at one point. When I deleted out the old one in Firebase the email link started working.
It shows up in my app association site like this, and oddly still does even though I deleted out the old link, but at least it works now!
{"applinks":{"apps":[],"details":[{"appID":"TEAMID.com.OLDBUNDLEIDENTIFIER.APPNAME","paths":["NOT //*","/*"]},{"appID":"TEAMID.com.NEWBUNDLEIDENTIFIER.APPNAME","paths":["NOT //","/"]}]}}
UPDATE: My full code to implement passwordless email login is below. It was painful for me to piece together using the documentation so hopefully this saves you the trouble.
Key steps assuming you understand the basics of Firebase Setup.
1) Setup a Dynamic Link Using the Firebase Video tutorial.
2) Code in View Controller:
var userEmail: String?
var link: String?
func sendFirebaseEmailLink() {
let actionCodeSettings = ActionCodeSettings.init()
let email = userEmail
actionCodeSettings.url = URL.init(string: String(format: "https://<myappname>.page.link/emaillogin/?email=%#", email!))
// The sign-in operation has to always be completed in the app.
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
Auth.auth().sendSignInLink(toEmail: email!,
actionCodeSettings: actionCodeSettings) { error in
if let error = error {
print(error.localizedDescription)
return
}
else {
UserDefaults.standard.set(email, forKey: "Email")
print("email sent to user")
}
// TODO: Notify user to check email and click the link.
}
}
// Sign in user after they clicked email link called from AppDelegate
#objc func signInUserAfterEmailLinkClick() {
// Get link url string from the dynamic link captured in AppDelegate.
if let link = UserDefaults.standard.value(forKey: "Link") as? String {
self.link = link
}
// Sign user in with the link and email.
Auth.auth().signIn(withEmail: userEmail!, link: link!) { (result, error) in
if error == nil && result != nil {
if (Auth.auth().currentUser?.isEmailVerified)! {
print("User verified with passwordless email")
// TODO: Do something after user verified like present a new View Controller
}
else {
print("User NOT verified by passwordless email")
}
}
else {
print("Error with passwordless email verfification: \(error?.localizedDescription ?? "Strangely, no error avaialble.")")
}
}
}
3) Code in AppDelegate
// For Passwordless Email Login to Handle Dynamic Link after User Clicks Email Link
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if let incomingURL = userActivity.webpageURL {
print("Incoming URL is \(incomingURL)")
// Parse incoming
let linkHandled = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else {
print("Found an error: \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
self.handleIncomingDynamicLink(dynamicLink)
}
}
if linkHandled {
return true
}
else {
// Maybe do other things with dynamic links in future?
return false
}
}
return false
}
// Handles the link and saves it to userDefaults to assist with login.
func handleIncomingDynamicLink(_ dynamicLink: DynamicLink) {
guard let url = dynamicLink.url else {
print("My dynamic link object has no url")
return
}
print("Incoming link parameter is \(url.absoluteString)")
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
// Save link to userDefaults to help finalize login.
UserDefaults.standard.set(link, forKey: "Link")
// Send notification to ViewController to push the First Time Login VC
NotificationCenter.default.post(
name: Notification.Name("SuccessfulPasswordlessEmailNotification"), object: nil, userInfo: nil)
}
}
For anyone using SwiftUI with AppDelegate and SceneDelegate files instead of UIKit, here's what I've done:
Create a function to send a link to the user's email
func sendSignLink(email: String) async throws {
do {
let actionCodeSettings = ActionCodeSettings()
actionCodeSettings.url = URL(string: "*enter your Firebase Dynamic link here*")
actionCodeSettings.handleCodeInApp = true
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
try await Auth.auth().sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings)
UserDefaults.standard.set(email, forKey: "email")
}
catch {
throw error
}
}
In the SceneDelegate file, import FirebaseDynamicLinks and add the below code
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if let incomingURL = userActivity.webpageURL {
print("\n \nIncoming URL is \(incomingURL)")
_ = DynamicLinks.dynamicLinks().handleUniversalLink(incomingURL) { (dynamicLink, error) in
guard error == nil else {
print("\n \nError with handling incoming URL: \(error!.localizedDescription)")
return
}
if let dynamicLink = dynamicLink {
guard let url = dynamicLink.url else {
print("\n \nDynamic link object has no url")
return
}
print("\n \nIncoming link parameter is \(url.absoluteString)")
let link = url.absoluteString
if Auth.auth().isSignIn(withEmailLink: link) {
// Send notification to trigger the rest of the sign in sequence
NotificationCenter.default.post(name: Notification.Name("Success"), object: nil, userInfo: ["link": link])
} else {
// Send error notification
NotificationCenter.default.post(name: Notification.Name("Error"), object: nil, userInfo: nil)
}
}
}
}
}
Create a function to handle the sign in after the user has clicked on the link in their email
func signInWithEmail(link: String) async throws {
do {
let email = UserDefaults.standard.value(forKey: "email")
try await Auth.auth().signIn(withEmail: email, link: link)
}
catch {
throw error
}
}
In a relevant view, handle the notifications which get posted
struct MyView: View {
var body: some View {
VStack {
Text("View")
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("Success"))) { notificationInfo in
if let userInfo = notificationInfo.userInfo {
if let link = userInfo["link"] as? String {
Task.init {
do {
try await signInWithEmail(link: link)
} catch {
print(error)
}
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("Error"))) { _ in
//do something with error
}
}
}
I have a Settingsviewcontroller.swift file that is connected to the storyboard and a separate file called connectionapi.swift.
Now, when a user clicks the checkbutton, the given username and password are checked against the API in the connectionapi class. Now when for instant the result generates an error I want to inform the user with an alert message. I am trying to find out for a few days how to do this, but I can't seem to find it. I keep ending up in errors.
Can someone please help me with some code examples:
the viewcontroler file
//
// ViewControllerSettings.swift
//
import Foundation
import UIKit
class ViewControllerSettings: UIViewController {
#IBOutlet weak var tGebruikersnaam: UITextField!
#IBOutlet weak var tCode: UITextField!
#IBOutlet weak var AnimatedImage: UIImageView!
#IBOutlet weak var lCopyright: UILabel!
#IBOutlet weak var lStatus: UILabel!
// declare a iCloud Store to save and load data from
var iCloudStore:NSUbiquitousKeyValueStore!
var userName:String = ""
var password:String = ""
let API = myAPI()
#IBAction func didTapOpslaan(_ sender: Any) {
if tGebruikersnaam.text == "" || tGebruikersnaam.text == nil || tCode.text == "" || tCode.text == nil {
return
}
print("Lets check username and password against the api")
let newUsername = "\(tGebruikersnaam.text!)"
let newCode = "\(tCode.text!)"
API.CheckUsernamePassword(username: newUsername, code: newCode) {
isValid in
print(isValid)
if isValid == true {
DispatchQueue.main.async {
print("The credentials are correct")
self.ShowAnimationOk()
}
}else {
DispatchQueue.main.async {
print("the credentials are wrong")
self.ShowAnimationNo()
}
}
}
}
// Code removed
}
The API Swift file
//
// ConnectAPI.swift
import Foundation
import UIKit
class myAPI{
let api_key = "b88a734f186sad"
let baseurl = "https://xxxx:443/xxxx/api"
func CheckUsernamePassword(username :String ,code:String, completion: #escaping (Bool)->() ) {
let urlString = "\(self.baseurl)/accounts/validateusernamepassword.json?username=\(username)&password=\(code)&api_key=\(self.api_key)"
let url = URL(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
if error != nil {
print("API | Error URLSession : \(error!)")
completion(false)
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!, options: []) as! [String:Any]
print("----------")
if parsedData["validated"] != nil {
if "\(parsedData["validated"]!)" == "1" {
print("API JSON | validated = \(parsedData["validated"]!)")
print("API JSON | message = \(parsedData["message"]!)")
completion(true)
}else {
print("Credential Check not valid")
print("API JSON | validated = \(parsedData["validated"]!)")
print("API JSON | message = \(parsedData["message"]!)")
completion(false)
}
}else{
print("Json Parse error: \(parsedData)")
// Raise a Alert here
}
} catch let error as NSError {
print("API | Error Parsing JSON \(error)" )
// Raise a Alert here
// main.showAlert(message: "API | Error Parsing JSON \(error)")
//A error occured when checking credentials, try again later.
completion(false)
}
}
}.resume()
}
I'm not sure what's your problem is. I assume that you have two problems:
you don't know how to pass error message from your API class to
the ViewControllerSettings class.
you don't know how to show alert in iOS before
If that's the case, let's look at this.
1. Passing error message
You can see that the completion block (e.g., #escaping (Bool) -> ()) will be called after the function checkUsernamePassword done with the URLSession's dataTask:
func CheckUsernamePassword(username :String ,code:String, completion: #escaping (Bool)->() ) { ... }
If success, you call the block with value true
completion(true)
in other cases you call with value false
completion(false)
To pass more informations (e.g. error message), you can simply add input parameter at the completion block. Better added names for each one so it's more clear. E.g., change it to:
..., completion: #escaping (valid: Bool, errorMessage: String?) -> ())
Then, if the API succeeds, you call it without errorMessage:
completion(valid: true, errorMessage: nil)
And if it's error, you pass either your own error message or get it from NSError
completion(valid: false, errorMessage: "Credential check not valid.")
2. Show the error in UIAlertController
First make a utility function that shows standard alert in your ViewControllerSettings:
// Simple Alert UI
func showAlert(title: String, message: String) {
let actionSheetController: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
// add close button
let cancelAction: UIAlertAction = UIAlertAction(title: "Close", style: .cancel) { _ in }
actionSheetController.addAction(cancelAction)
// show on self
self.present(actionSheetController, animated: true, completion: nil)
}
If you did the first step, you have accessed to your error message at call site (ViewController).
When you call the API, errorMessage is available, just show it with alert:
// didTap...
API.CheckUsernamePassword(username: newUsername, code: newCode) {
(isValid, errorMessage) in
if isValid {
...
} else {
let error = errorMessage! // I don't recommend forced-casting though
self.showAlert(title: "Error!", message: error) // show Alert UI
}
}
I am making a "texting app" you can call it and it uses cloudkit and I have been looking everywhere to add notifications that work with cloudkit... Would someone be able to tell me the code to add push notifications for cloudkit in detail because I am very lost... Also I wan't the notifications to go to different "texting rooms" (in cloudkit it would be record types...) For instance I have one record type called "text" and another one called "text 2" I don't want notifications from "text" to get to people who use "text2" and vise versa.
Using Swift 2.0 with El Captain & Xcode 7.2.1
Elia, You need to add this to your app delegate. Which will arrive in a userInfo packet of data, which you can then parse to see which database/app sent it.
UIApplicationDelegate to the class
application.registerForRemoteNotifications() to the
func application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
Than this method
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
let notification = CKQueryNotification(fromRemoteNotificationDictionary: userInfo as! [String : NSObject])
let container = CKContainer(identifier: "iCloud.com")
let publicDB = container.publicCloudDatabase
if notification.notificationType == .Query {
let queryNotification = notification as! CKQueryNotification
if queryNotification.queryNotificationReason == .RecordUpdated {
print("queryNotification.recordID \(queryNotification.recordID)")
// Your notification
}
}
print("userInfo \(userInfo["ck"])")
NSNotificationCenter.defaultCenter().postNotificationName("NotificationIdentifier", object: self, userInfo:dataDict)
}
}
}
}
}
That'll get you started.
You can use this method to check your subscriptions programmatically, of course while your developing you can use the dashboard.
func fetchSubsInPlace() {
let container = CKContainer(identifier: "iCloud.com")
let publicDB = container.publicCloudDatabase
publicDB.fetchAllSubscriptionsWithCompletionHandler({subscriptions, error in
for subscriptionObject in subscriptions! {
let subscription: CKSubscription = subscriptionObject as CKSubscription
print("subscription \(subscription)")
}
})
}
And finally when you got it; you can this routine to ensure you capture any subscriptions you missed while your app was sleeping and make sure that subscriptions don't go to all your devices, once you treated them too.
func fetchNotificationChanges() {
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: nil)
var notificationIDsToMarkRead = [CKNotificationID]()
operation.notificationChangedBlock = { (notification: CKNotification) -> Void in
// Process each notification received
if notification.notificationType == .Query {
let queryNotification = notification as! CKQueryNotification
let reason = queryNotification.queryNotificationReason
let recordID = queryNotification.recordID
print("reason \(reason)")
print("recordID \(recordID)")
// Do your process here depending on the reason of the change
// Add the notification id to the array of processed notifications to mark them as read
notificationIDsToMarkRead.append(queryNotification.notificationID!)
}
}
operation.fetchNotificationChangesCompletionBlock = { (serverChangeToken: CKServerChangeToken?, operationError: NSError?) -> Void in
guard operationError == nil else {
// Handle the error here
return
}
// Mark the notifications as read to avoid processing them again
let markOperation = CKMarkNotificationsReadOperation(notificationIDsToMarkRead: notificationIDsToMarkRead)
markOperation.markNotificationsReadCompletionBlock = { (notificationIDsMarkedRead: [CKNotificationID]?, operationError: NSError?) -> Void in
guard operationError == nil else {
// Handle the error here
return
}
}
let operationQueue = NSOperationQueue()
operationQueue.addOperation(markOperation)
}
let operationQueue = NSOperationQueue()
operationQueue.addOperation(operation)
}
}
I've tried everything, but the only way I could get a successful test is to actually send the notification in the test function, which kinda defeats the purpose.
I have a button. When I tap the button, it sends a notification. How can I use expectationForNotification to see if this notification gets sent?
func testExample() {
let app = XCUIApplication()
let button = app.buttons["Button"]
let expectation = expectationForNotification("TEST_NOTE", object: nil) {
(notification: NSNotification!) -> Bool in
print("SUCCESS")
return true
}
button.tap()
waitForExpectationsWithTimeout(5, handler: nil)
}
it looks to me that you will have to fulfill the expectation...
func testExample()
{
let app = XCUIApplication()
let button = app.buttons["Button"]
let expectation = expectationWithDescription("waiting for the tap")
expectationForNotification("TEST_NOTE", object: nil)
{
notification in
expectation.fulfill()
return true
}
button.tap()
waitForExpectationsWithTimeout(30)
{
error in
if let e = error
{
XCTFail("\(e.debugDescription)")
}
}
}