Combine Timer.TimerPublisher not starting - swift

I'm creating a login token validity timer, and I figured it needs to be a singleton that ticks and every second (or every minute or whatever), checks to see whether the login token is still valid.
But I can't even get the singleton to print a message every second. Why not?
import SwiftUI
import Combine
class TokenObserver: ObservableObject {
private let publisher = Timer.TimerPublisher(interval: 1.0, runLoop: .main, mode: .default)
private let cancellable: AnyCancellable?
static let instance = TokenObserver()
let uuid = UUID()
private init() {
NSLog("TokenObserver.init()")
self.cancellable = self.publisher.sink(receiveCompletion: { completion in
NSLog("TokenObserver \(completion)")
}, receiveValue: { date in
NSLog("TokenObserver timestamp=" + ISO8601DateFormatter().string(from: date))
})
}
deinit {
NSLog("TokenObserver.deinit()")
self.cancellable?.cancel()
}
}
struct ContentView: View {
var body: some View {
Text("Hello, world! Instance = " + TokenObserver.instance.uuid.uuidString)
.padding()
}
}

You have to call autoconnect() on the Timer publisher to fire it.
private init() {
NSLog("TokenObserver.init()")
self.cancellable = self.publisher.autoconnect().sink(receiveCompletion: { completion in
NSLog("TokenObserver \(completion)")
}, receiveValue: { date in
NSLog("TokenObserver timestamp=" + ISO8601DateFormatter().string(from: date))
})
}

Related

Publishing and Consuming a transcript from SFSpeechRecognizer

I'm using Apple's example of an Observable wrapper around SFSpeechRecognizer as follows:
class SpeechRecognizer: ObservableObject {
#Published var transcript: String
func transcribe() {}
}
The goal is to use a ViewModel to both consume the transcript as it is generated, as well as passing on the value to a SwiftUI View for visual debugging:
class ViewModel : ObservableObject {
#Published var SpeechText: String = ""
#ObservedObject var speech: SpeechRecognizer = SpeechRecognizer()
public init() {
speech.transcribe()
speech.transcript.publisher
.map { $0 as! String? ?? "" }
.sink(receiveCompletion: {
print ($0) },
receiveValue: {
self.SpeechText = $0
self.doStuff(transcript: $0)
})
}
private void doStuffWithText(transcript: String) {
//Process the output as commands in the application
}
}
I can confirm that if I observe transcript directly in a SwiftUI view, that the data is flowing through. My problem is receiving the values as they change, and then assigning that data to my own published variable.
How do I make this work?
Subscription should be stored otherwise it is canceled immediately, also you need to make subscription before actual usage (and some other memory related modifications made). So I assume you wanted something like:
class ViewModel : ObservableObject {
#Published var SpeechText: String = ""
var speech: SpeechRecognizer = SpeechRecognizer() // << here !!
private var subscription: AnyCancellable? = nil // << here !!
public init() {
self.subscription = speech.transcript.publisher // << here !!
.map { $0 as! String? ?? "" }
.sink(receiveCompletion: {
print ($0) },
receiveValue: { [weak self] value in
self?.SpeechText = value
self?.doStuffWithText(transcript: value)
})
self.speech.transcribe() // << here !!
}
private func doStuffWithText(transcript: String) {
//Process the output as commands in the application
}
}
Tested with Xcode 13.2

UI locking up when using AlamoFire downloadProgress

I'm trying to create a download progress bar and show an alert at the same time when a download is completing.
For this task, I'm using AlamoFire with SwiftUI since it makes downloading easy.
However, when I track the progress using a ProgressView with a Published variable, the entire UI locks up and I can't figure out how to fix it.
I tried adding the downloadProgress to a separate DispatchQueue, but I still have to update the UI from the main thread otherwise Xcode will complain.
How to test the attached example code:
Click "Start download"
Wait for the ProgressView to move a bit
Click the "Show alert" button
Try closing the alert, it won't close.
I would appreciate any help.
import SwiftUI
import Alamofire
struct ContentView: View {
#StateObject var viewModel: ViewModel = ViewModel()
#State private var showAlert = false
var body: some View {
VStack {
Button("Show Alert") {
showAlert.toggle()
}
Button("Start download") {
viewModel.startDownload()
}
if viewModel.showProgressView {
ProgressView("Downloading…", value: viewModel.downloadProgress, total: 1.0)
.progressViewStyle(.linear)
}
}
.alert(isPresented: $showAlert) {
Alert(
title: Text("Text"),
dismissButton: .cancel()
)
}
}
}
class ViewModel: ObservableObject {
#Published var currentDownload: DownloadRequest? = nil
#Published var downloadProgress: Double = 0.0
#Published var showProgressView: Bool = false
func startDownload() {
print("Function called!")
showProgressView.toggle()
let queue = DispatchQueue(label: "alamofire", qos: .utility)
let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory)
AF.download("https://speed.hetzner.de/10GB.bin", to: destination)
.downloadProgress(queue: queue) { progress in
print(progress.fractionCompleted)
DispatchQueue.main.async {
self.downloadProgress = progress.fractionCompleted
}
}
.response { response in
print(response)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The issue here is that you are effectively spamming the UI thread with updates, since alamofire calls the closure provided to downloadProgress very often (look at the console prints). You need to stagger the updates from AF progress a bit so that the button press to dismiss the alert can register (in Combine this would be known as debounce). What I've done here is added a little time counter so that it only updates the progress every 1 second. The time between those updates keeps the UI thread free to respond to taps etc.
import SwiftUI
import Alamofire
struct ContentView: View {
#StateObject var viewModel: ViewModel = ViewModel()
#State private var showAlert = false
var body: some View {
VStack {
Button("Show Alert") {
showAlert.toggle()
}
Button("Start download") {
viewModel.startDownload()
}
if viewModel.showProgressView {
ProgressView("Downloading…", value: viewModel.downloadProgress, total: 1.0)
.progressViewStyle(.linear)
}
}
.alert(isPresented: $showAlert) {
Alert(
title: Text("Text"),
dismissButton: .cancel()
)
}
}
}
class ViewModel: ObservableObject {
#Published var currentDownload: DownloadRequest? = nil
#Published var downloadProgress: Double = 0.0
#Published var showProgressView: Bool = false
func startDownload() {
print("Function called!")
showProgressView.toggle()
let queue = DispatchQueue(label: "net", qos: .userInitiated)
let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory)
var last = Date()
AF.download("https://speed.hetzner.de/10GB.bin", to: destination)
.downloadProgress(queue:queue) { progress in
print(progress.fractionCompleted)
if Date().timeIntervalSince(last) > 1 {
last = Date()
DispatchQueue.main.async {
self.downloadProgress = progress.fractionCompleted
}
}
}
.response { response in
// print(response)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Using a class inside a struct is giving an error: "partial application of 'mutating' method is not allowed"

I am creating a class inside a struct to create a timer that sends information between an Apple Watch and the paired phone. When trying to run the timer with a button the error:
Partial application of the 'mutating' method is not allowed
The way I'm creating the class is the following:
import SwiftUI
struct ContentView: View {
//Timer to send information to phone
var timerLogic: TimerLogic!
var body: some View {
Button(action: startTimer, //"partial application of 'mutating' method is not allowed"
label: {
Image(systemName: "location")
})
}
// Class with the timer logic
class TimerLogic {
var structRef: ContentView!
var timer: Timer!
init(_ structRef: ContentView) {
self.structRef = structRef
self.timer = Timer.scheduledTimer(
timeInterval: 3.0,
target: self,
selector: #selector(timerTicked),
userInfo: nil,
repeats: true)
}
func stopTimer() {
self.timer?.invalidate()
self.structRef = nil
}
// Function to run with each timer tick
#objc private func timerTicked() {
self.structRef.timerTicked()
}
}
mutating func startTimer() {
self.timerLogic = TimerLogic(self)
}
// Function to run with each timer tick, this can be any action
func timerTicked() {
let data = ["latitude": "\(location.coordinate.latitude)", "longitud": "\(location.coordinate.longitude)"]
connectivity.sendMessage(data)
}
}
The closest solution that might solve the error is this one or is there another one?
SwiftUI Views should not have mutating properties/functions. Instead, they should use property wrappers like #State and #StateObject for state.
Besides the mutating function, you're fighting the principals of SwiftUI a bit. For example, you should never try to keep a reference to a View and call a function on it. Views are transient in SwiftUI and should not be expected to exist again if you need to call back to them. Also, SwiftUI tends to go hand-in-hand with Combine, which would be a good fit for implementing in your Timer code.
This might be a reasonable refactor:
import SwiftUI
import Combine
// Class with the timer logic
class TimerLogic : ObservableObject {
#Published var timerEvent : Timer.TimerPublisher.Output?
private var cancellable: AnyCancellable?
func startTimer() {
cancellable = Timer.publish(every: 3.0, on: RunLoop.main, in: .default)
.autoconnect()
.sink { event in
self.timerEvent = event
}
}
func stopTimer() {
cancellable?.cancel()
}
}
struct ContentView: View {
//Timer to send information to phone
#StateObject var timerLogic = TimerLogic()
var body: some View {
Button(action: timerLogic.startTimer) {
Image(systemName: "location")
}
.onChange(of: timerLogic.timerEvent) { _ in
timerTicked()
}
}
// Function to run with each timer tick, this can be any action
func timerTicked() {
print("Timer ticked...")
//...
}
}

Swift: return value every x seconds

I'm trying to generate random string every 10 seconds. I put this function in a class in another file and will call the function in another view controller. But now I'm not getting any output when I call it. How to can I fix this code
class Data{
static let instance = Data()
func randomString(of length: Int){
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var s = ""
for _ in 0 ..< length {
s.append(letters.randomElement()!)
print("\(s) = I'm in randomString Func")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) { [weak self] in
self?.randomString(of: 5)
}
}
}
in a view controller I put it under a button action and call it with this code
Button(action: {
info = Data.instance.randomString(of:5)
print(info)
}, label: {
Text ("PRINT")
.font(.callout)
.foregroundColor(Color.primary)
})
A possible solution is to use the Timer from the Combine framework:
struct ContentView: View {
#State private var text = "initial text"
#State private var timer: AnyCancellable?
var body: some View {
Text(text)
Button(action: startTimer) { // start timer on button tap, alternatively put it in `onAppear`
Text("Start timer")
}
}
func startTimer() {
// start timer (tick every 10 seconds)
timer = Timer.publish(every: 10, on: .main, in: .common)
.autoconnect()
.sink { _ in
text = DataGenerator.instance.randomString(of: 5)
}
}
}
You also need to return the String from the randomString function. A good thing would also be to rename Data to avoid collisions:
class DataGenerator { // rename the class
static let instance = DataGenerator()
func randomString(of length: Int) -> String { // return `String`
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var s = ""
for _ in 0 ..< length {
s.append(letters.randomElement()!)
print("\(s) = I'm in randomString Func")
}
return s // return String, don't call `DispatchQueue.main.async` here
}
}
You may also consider moving the timer logic out of the view like in:
How to make the View update instant in SwiftUI?

Using Combine to parse phone number String

I'm trying to wrap my mind around how Combine works. I believe I'm doing something wrong when I use the .assign operator to mutate the #Published property I'm operating on. I've read the documentation on Publishers, Subscribers, and Operators. But I'm a bit loose on where exactly to create the Publisher if I don't want it to be a function call.
import SwiftUI
import Combine
struct PhoneNumberField: View {
let title: String
#ObservedObject var viewModel = ViewModel()
var body: some View {
TextField(title,text: $viewModel.text)
}
class ViewModel: ObservableObject {
#Published var text: String = ""
private var disposables = Set<AnyCancellable>()
init() {
$text.map { value -> String in
self.formattedNumber(number: value)
}
//something wrong here
.assign(to: \.text, on: self)
.store(in: &disposables)
}
func formattedNumber(number: String) -> String {
let cleanPhoneNumber = number.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let mask = "+X (XXX) XXX-XXXX"
var result = ""
var index = cleanPhoneNumber.startIndex
for ch in mask where index < cleanPhoneNumber.endIndex {
if ch == "X" {
result.append(cleanPhoneNumber[index])
index = cleanPhoneNumber.index(after: index)
} else {
result.append(ch)
}
}
return result
}
}
}
struct PhoneNumberParser_Previews: PreviewProvider {
static var previews: some View {
PhoneNumberField(title: "Phone Number")
}
}
Use .receive(on:):
$text.map { self.formattedNumber(number: $0) }
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] value in
self?.text = value
})
.store(in: &disposables)
This will allow you to listen to changes of the text variable and update it in the main queue. Using main queue is necessary if you want to update #Published variables read by some View.
And to avoid having a retain cycle (self -> disposables -> assign -> self) use sink with a weak self.