Subscribe to MQTT topic in SwiftUI iOS application - ios

I'm writing a small iOS app to send MQTT messages to a broker which is hosted on a Raspberry Pi companion computer on my quadcopter. In the iOS app primary view, the app will connect to MQTT Broker (check), send messages associated with several buttons (check), and monitor various quadcopter telemetries such as mode, distance, and lat/long. The iPhone app will display the drones position and the user's position (check) on a map.
The part I'm having issues with currently is having the app maintain a continuous subscription to a topic and then update several variables in the code. Currently, the code below contains the subscription line and "did receive message" code inside of of a button view which only works momentarily when that button is pressed.
I've tried pasting this code in various places within the content view with no success; admittedly I'm a novice coder and Swift/iOS is very new to me. Ideally, the app would continually monitor certain topics and update several variables to equal the message when certain topics are posted to by the drone.
import MapKit
import SwiftUI
import CocoaMQTT
struct Drone {
var coordinate: CLLocationCoordinate2D
var distance: UInt8
var direction: UInt8
}
//
struct ContentView: View {
#State public var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 34.0000, longitude: -86.0000), span: MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001))
#State private var trackingMode = MapUserTrackingMode.follow
#State private var safety = false
#State private var showingSheet = false
#State public var Connection:Bool = false
var Bee: Drone = Drone(coordinate: .init(latitude: 34.0010, longitude: -86.0010), distance: 10, direction: 180)
let mqttClient = CocoaMQTT(clientID: "swift", host: "mqtt.eclipse.org", port: 1883)
var body: some View {
VStack {
HStack{
Text("Beer Bee")
Spacer()
Button(action: {
self.mqttClient.username="user"
self.mqttClient.password="password"
self.mqttClient.keepAlive=60
self.mqttClient.connect()
self.Connection.toggle()
}, label: {
Text(Connection ? "Disconnect":"Connect")
})
}
Map(coordinateRegion: $region, interactionModes: .zoom, showsUserLocation: true, userTrackingMode: $trackingMode)
HStack {
Text("Flight Mode")
Spacer()
Text("Distance")
Spacer()
Text("Position Accuracy")
}
HStack(alignment: .center) {
Toggle(isOn: $safety) {
//action here
}
.toggleStyle(SwitchToggleStyle(tint: Color.red))
Button("Main") {
self.mqttClient.publish("topic/dronetest", withString: "hello world!")
}
Spacer()
}
Button("Hold") {
self.mqttClient.subscribe("topic/dronetest")
self.mqttClient.didReceiveMessage = { mqtt, message, id in
print("Message received in topic \(message.topic) with payload \(message.string!)")
}
}
Spacer()
Button("Manual Control") {
self.showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SecondView()//region: self.region, Connection: self.Connection)
}
Spacer()
}
.background(/*#START_MENU_TOKEN#*//*#PLACEHOLDER=View#*/Color.yellow/*#END_MENU_TOKEN#*/)
}
}

this is my first answer in StackOverflow and I'm a novice too. I was having the same problem, but I can solve it by declaring the "didConnectAck" for the mqttClient; the subscription remains. Just add the following missing lines to your code:
self.mqttClient.didConnectAck = { mqtt, ack in
self.mqttClient.subscribe("topic/dronetest")
self.mqttClient.didReceiveMessage = { mqtt, message, id in
print("Message received in topic \(message.topic) with payload \(message.string!)")
}
}
Also, (maybe?) can be useful to add the following line in your first button:
self.mqttClient.autoReconnect = true
I hope I can help you with this answer. Good luck.

Related

UIScreen.capturedDidChangeNotification not getting called in SwiftUI

I'm trying to detect when a user enters/exits airplay mode in SwiftUI.
#State var isAirplaying = false
...
var body: some View {
ZStack {
SomeVideoView()
if isAirplaying {
Text("Connected to airplay")
}
}
.edgesIgnoringSafeArea(.all)
.onReceive(NotificationCenter.default.publisher(for: UIScreen.capturedDidChangeNotification)) { _ in
isAirplaying.toggle()
}
}
However, when entering/exiting Airplay, this capturedDidChangeNotification never gets called. What am I missing? Is there a better way to do this?
(I tried modeDidChangeNotification as well.)

SwiftUI Alert does not dismiss when timer is running

Semi related question: SwiftUI ActionSheet does not dismiss when timer is running
I am currently experiencing an issue with alerts in a project that I am working on. Presented alerts will not dismiss when there is a timer running in the background. Most of the time it requires several clicks of the dismissal button to disappear. I have recreated this issue with as little overhead as possible in a sample project.
My primary project has this issue when trying to display an alert on a different view but I could not reproduce that issue in the sample project. The issue can be reliably replicated by toggling the alert on the same view that the timer is running. I have also tested by removing the binding from the text field to stop the text field view from updating. The alert still fails to dismiss on the first click. I am unsure if there is a way to work around this and am looking for any advice possible.
Xcode 13.0/iOS 15.0 and occurs in iOS 14.0 also
Timerview.swift
struct TimerView: View {
#ObservedObject var stopwatch = Stopwatch()
#State var isAlertPresented:Bool = false
var body: some View {
VStack{
Text(String(format: "%.1f", stopwatch.secondsElapsed))
.font(.system(size: 70.0))
.minimumScaleFactor(0.1)
.lineLimit(1)
Button(action:{
stopwatch.actionStartStop()
}){
Text("Toggle Timer")
}
Button(action:{
isAlertPresented.toggle()
}){
Text("Toggle Alert")
}
}
.alert(isPresented: $isAlertPresented){
Alert(title:Text("Error"),message:Text("I am presented"))
}
}
}
Stopwatch.swift
class Stopwatch: ObservableObject{
#Published var secondsElapsed: TimeInterval = 0.0
#Published var mode: stopWatchMode = .stopped
func actionStartStop(){
if mode == .stopped{
start()
}else{
stop()
}
}
var timer = Timer()
func start() {
secondsElapsed = 0.0
mode = .running
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
self.secondsElapsed += 0.1
}
}
func stop() {
timer.invalidate()
mode = .stopped
}
enum stopWatchMode {
case running
case stopped
}
}
Edit:
Moving the button to a custom view solves the initial problem but is there a solution for when the button needs to interact with the Observable object?
Button(action:{
do{
try stopwatch.actionDoThis()
}catch{
isAlertPresented = true
}
}){
Text("Toggle Alert")
}.alert(isPresented: $isAlertPresented){
Alert(title:Text("Error"),message:Text("I am presented"))
Every time timer runs UI will recreate, since "secondsElapsed" is an observable object. SwiftUI will automatically monitor for changes in "secondsElapsed", and re-invoke the body property of your view.
In order to avoid this we need to separate the button and Alert to another view like below.
struct TimerView: View {
#ObservedObject var stopwatch = Stopwatch()
#State var isAlertPresented:Bool = false
var body: some View {
VStack{
Text(String(format: "%.1f", stopwatch.secondsElapsed))
.font(.system(size: 70.0))
.minimumScaleFactor(0.1)
.lineLimit(1)
Button(action:{
stopwatch.actionStartStop()
}){
Text("Toggle Timer")
}
CustomAlertView(isAlertPresented: $isAlertPresented)
}
}
}
struct CustomAlertView: View {
#Binding var isAlertPresented: Bool
var body: some View {
Button(action:{
isAlertPresented.toggle()
}){
Text("Toggle Alert")
}.alert(isPresented: $isAlertPresented){
Alert(title:Text("Error"),message:Text("I am presented"))
}
}
}
If you really need the ObservedObject or any attribute of it in order to perform any action in case of "OK" action of the alert, you can do a workaround.
struct TimerView: View {
#ObservedObject var stopwatch = Stopwatch()
#State var isResetAccepted: Bool = false
var body: some View {
VStack{
Text(String(format: "%.1f", stopwatch.secondsElapsed))
.font(.system(size: 70.0))
.minimumScaleFactor(0.1)
.lineLimit(1)
Button(action:{
stopwatch.actionStartStop()
}){
Text("Toggle Timer")
}
CustomAlertView(isResetAccepted: $isResetAccepted)
.onChange(of: isResetAccepted) { newValue in
if newValue {
isResetAccepetd = false
stopwatch.reset()
}
}
}
}
}
struct CustomAlertView: View {
#Binding var isResetAccepted: Bool
#State var isAlertPresented: Bool = false
var body: some View {
Button(action:{
isAlertPresented.toggle()
}){
Text("Toggle Alert")
}.alert(isPresented: $isAlertPresented){
Alert(title:Text("Error"),
message:Text("I am presented"),
primaryButton: .destructive(Text("Cancel"), action: {
self.isResetAccepted = false
self.isAlertPresented = false
}),
secondaryButton: .default(Text("OK"), action: {
self.isResetAccepted = true
self.isAlertPresented = false
}))
}
}
}

Swift UI overwhelmed by high-frequency #StateObject updates?

Scenario
A simple SwiftUI App that consists of a TabView with two tabs. The App struct has a #StateObject property, which is being repeatedly and very quickly (30 times per second) updated by simulateFastStateUpdate.
In this example, simulateFastStateUpdate is not doing any useful work, but it closely resembles a real function that quickly updates the app's state. The function does some work on a background queue for a short interval of time and then schedules a state update on the main queue. For example, when using the camera API, the app might update the preview image as frequently as 30 times per second.
Question
When the app is running, the TabView does not respond to taps. It's permanently stuck on the first tab. Removing liveController.message = "Nice" line fixes the issue.
Why is TabView stuck?
Why is updating #StateObject causing this issue?
How to adapt this simple example, so that the TabView is not stuck?
import SwiftUI
class LiveController: ObservableObject {
#Published var message = "Hello"
}
#main
struct LiveApp: App {
#StateObject var liveController = LiveController()
var body: some Scene {
WindowGroup {
TabView() {
Text(liveController.message)
.tabItem {
Image(systemName: "1.circle")
}
Text("Tab 2")
.tabItem {
Image(systemName: "2.circle")
}
}
.onAppear {
DispatchQueue.global(qos: .userInitiated).async {
simulateFastStateUpdate()
}
}
}
}
func simulateFastStateUpdate() {
DispatchQueue.main.async {
liveController.message = "Nice"
}
// waits 33 ms ~ 30 updates per second
usleep(33 * 1000)
DispatchQueue.global(qos: .userInitiated).async {
simulateFastStateUpdate()
}
}
}
You are blocking the main thread with these constant updates and the app is busy processing your UI updates and can't handle touch inputs (also received on the main thread).
Whatever creates this rapid event stream needs to be throttled. You can use Combine's throttle or debounce functionality to reduce the frequency of your UI updates.
Look at this sample, I added the class UpdateEmittingComponent producing updates with a Timer. This could be your background component updating rapidly.
In your LiveController I'm observing the result with Combine. There I added a throttle into the pipeline which will cause the message publisher to fiere once per second by dropping all in-between values.
Removing the throttle will end up in an unresponsive TabView.
import SwiftUI
import Combine
/// class simulating a component emitting constant events
class UpdateEmittingComponent: ObservableObject {
#Published var result: String = ""
private var cancellable: AnyCancellable?
init() {
cancellable = Timer
.publish(every: 0.00001, on: .main, in: .default)
.autoconnect()
.sink {
[weak self] _ in
self?.result = "\(Date().timeIntervalSince1970)"
}
}
}
class LiveController: ObservableObject {
#Published var message = "Hello"
#ObservedObject var updateEmitter = UpdateEmittingComponent()
private var cancellable: AnyCancellable?
init() {
updateEmitter
.$result
.throttle(for: .seconds(1),
scheduler: RunLoop.main,
latest: true
)
.assign(to: &$message)
}
}
#main
struct LiveApp: App {
#StateObject var liveController = LiveController()
var body: some Scene {
WindowGroup {
TabView() {
Text(liveController.message)
.tabItem {
Image(systemName: "1.circle")
}
Text("Tab 2")
.tabItem {
Image(systemName: "2.circle")
}
}
}
}
}

SwiftUI Schedule Local Notification Without Button?

This may have a very simple answer, as I am pretty new to Swift and SwiftUI and am just starting to learn. I'm trying to schedule local notifications that will repeat daily at a specific time, but only do it if a toggle is selected. So if a variable is true, I want that notification to be scheduled. I looked at some tutorials online such as this one, but they all show this using a button. Instead of a button I want to use a toggle. Is there a certain place within the script that this must be done? What do I need to do differently in order to use a toggle instead of a button?
You can observe when the toggle is turned on and turned off -- In iOS 14 you can use the .onChange modifier to do this:
import SwiftUI
struct ContentView: View {
#State var isOn: Bool = false
var body: some View {
Toggle(isOn: $isOn, label: {
Text("Notifications?")
})
/// right here!
.onChange(of: isOn, perform: { toggleIsOn in
if toggleIsOn {
print("schedule notification")
} else {
print("don't schedule notification")
}
})
}
}
For earlier versions, you can try using onReceive with Combine:
import SwiftUI
import Combine
struct ContentView: View {
#State var isOn: Bool = false
var body: some View {
Toggle(isOn: $isOn, label: {
Text("Notifications?")
})
/// a bit more complicated, but it works
.onReceive(Just(isOn)) { toggleIsOn in
if toggleIsOn {
print("schedule notification")
} else {
print("don't schedule notification")
}
}
}
}
You can find even more creative solutions to observe the toggle change here.

Receive notifications for focus changes between apps on Split View when using SwiftUI

What should I observe to receive notifications in a View of focus changes on an app, or scene, displayed in an iPaOS Split View?
I'm trying to update some data, for the View, as described here, when the user gives focus back to the app.
Thanks.
Here is a solution that updates pasteDisabled whenever a UIPasteboard.changedNotification is received or a scenePhase is changed:
struct ContentView: View {
#Environment(\.scenePhase) private var scenePhase
#State private var pasteDisabled = false
var body: some View {
Text("Some Text")
.contextMenu {
Button(action: {}) {
Text("Paste")
Image(systemName: "doc.on.clipboard")
}
.disabled(pasteDisabled)
}
.onReceive(NotificationCenter.default.publisher(for: UIPasteboard.changedNotification)) { _ in
updatePasteDisabled()
}
.onChange(of: scenePhase) { _ in
updatePasteDisabled()
}
}
func updatePasteDisabled() {
pasteDisabled = !UIPasteboard.general.contains(pasteboardTypes: [aPAsteBoardType])
}
}

Resources