来自iOS中的MQTT的通知

如果您有一个应用程序正在监视一个来自MQTT代理的实时数据流,如我在先前的文章中所述,则您可能希望基于该实时数据流发送用户通知。

这可以在多种情况下工作,但就我而言,我有一个传感器,如果传感器检测到某个读数,则想警告用户。

为此,我遇到了两种可能性:

一种。 设置APNs(Apple推送通知服务)服务器,该服务器监视MQTT数据并在必要时向每个设备发送推送通知

要么

b。 直接从客户端设备读取MQTT数据,并在必要时发送本地通知

在我的情况下, 选项B更具吸引力,因为它消除了中间人 (APN),这意味着您不必设置服务器即可执行该任务。 选项B不仅更容易,而且还节省了大量的时间和运行APN的设备资源。

但是,您应该注意,有些付费服务可以为您处理APN。 如果您的项目规模较大且预算较大,则可能需要进一步研究该选项。

为了建立这个MQTT后台通知系统,我们需要弄清楚一些事情:

  1. 设置背景提取
  2. 在后台获取过程中获取mqtt数据
  3. 必要时发送通知

它是什么

在iOS中,后台抓取是指将应用从后台唤醒以执行简短的任务,这通常涉及检查服务器上是否有要下载的新信息。

这很有用,这样用户回去打开应用程序时就可以为他们准备好新信息,而不必等待其加载。

如果您愿意,可以在这里阅读更多有关后台获取和其他后台模式的信息。

如何设定

为了在您的应用程序中实现后台提取,您必须将您的应用程序标记为具有该功能。 为此,请选择应用程序目标 ,然后在顶部选择“ Capabilities ”。 从那里打开背景模式开关,并选中背景提取框,如下所示:

现在,您必须告诉应用程序您要多久运行一次后台获取。 为此,将以下内容添加到application(didFinishLaunchingWithOptions:)方法中:

  //尽可能频繁地运行后台抓取 
UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)

开发人员可以根据具体情况决定此频率,但就我而言,我绝对希望尽可能频繁地获取数据。

现在,您可以在AppDelegate中添加一个方法,该方法将在每次后台获取时调用。 它看起来应该像这样:

  func应用程序(_应用程序:UIApplication,performFetchWithCompletionHandler completeHandler:@转义(UIBackgroundFetchResult)->无效){completionHandler(UIBackgroundFetchResult.newData) 
}

当然,我们稍后会添加到该方法,但是正如您已经看到的那样,该方法必须在完成时调用完成处理程序 。 完成处理程序报告是否找到了新数据,系统将使用它来确定运行应用程序的后台获取的最佳时间以及应多久运行一次。

因此,至关重要的是准确地报告该信息,而不是像上述方法那样盲目地说有新数据。 该操作只是作为占位符完成的,稍后我们将修复该问题。

现在,我们要在后台获取期间接收我们的MQTT数据。 设置MQTT可以是其自己的一个微型项目,因此,如果尚未设置MQTT,则应阅读此文章。

当然,在这种情况下,我们将需要做一些更改以使MQTT有用。 我们将需要从后台模式创建一个连接,并使AppDelegate成为MQTT连接的委托。 我们可以使用与上述文章到AppDelegate相同的方法来建立连接:

  func setUpMQTT(){ 
让clientID =“ CocoaMQTT-” +字符串(ProcessInfo()。processIdentifier)
mqtt = CocoaMQTT(clientID:clientID,主机:“ localhost”,端口:1883)
mqtt.delegate =自我
mqtt.username =“测试”
mqtt.password =“公共”
mqtt.willMessage = CocoaMQTTWill(topic:“ / will”,消息:“ dieout”)
mqtt.keepAlive = 60
mqtt.connect()
}

现在,要使AppDelegate符合MQTTDelegate协议,您需要添加一个扩展,也如链接文章中所示,仅这次是使用AppDelegate而不是ViewController。

 扩展名AppDelegate:CocoaMQTTDelegate { 
//这两个方法是我们目前所关心的
func mqtt(_ mqtt:CocoaMQTT,didConnect主机:字符串,端口:整数){
}

func mqtt(_ mqtt:CocoaMQTT,didReceiveMessage消息:CocoaMQTTMessage,id:UInt16){
}

// CocoaMQTTDelegate的其他必需方法
func mqtt(_ mqtt:CocoaMQTT,didReceive信任:SecTrust,completeHandler:
@转义(Bool)->无效){
completeHandler(true)
}

func mqtt(_ mqtt:CocoaMQTT,didConnectAck ack:CocoaMQTTConnAck){
}

func mqtt(_ mqtt:CocoaMQTT,didPublishMessage消息:CocoaMQTTMessage,id:UInt16){
}

func mqtt(_ mqtt:CocoaMQTT,didPublishAck id:UInt16){
}

func mqtt(_ mqtt:CocoaMQTT,didSubscribeTopic主题:字符串){
}

func mqtt(_ mqtt:CocoaMQTT,didUnsubscribeTopic主题:字符串){
}

func mqttDidPing(_ mqtt:CocoaMQTT){
}

func mqttDidReceivePong(_ mqtt:CocoaMQTT){
}

func mqttDidDisconnect(_ mqtt:CocoaMQTT,withError err:Error?){
}

func _console(_ info:String){
}
}

权限

现在,我们需要设置难题的第三部分,即通知,然后才能将所有内容连接起来并使它们协同工作。

在这种情况下,我们使用的通知称为本地通知,因为它们是从设备本地发送的,而不是从APNs服务器发送的通知。

要向用户发送本地通知,我们将不得不向用户请求权限,就像您在过去首次打开应用程序时可能会看到的那样。 这可以通过将以下代码添加到application(didFinishLaunchingWithOptions:)方法中来完成:

  //提示是否允许发送通知 
让设置= UIUserNotificationSettings(类型:UIUserNotificationType.alert,类别:无)
UIApplication.shared.registerUserNotificationSettings(设置)

发送通知

现在,我们可以在AppDelegate中创建一个可用于发送用户通知的方法。 对于我们的示例,我们将有一个字符串参数,该参数将用于在通知中向用户显示MQTT消息。 此方法应如下所示:

  ///向用户发送带有给定消息的通知 
///
///-参数msg:在通知中以字符串形式显示的消息
func sendNotification(msg:String){
让localNotification:UILocalNotification = UILocalNotification()
localNotification.alertBody =“ MQTT消息:” +消息
localNotification.fireDate = NSDate(timeIntervalSinceNow:1)作为日期
UIApplication.shared.scheduleLocalNotification(localNotification)
}

现在,当我们稍后接收到MQTT消息时,可以调用该方法。

现在,我们分别设置了后台获取,MQTT和通知,但是我们仍然必须将它们连接在一起

为此,我们将必须修改我们之前制作的application(_:performFetchWithCompletionHandler:)方法。 在这种方法中,我们将必须做两件事:

  1. 确保建立了MQTT连接。
  2. 等待给定的时间以使MQTT消息进入

MQTT与传统的数据获取不同,因为我们无法发送请求以询问我们的MQTT设备的状态(除非我们为此设置了中间服务器)。 相反,我们能做的最好的事情就是简单地建立连接并坐下来等待消息。

 打印(“正在获取”) 
backgroundMessageReceived = false
RKMQTTConnectionManager.createConnectionIfNecessary()
// TODO:进一步检查等待时间是4还是5秒
DispatchQueue.main.asyncAfter(最后期限:.now()+ .seconds(4),执行:{
打印(“完成”)
如果self.backgroundMessageReceived {
打印(“已完成并已接收数据”)
completeHandler(UIBackgroundFetchResult.newData)
}其他{
打印(“无数据”)
completeHandler(UIBackgroundFetchResult.noData)
}
})

您可能注意到了我对RKMQTTConnectionManager.createConnectionIfNecessary()调用, RKMQTTConnectionManager.createConnectionIfNecessary()我将介绍该方法。 但是,首先,我们应该了解所有这些设置:在后台获取期间收到消息时该怎么做。 为此,我们需要返回extension AppDelegate: CocoaMQTTDelegate {我们之前创建并修改了mqtt(_: didConnect:)方法,因为该方法是在收到MQTT消息时调用的方法(只要我们进入后台模式时,请更新MQTT委托(稍后将做)。

该方法如下所示:

 // Only send one notification even if multiple messages are received 
print(“Message reveived”)
if shouldSendNotification(message.string) && !wasNotificationSent {
sendNotification(msg: message.string)
}
}
backgroundMessageReceived = true

shouldSendNotification方法将取决于具体情况,但是通常它将采用MQTT消息的字符串并返回一个布尔值,该布尔值表示是否根据该消息的内容发送通知。 无论消息是什么,甚至可能总是要发送通知,在这种情况下,不需要布尔检查。

现在我们已经设置了消息接收处理程序,剩下的只有一件事: RKMQTTConnectionManager

我在后台获取方法的开头调用了RKMQTTConnectionManager.createConnectionIfNecessary() ,但我并没有真正解释为什么。 其背后的原因是,我想将MQTT连接保持在一个位置,而不是在AppDelegate中创建一个新连接以仅在后台获取期间使用。

相反,如果我们仅创建一个具有静态MQTT连接的MQTT连接管理器,以及用于建立连接,跟踪连接状态和更改MQTT委托的方法(这将很快完成),则更为简单。

该MQTT连接管理器可以如下所示:

 进口基金会 
进口可可
///用于处理mqtt连接的类,已创建,以便可以在背景和前景之间传递连接
类RKMQTTConnectionManager {
私人静态var mqtt连接:CocoaMQTT?
私有静态var currentSensorTopic:字符串!
公共静态var isSetup = false

///设置初始mqtt连接
公共静态函数setup(){
//使用默认IP进行初始连接,以便在等待MongoDB数据时可以显示实时数据
currentSensorTopic =“传感器1 / ppmv”
connectWith(主机:Secrets.MQTT_ADDRESS,端口:Secrets.MQTT_PORT)
isSetup = true
}

///创建与指定的mqtt代理的连接
///
///-参数:
///-主机:经纪人地址的字符串
///-端口:代理的端口号的uint16
公共静态函数connectWith(host:String,port:UInt16){
让clientID =“ CocoaMQTT-” +字符串(ProcessInfo()。processIdentifier)

mqttConnection = CocoaMQTT(客户端ID:客户端ID,主机:主机,端口:端口)
mqttConnection!.username =“ MY MQTT USERNAME”
mqttConnection!.password =“我的MQTT密码”
mqttConnection!.willMessage = CocoaMQTTWill(topic:“ / will”,消息:“ dieout”)
mqttConnection!.keepAlive = 60
mqttConnection!.autoReconnect = true
mqttConnection!.autoReconnectTimeInterval = 1
mqttConnection!.connect()
}


///设置mqtt连接的委托以接收来自代理的新消息
///
///-参数委托:接收消息的新委托
公共静态函数setDelegate(delegate:CocoaMQTTDelegate){
RKMQTTConnectionManager.mqttConnection?.delegate =委托
}

///如果需要取消订阅先前订阅的主题,请订阅给定的主题
///
///-参数topic:要订阅的新主题名称
公共静态功能订阅(主题:字符串){
如果让previousTopic = currentSensorTopic {
mqttConnection?.unsubscribe(previousTopic)
}
mqttConnection?.subscribe(topic)
currentSensorTopic =主题
}

///如果尚未创建基本连接,则建立一个基本连接
公共静态函数createConnectionIfNecessary(){
如果mqttConnection == nil {
connectWith(主机:“ 127.0.0.1”,端口:1883)
}
}
}

此MQTTConnection管理器将负责MQTT连接,并允许我们从应用程序内部和后台模式访问它。

要在应用程序本身的前景中使用此连接管理器,而忽略所有后台获取,我们将其设置如下:

  RKMQTTConnectionManager.setup() 
RKMQTTConnectionManager.setDelegate(代表:自我)

因此,该代码现在可以替换我们之前编写的setUpMqtt()方法。 当然,您将需要订阅适当的主题,这在mqtt(_: didConnect:)方法中最有意义,因为您只能在建立连接后进行订阅。

现在,在后台模式下接收消息所需要做的就是将MQTT委托设置为AppDelegate本身。 为此,我们需要在AppDelegateapplicationDidEnterBackground(_:)方法中执行以下操作。

  func applicationDidEnterBackground(_ application:UIApplication){ 
RKMQTTConnectionManager.setDelegate(代表:自我)
}

每当应用程序进入后台模式时,这会将MQTT委托设置为AppDelegate。 这样,当在后台获取期间收到MQTT消息时,将调用适当的处理程序。

然后,当用户打开应用程序时,MQTT委托将自动更改回MainViewController,因为在新的setUpMQTT()方法中调用了setUpMQTT()

至此,我们已经涵盖了我自己实现MQTT消息通知时出现的所有问题。 由于项目的性质,您可能会遇到更多问题,或者您遇到的问题甚至更少。

例如,由于我项目的两个方面,在我的情况下,RKMQTTConnectionManager是必需的:

  1. 我正在使用MQTT连接进行安全身份验证,这需要一些时间。 因此,对我而言,保持相同的连接比浪费宝贵的后台获取时间重新进行身份验证更有效。
  2. 我正在应用程序内和后台提取中使用MQTT数据。 因此,再次重申,对我来说,保持这种联系而不是创建一个新的联系是很有意义的。

如果我只想在后台使用MQTT数据,而不必担心昂贵的身份验证,那么mqtt +后台获取过程可能会更简单。 但是,我希望通过展示更复杂的案例,使您如何处理较简单的案例更加明显。

无论如何, 感谢您的阅读 并祝您项目顺利

RK