RxSwift: Repeat a (completed) stream - swift3

Assume I have a button which can be used to start and stop (toggle) an action.
let toggleStream: Observable<Bool> = toggleBtn.rx.tap.scan(false) { state, _ in !state }
I have another stream, that emits Integers continuously.
let emitter = Observable<Int>.interval(2.0, scheduler: timerScheduler)
Now I want to use the toggle stream to start and stop the emitting of the second stream. This is my approach:
Observable.combineLatest(toggleStream, emitter) { shouldEmit, evt in
return (shouldEmit, evt)
}.takeWhile{ (shouldEmit, evt:Int) in
return shouldEmit == true
}.map {(_, evt) in
return evt
}
This works great for the first time. I can press the button and the Observable starts emitting its Ints. Also stopping works. Sadly I can't start it for a second time, because the stream is completed. How can I restart/retry/repeat it when the user toggles the button again?

Here's how I did it in the playground. You should be able to extrapolate:
//: Playground - noun: a place where people can play
import RxSwift
let toggleButton = PublishSubject<Void>()
let toggleStream: Observable<Bool> = toggleButton
.scan(false) { state, _ in !state }
.debug()
.shareReplayLatestWhileConnected()
let emit = toggleStream
.filter { $0 }
.flatMapLatest { _ in
Observable<Int>.interval(2.0, scheduler: MainScheduler.instance)
.takeUntil(toggleStream.filter { !$0 })
}
_ = emit.subscribe( {
print($0)
})
toggleButton.onNext()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0) {
toggleButton.onNext()
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 11.0) {
toggleButton.onNext()
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 17.0) {
toggleButton.onNext()
}
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

Related

View Does Not Reload When Observed Object Changed

I'm struggling with what I thought would be a pretty simple problem to solve.
See below...
I have a class - "Connect" - This is #Observable.
Within Connect I have a property "theMessage" which is #Published.
I then have my ContentView which references the property "theMessage"
When the app is launched the view loads OK but...
When the button is the methods within Connect are triggered but the view does not reload.
I believe that the problem is in the "receive" method towards the bottom of "Connect"
Within the closure of this method I can see I'm the debug..
incomingMessage received OK
theMethod gets set OK
But the view doesn't change
Any Help Or Ideas Would Be Appreciated
import Foundation
import Network
class Connect: ObservableObject {
static let sharedInstance = Connect()
private var talking: NWConnection?
private var listening: NWListener?
#Published var theMessage = "Still No Message"
// DEFINE LISTENER
func listenUDP(port: NWEndpoint.Port) {
do {
self.listening = try NWListener(using: .udp, on: port)
self.listening?.stateUpdateHandler = {(newState) in
switch newState {
case .ready:
print("ready")
default:
break
}
}
self.listening?.newConnectionHandler = {(newConnection) in
newConnection.stateUpdateHandler = {newState in
switch newState {
case .ready:
print("new connection")
self.receive(on: newConnection)
default:
break
}
}
newConnection.start(queue: DispatchQueue(label: "new client"))
}
} catch {
print("unable to create listener")
}
self.listening?.start(queue: .main)
}// END OF FUNC - LISTEN TO UDP
// DEFINE ON RECEIVE
func receive(on connection: NWConnection) {
connection.receiveMessage { (data, context, isComplete, error) in
if let error = error {
print(error)
return
}
if let data = data, !data.isEmpty {
let incomingString = String(decoding: data, as: UTF8.self)
print("Incoming String -\(incomingString)")
DispatchQueue.main.async { [weak self] in
self?.objectWillChange.send()
self?.theMessage = incomingString
print(self?.theMessage ?? "Self Got Binned")
}
}
}
}// END OF FUNC - RECEIVE
// DEFINE TALKER
func connectToUDP(hostUDP:NWEndpoint.Host,portUDP:NWEndpoint.Port) {
self.talking = NWConnection(host: hostUDP, port: portUDP, using: .udp)
self.talking?.stateUpdateHandler = { (newState) in
switch (newState) {
case .ready:
break
default:
break
}
}
self.talking?.start(queue: .main)
}// END OF DEFINE TALKER
// SEND A MESSAGE
func sendUDP(_ content: String) {
let contentToSendUDP = content.data(using: String.Encoding.utf8)
self.talking?.send(content: contentToSendUDP, completion: NWConnection.SendCompletion.contentProcessed(({ (NWError) in
if (NWError == nil) {
// code
} else {
print("ERROR! Error when data (Type: String) sending. NWError: \n \(NWError!) ")
}
})))
}
}// END OF CLASS - CONNECT
import SwiftUI
import Network
struct ContentView: View {
#ObservedObject var connect = Connect.sharedInstance
let communication = Connect()
var body: some View {
VStack {
Text("Incoming Message - \(self.connect.theMessage)")
.padding(100)
.onAppear(){
// LISTENER
let port2U = NWEndpoint.Port.init(integerLiteral: 1984)
communication.listenUDP(port: port2U)
}
Button(action: {
let host = NWEndpoint.Host.init("localhost")
let port = NWEndpoint.Port.init("1984")
self.communication.connectToUDP(hostUDP: host, portUDP: port!)
self.communication.sendUDP("/cue/MyText/start")
}) {
Text("smoke")
}
}// END VSTACK
}// END OF BODY
}// END OF VIEW
This code in your Connect class creates a singleton instance
static let sharedInstance = Connect()
Then the View creates another instance with this
let communication = Connect()
One cannot see what the other is doing. It is like creating having 2 cars, 2 houses, 2 people.
Remove communication and replace with connect. Observe and use the Singleton.
struct ConnectView: View {
#ObservedObject var connect = Connect.sharedInstance
var body: some View {
VStack {
Text("Incoming Message - \(self.connect.theMessage)")
.padding(100)
.onAppear(){
// LISTENER
let port2U = NWEndpoint.Port.init(integerLiteral: 1984)
connect.listenUDP(port: port2U)
}
Button(action: {
let host = NWEndpoint.Host.init("localhost")
let port = NWEndpoint.Port.init("1984")
self.connect.connectToUDP(hostUDP: host, portUDP: port!)
self.connect.sendUDP("/cue/MyText/start")
}) {
Text("smoke")
}
}// END VSTACK
}// END OF BODY
}// END OF VIEW
It is good practice to make the initializer of a class private if you will have a Singleton pattern.
Add this to your Connect class to prevent this issue.
private init(){
}

SwiftUI & Scheduled timers

I'm building an app with SwiftUI with a stop watch functionality.
The TimedSession class has a var timer: Timer computed property like this:
Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
guard let time = self.currentTime else {
print("no current time")
return
}
if self.status != .running {
timer.invalidate()
return
}
time.duration += 1
self.objectWillChange.send()
}
this works well until I start to interact with other parts of the UI, like a scrollview. It's not just that the timer UI is locked, but the block: callbck on scheduledTimer isn't being executed.
What's the best way to put the callback block into the background? I've tried with GCD but no good so far.
Scheduled Timer works by default in .default run loop mode, but during user interaction, like scrolling, a run loop works in different tracking mode.
The solution is set up Timer for all standard modes, named .common, like below
// just create non attached timer
let timer = Timer(timeInterval: 0.01, repeats: true) { timer in
guard let time = self.currentTime else {
print("no current time")
return
}
if self.status != .running {
timer.invalidate()
return
}
time.duration += 1
self.objectWillChange.send()
}
...
init() {
// Start timer in run-loop on common modes
RunLoop.main.add(timer, forMode: .common)
}

SwiftUI ObservableObject and Published issue

here is something that keeps me awake for three days already: I'm writing a little app that connects via BlueTooth to an Arduino. To get visual feedback about the connection state and the transmitted data, I use a view that allows me to connect/disconnect as well as shows me the state and data:
VStack {
Text("Glove Training App")
.font(.title)
HStack {
Button(action: { MyBluetoothManager.shared.scan() }) {
Text("Connect")
.padding(30)
}
Text(" | ")
Button(action: { MyBluetoothManager.shared.disconnect()}) {
Text("Disconnect")
.padding(30)
}
}
Text(manager.stateChange)
.font(.subheadline)
.padding(.bottom, 30)
Text(peripheral.transmittedString)
.font(.subheadline)
.padding(.bottom, 30)
}
}
In a separate file I have all the BT management:
class MyBluetoothManager: NSObject, ObservableObject {
#Published var stateChange: String = "Initializing..." {
willSet { objectWillChange.send() }
}
static let shared = MyBluetoothManager()
let central = CBCentralManager(delegate: MyCentralManagerDelegate.shared,
queue: nil, options: [
CBCentralManagerOptionRestoreIdentifierKey: restoreIdKey,
])
(...)
func setConnected(peripheral: CBPeripheral) {
(...)
state = .connected(peripheral)
self.stateChange = "Connected"
print("Connected")
}
}
class MyPeripheralDelegate: NSObject, ObservableObject, CBPeripheralDelegate {
let objectWillChange = ObservableObjectPublisher()
var transmittedString: String = "No data" {
willSet { objectWillChange.send()
}
}
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
(...)
let rxData = characteristic.value
if let str = NSString(data: rxData!, encoding: String.Encoding.utf8.rawValue) as String? {
print(str)
self.transmittedString = str
let measurement = str.components(separatedBy: "|")
(...)
} else {
print("not a valid UTF-8 sequence")
}
}
}
The values are initially set correctly, but then never updated. In the terminal I can see the printed values and the app works otherwise as expected. I'm on the latest version of XCode.
I looked at several tutorials, and this seems to be tricky. Any help would be highly appreciated.
Cheers,
Christian
EDIT: Here is the full BluetoothManager class (not my code mostly but works fine):
class MyBluetoothManager: NSObject, ObservableObject {
#Published var stateChange: String = "Initializing..." {
willSet { objectWillChange.send() }
}
static let shared = MyBluetoothManager()
let central = CBCentralManager(delegate: MyCentralManagerDelegate.shared,
queue: nil, options: [
CBCentralManagerOptionRestoreIdentifierKey: restoreIdKey,
])
var state = State.poweredOff
enum State {
case poweredOff
case restoringConnectingPeripheral(CBPeripheral)
case restoringConnectedPeripheral(CBPeripheral)
case disconnected
case scanning(Countdown)
case connecting(CBPeripheral, Countdown)
case discoveringServices(CBPeripheral, Countdown)
case discoveringCharacteristics(CBPeripheral, Countdown)
case connected(CBPeripheral)
case outOfRange(CBPeripheral)
var peripheral: CBPeripheral? {
switch self {
case .poweredOff: return nil
case .restoringConnectingPeripheral(let p): return p
case .restoringConnectedPeripheral(let p): return p
case .disconnected: return nil
case .scanning: return nil
case .connecting(let p, _): return p
case .discoveringServices(let p, _): return p
case .discoveringCharacteristics(let p, _): return p
case .connected(let p): return p
case .outOfRange(let p): return p
}
}
}
func scan() {
guard central.state == .poweredOn else {
self.stateChange = "Cannot scan, BT is not powered on"
print("Cannot scan, BT is not powered on")
return
}
central.scanForPeripherals(withServices: [myDesiredServiceId], options: nil)
state = .scanning(Countdown(seconds: 10, closure: {
self.central.stopScan()
self.state = .disconnected
self.stateChange = "Scan timed out"
print("Scan timed out")
}))
}
func disconnect(forget: Bool = false) {
if let peripheral = state.peripheral {
central.cancelPeripheralConnection(peripheral)
}
if forget {
UserDefaults.standard.removeObject(forKey: peripheralIdDefaultsKey)
UserDefaults.standard.synchronize()
}
self.stateChange = "Disconnected"
state = .disconnected
}
func connect(peripheral: CBPeripheral) {
central.connect(peripheral, options: nil)
state = .connecting(peripheral, Countdown(seconds: 10, closure: {
self.central.cancelPeripheralConnection(peripheral)
self.state = .disconnected
self.stateChange = "Connect timed out"
print("Connect timed out")
}))
}
func discoverServices(peripheral: CBPeripheral) {
peripheral.delegate = MyPeripheralDelegate.shared
peripheral.discoverServices([myDesiredServiceId])
state = .discoveringServices(peripheral, Countdown(seconds: 10, closure: {
self.disconnect()
self.stateChange = "Could not discover services"
print("Could not discover services")
}))
}
func discoverCharacteristics(peripheral: CBPeripheral) {
guard let myDesiredService = peripheral.myDesiredService else {
self.disconnect()
return
}
peripheral.delegate = MyPeripheralDelegate.shared
peripheral.discoverCharacteristics([myDesiredCharacteristicId],
for: myDesiredService)
state = .discoveringCharacteristics(peripheral, Countdown(seconds: 10,
closure: {
self.disconnect()
self.stateChange = "Could not discover characteristics"
print("Could not discover characteristics")
}))
}
func setConnected(peripheral: CBPeripheral) {
guard let myDesiredCharacteristic = peripheral.myDesiredCharacteristic
else {
self.stateChange = "Missing characteristic"
print("Missing characteristic")
disconnect()
return
}
UserDefaults.standard.set(peripheral.identifier.uuidString,
forKey: peripheralIdDefaultsKey)
UserDefaults.standard.synchronize()
peripheral.delegate = MyPeripheralDelegate.shared
peripheral.setNotifyValue(true, for: myDesiredCharacteristic)
state = .connected(peripheral)
self.stateChange = "Connected"
print("Connected")
}
}
Button(action: { MyBluetoothManager.shared.scan() }) {
Text("Connect")
.padding(30)
}
Text(" | ")
Button(action: { MyBluetoothManager.shared.disconnect()}) {
Text("Disconnect")
.padding(30)
}
}
Text(manager.stateChange) << why don't you use MyBluetoothManager.shared here ? is there a second instance? this might be the error...but unfortunately you just showed us a small piece of code...

How to stop a method while it's running in swiftui

I have a problem and I have to find a solution. I must immediately stop the "Starting" method if the user presses the Cancel button.
I have not fixed the problem yet because I can not block the method while it is running but only at the end or at the beginning of the execution.
ContentView:
Button(action: {
self.Starting()
DispatchQueue.main.asyncAfter(deadline: .init(uptimeNanoseconds: 0), execute: self.workItem)
}) {
Text("START")
}
Button(action: {
self.workItem.cancel()
}) {
Text("CANCEL")
}
Starting method:
func Starting() {
self.workItem = DispatchWorkItem {
DispatchQueue.main.async {
self.impostazioniChek=true
}
DispatchQueue.global().async {
if (self.pm.MarksSwitch)
{
sleep(UInt32(self.MarksT))
self.Marks()
}
if (self.pm.ReadySwitch)
{
sleep(UInt32(self.ReadyT))
self.Ready()
}
self.Start()
sleep(3)
DispatchQueue.main.async {
self.tm.start()
}
}
}
}
You cannot cancel because
1) almost all the time work items runs on main (UI) queue, so block it
2) inside work item there is no check for isCancelled. DispatchWorkItem.cancel() only marks it cancelled, but does not stops execution you have to do this.
So to make this work it needs to redirect everything not-UI related in your work item into .background queue explicitly.
Eg. instead of
DispatchQueue.main.asyncAfter(deadline: .init(uptimeNanoseconds: 0), execute: self.workItem)
use
DispatchQueue.global(qos: .background).async { self.workItem }
Instead of
DispatchQueue.global().async {
if (self.pm.MarksSwitch)
Just use (because it is already in background)
if (self.pm.MarksSwitch)
and I'm not sure what is (probably it also should be run on background queue)
self.tm.start()
And on second add inside block interruption on cancellation by regular check, like below
...
}
if self.workItem.isCancelled { // add as many such as needed
return
}
if (self.pm.ReadySwitch)
...

comparing loop element to the name of the UIbutton pressed inside the buttonpressed function

I am looping through an array of string arrays. I am comparing the element at index 0 to the title of the button pressed on the page(ideally). But I am getting an Unresolved Identifier error, which means I am doing something wrong. How Can I compare the element in the loop to the title of the button made programatically on the page. Here is my code! The issue is with the function at the bottom, in the if statement that is in the for loop. I don't know how to say 'if this index position of the element is equal too the title of the button pressed'.
import UIKit
import AVKit
import AVFoundation
class ViewController: UIViewController {
var songArray: [Array<String>] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//connect to website
let url = URL(string:"*******")
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil
{
print("error")
}
else
{
if let content = data
{
do
{
//download JSON data from php page, display data
let JSON = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as! [[String]]
print(JSON)
//Make buttons with JSON array
var buttonY: CGFloat = 20
for song in JSON {
self.songArray.append(song)
let SongButton = UIButton(frame: CGRect(x: 50, y: buttonY, width: 250, height: 30))
buttonY = buttonY + 50 // 50px spacing
SongButton.layer.cornerRadius = 10 //Edge formatting for buttons
SongButton.backgroundColor = UIColor.darkGray //Color for buttons
SongButton.setTitle("\(song[0])", for: UIControlState.normal) //button title
SongButton.titleLabel?.text = "\(song[0])"
SongButton.addTarget(self,action: #selector(self.songButtonPressed(_:)), for: UIControlEvents.touchUpInside) //button press / response
self.view.addSubview(SongButton) // adds buttons to view
}
}
catch
{
}
}
}
}
task.resume()
print(songArray)
}
func songButtonPressed(_ sender:UIButton!) { // function for buttons
for song in songArray {
if "\(song[0])" == SongButton.titleLabel?.text {
let URL = NSURL(string: "\(song[1])")
let player = AVPlayer(url: URL! as URL)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.view.bounds
self.view.layer.addSublayer(playerLayer)
player.play()
}
}
}
}
My train of thought is to loop through the array of arrays, compare index position 0 to all button titles on the page, and if it matches, plus index position 2 into the AV player. Thanks for any help or advice on the logic behind my code, I am a beginner and I know this level of programming is a bit over my head
It's most likely
for song in songArray {
// the string interpolation "\()" is redundant
if song[0] == sender.titleLabel?.text { ... }
or maybe
if song[0] == sender.title { ... }
SongButton is not related to the action method.