Swift: return value every x seconds - swift

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?

Related

SwiftUI unwrapping optionals inside View (pyramid of doom)

I have a sample program that does three things
Generate a random integer from -10...10 (regular function)
Generate 10 million random numbers from -10...10 (asynchronous function)
Calculate the average of #2 (throwing asynchronous function)
Below is the full working code. It works without errors, but the view has a horrible readability with three nested if/let loops. What's the best way/convention to get rid of the pyramid of doom in this scenario?
Result screenshot (how it should work)
Working code (methods)
class NumberManager: ObservableObject {
#Published var integer: Int?
#Published var numbers: [Double]?
#Published var average: Double?
func generateInt() {
self.integer = Int.random(in: -10...10)
}
func generateNumbers() async {
self.numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
// takes about 5 seconds to run...
}
func calculateAverageNumber(for numbers: [Double]) async throws {
guard !numbers.isEmpty else {
print("numbers not generated")
return
}
let total = numbers.reduce(0, +)
let average = total / Double(numbers.count)
self.average = average
}
}
Working code (view)
struct ContentView: View {
#StateObject var numberManager = NumberManager()
var body: some View {
if let integer = numberManager.integer {
if let numbers = numberManager.numbers {
if let average = numberManager.average {
Text("Integer is \(integer)")
Text("First number is: \(numbers[0])")
Text("Average is: \(average)")
} else {
LoadingView(loadingType: "Calculating average")
.task {
do {
try await numberManager.calculateAverageNumber(for: numbers)
} catch {
print("empty numbers array")
}
}
}
} else {
LoadingView(loadingType: "Generating numbers")
.task {
await numberManager.generateNumbers()
}
}
} else {
LoadingView(loadingType: "Generating int")
.task {
numberManager.generateInt()
}
}
}
}
What I tried so far...
I tried building helper functions to build views as below, and called those functions that returns views inside my ContentView. When I run it, the integer and the number array gets generated and shows, but the last task that calculates the average does not get called again at all.
Result screenshot(with issues)
Code (Runs without errors. But the last task that calculates average doesn't get executed)
struct ContentView: View {
#StateObject var numberManager = NumberManager()
var body: some View {
intergerView()
.task {
print("Generating Int")
numberManager.generateInt()
}
numbersView()
.task {
print("Generating Numbers")
await numberManager.generateNumbers()
}
averageView()
.task {
do {
print("Calculating Average")
try await numberManager.calculateAverageNumber(for: numberManager.numbers ?? [])
} catch {
print("error")
}
}
}
}
private func intergerView() -> some View {
guard let integer = numberManager.integer else {
return AnyView(LoadingView(loadingType: "Generating int"))
}
return AnyView(Text("Integer is \(integer)"))
}
private func numbersView() -> some View {
guard let numbers = numberManager.numbers else {
return AnyView(LoadingView(loadingType: "Generating numbers"))
}
return AnyView(Text("First number is: \(numbers[0])"))
}
private func averageView() -> some View {
guard let average = numberManager.average else {
return AnyView(LoadingView(loadingType: "Calculating average"))
}
return AnyView(Text("Average is: \(average)"))
}
EDIT: In my app, I have a view that does all different functions in one view (it's like a dashboard). Some require others to run first (like calculating the average), whereas some can run on its own (Like generating one random integer). I want to display whatever that's loaded first, while displaying a loadingview placeholder for parts that aren't loaded yet.
Several issues here:
generateNumbers and calculateAverageNumber depend on each other. So they need to await each other.
your "working code" does not match the description of your code. You say you want to show what ever finishes first but your if/else statements introduce dependencies between all 3 functions/views
you donĀ“t need 3 different views. One that can be customized should be enough.
class NumberManager: ObservableObject {
#Published var integer: Int?
#Published var numbers: [Double]?
#Published var average: Double?
func generateInt() {
self.integer = Int.random(in: -10...10)
}
func generateNumbers() async {
self.numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
// takes about 5 seconds to run...
}
// No need for arguments here
func calculateAverageNumber() async throws {
guard let numbers = numbers, !numbers.isEmpty else {
print("numbers not generated")
return
}
let total = numbers.reduce(0, +)
let average = total / Double(numbers.count)
self.average = average
}
//This function will handle the dependenies of generating the values and calculating the avarage
func calculateNumbersAndAvarage() async throws{
await generateNumbers()
try await calculateAverageNumber()
}
}
The View:
struct ContentView: View{
#StateObject private var numberManager = NumberManager()
var body: some View{
//Show the different detail views.
VStack{
Spacer()
Spacer()
DetailView(text: numberManager.integer != nil ? "Integer is \(numberManager.integer!)" : nil)
.onAppear {
numberManager.generateInt()
}
Spacer()
Group{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
Spacer()
DetailView(text: numberManager.average != nil ? "Average is: \(numberManager.average!)" : nil)
}.onAppear {
Task{
do{
try await numberManager.calculateNumbersAndAvarage()
}
catch{
print("error")
}
}
}
Spacer()
Spacer()
}
}
//Just to make it more readable
var isNumbersValid: Bool{
numberManager.numbers != nil && numberManager.numbers?.count != 0
}
}
and the DetailView:
struct DetailView: View{
let text: String?
var body: some View{
// If no text to show, show `ProgressView`, or `LoadingView` in your case. You can inject the view directly or use a property for the String argument.
if let text = text {
Text(text)
.font(.headline)
.padding()
} else{
ProgressView()
}
}
}
The code should speak for itself. If you have any further question regarding this code please feel free to do so, but please read and try to understand how this works first.
Edit:
This does not wait for calculateAverageNumber to finish before displaying numbers[0]. The reason for it showing at the same time is that it takes almost no time to calculat the avarage. Try adding this between the 2 functions in calculateNumbersAndAvarage.
try await Task.sleep(nanoseconds: 4_000_000_000)
and you will see that it shows as it should.
You are almost done. Use #ViewBuilder , remove AnyView wrapper and dont use guard
#ViewBuilder
var intergerView: some View {
if let integer = numberManager.integer {
LoadingView(loadingType: "Generating int")
} else {
Text("Integer is \(integer)")
}
}

Combine Timer.TimerPublisher not starting

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))
})
}

Retrieving keyboard height when textfield editing starts in SwiftUI

So I have a class with a published variable called keyboardHeight that is used to retrieve the value of the keyboardHeight:
import UIKit
class KeyboardHeightHelper: ObservableObject {
#Published var keyboardHeight: CGFloat = 0
init() {
self.listenForKeyboardNotifications()
}
private func listenForKeyboardNotifications() {
NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification,
object: nil,
queue: .main) { (notification) in
guard let userInfo = notification.userInfo,
let keyboardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
self.keyboardHeight = keyboardRect.height
}
NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidHideNotification,
object: nil,
queue: .main) { (notification) in
self.keyboardHeight = 0
}
}
}
Then, in ContentView I just have a TextField that should print the keyboard height when you start/stop editing the field:
import SwiftUI
struct ContentView: View {
#State var textFieldText = ""
#ObservedObject var keyboardHeightHelper = KeyboardHeightHelper()
var body: some View {
VStack {
TextField("Text field",
text: $textFieldText, onEditingChanged: { _ in print("the keyboard height is \(self.keyboardHeightHelper.keyboardHeight)") })
}
}
}
The problem I have is this: When I am not editing the textfield and then click it, it prints the keyboard height is 0.0 (I guess this is because it grabs the keyboardHeight value before it presents the keyboard, so at the time the height is 0.0 as the keyboard isn't seen). When I press return and the keyboard is dismissed, the height of the keyboard (for the iPhone 8 simulator) is printed as the correct value of 260.0. My question is how do I access the value of the keyboard when I start editing?
Try this. You can integrate it using just a modifier to SwiftUI view:
extension UIResponder {
static var currentFirstResponder: UIResponder? {
_currentFirstResponder = nil
UIApplication.shared.sendAction(#selector(UIResponder.findFirstResponder(_:)), to: nil, from: nil, for: nil)
return _currentFirstResponder
}
private static weak var _currentFirstResponder: UIResponder?
#objc private func findFirstResponder(_ sender: Any) {
UIResponder._currentFirstResponder = self
}
var globalFrame: CGRect? {
guard let view = self as? UIView else { return nil }
return view.superview?.convert(view.frame, to: nil)
}
}
extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { $0.keyboardHeight }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
extension Notification {
var keyboardHeight: CGFloat {
return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
}
}
struct KeyboardAdaptive: ViewModifier {
#State private var bottomPadding: CGFloat = 0
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.padding(.bottom, self.bottomPadding)
.onReceive(Publishers.keyboardHeight) { keyboardHeight in
let keyboardTop = geometry.frame(in: .global).height - keyboardHeight
let focusedTextInputBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0
self.bottomPadding = max(0, focusedTextInputBottom - keyboardTop - geometry.safeAreaInsets.bottom)
}
.animation(.easeOut(duration: 0.16))
}
}
}
extension View {
func keyboardAdaptive() -> some View {
ModifiedContent(content: self, modifier: KeyboardAdaptive())
}
}
Usage:
struct ContentView: View {
#State private var text = ""
var body: some View {
VStack {
Spacer()
TextField("Enter something", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding()
.keyboardAdaptive()//note this
}
}
Credits to multiple online sources.
The .onEditingChanged is not appropriate place to read keyboard height, because you receive this callback right in the moment of click in TextField, so there is no keyboard yet shown (and, so, no notification received).
Instead you can listen explicitly for your keyboardHeight property publisher and be notified exactly when it is changed (what is performed on keyboard notifications synchronously, so in time)
Here is a solution (tested with Xcode 12 / iOS 14)
VStack {
TextField("Text field",
text: $textFieldText, onEditingChanged: { _ in })
.onReceive(keyboardHeightHelper.$keyboardHeight) { value in
print("the keyboard height is \(value)")
}
}

SwiftUI how to run a func after another is fully completed

I am running a func that goes into a for loop and append to a an array. In the next line I am running another func that uses the first element of that array, however, the app crashes since at the time of the 2nd func execution it finds the array empty. I trued using sync() queue and completion handlers but still have the issue. The only way that it is working at the moment is to call a Timer to wait for a few seconds but that is not ideal way to do it of course. Do you have any suggestions?
The 1st func is as follows:
func openRun () {
let openPanel = NSOpenPanel()
...
if result.rawValue == NSApplication.ModalResponse.OK.rawValue {
let rawURL = openPanel.url!.path
//some codes that extract image files from the openned path
for image in imageList {
images.append(newImage)
}
}
}
It is hard to see, what you try to do. Check next Playground snippet which use NSOpenPanel to select some .swift file(s) and asynchronously (random delay mimics the real world usage) calculates length of its absolute path and show the results in SwiftUI View.
//: A Cocoa based Playground to present user interface
import AppKit
import SwiftUI
import PlaygroundSupport
let panel = NSOpenPanel()
panel.allowsMultipleSelection = true
panel.allowedFileTypes = ["swift"]
struct Info: Identifiable {
let id = UUID()
let txt: String
let length: Int
}
struct ContentView: View {
#State var arr: [Info] = []
var body: some View {
VStack {
Button(action: {
panel.begin { (respond) in
panel.urls.forEach { (url) in
self.urlLength(url: url) { (i) in
self.arr.append(Info(txt: url.lastPathComponent, length: i))
}
}
}
}) {
Text("action")
}.padding()
List(arr) { (item) in
HStack {
Text(item.txt)
Text(item.length.description).foregroundColor(Color.yellow)
}
}
}.frame(width: 200, height: 400)
.border(Color.red)
}
func urlLength(url: URL, completion: #escaping (Int)->()) {
DispatchQueue.global().asyncAfter(deadline: .now() + Double.random(in: 0.0 ..< 3.0)) { [url] in
let c = url.absoluteString.count
DispatchQueue.main.async {
completion(c)
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
this funny example demonstrates how to use asynchronous code with SwiftUI

SwiftUI how to prevent view to reload whole body

Basically I try to figure out when my viewModel get updated, it will notify view and it will refresh whole body. How to avoid that. For example if my view GoLiveView already present another view BroadcasterView, and later my goLiveViewModel get updated, GoLiveView will be refreshed, and it will create BroadcasterView again , because showBroadcasterView = true. And it will cause so many issues down the road, because of that.
struct GoLiveView: View {
#ObservedObject var goLiveViewModel = GoLiveViewModel()
#EnvironmentObject var sessionStore: SessionStore
#State private var showBroadcasterView = false
#State private var showLiveView = false
init() {
goLiveViewModel.refresh()
}
var body: some View {
NavigationView {
List(goLiveViewModel.rooms) { room in // when goLiveViewModed get updated
NavigationLink(destination: LiveView(clientRole: .audience, room: room, showLiveView: $showLiveView))) {
LiveCell(room: room)
}
}.background(Color.white)
.navigationBarTitle("Live", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
self.showBroadcasterView = true
}, label: {
Image("ic_go_live").renderingMode(.original)
})).frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(red: 34/255, green: 34/255, blue: 34/255))
.sheet(isPresented: $showBroadcasterView) { // here is problem, get called many times, hence reload whole body ,and create new instances of BroadcasterView(). Because showBroadcasterView = is still true.
BroadcasterView(broadcasterViewModel: BroadcasterViewModel(showBroadcasterView: $showBroadcasterView))
.environmentObject(self.sessionStore)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.clear)
}
}
}
this is my GoliveViewModel
typealias RoomsFetchOuput = AnyPublisher<RoomsFetchState, Never>
enum RoomsFetchState: Equatable {
static func == (lhs: RoomsFetchState, rhs: RoomsFetchState) -> Bool {
switch (lhs, rhs) {
case (.loading, .loading): return true
case (.success(let lhsrooms), .success(let rhsrooms)):
return lhsrooms == rhsrooms
case (.noResults, .noResults): return true
case (.failure, .failure): return true
default: return false
}
}
case loading
case success([Room])
case noResults
case failure(Error)
}
class GoLiveViewModel: ObservableObject {
private lazy var webServiceManager = WebServiceManager()
#Published var rooms = [Room]()
private lazy var timer = Timer()
private var cancellables: [AnyCancellable] = []
init() {
timer = Timer.scheduledTimer(timeInterval: 4.0, target: self, selector: #selector(refresh) , userInfo: nil, repeats: true) // call every 4 second refresh
}
func fetch() -> RoomsFetchOuput {
return webServiceManager.fetchAllRooms()
.map ({ result -> RoomsFetchState in
switch result {
case .success([]): return .noResults
case let .success(rooms): return .success(rooms)
case .failure(let error): return .failure(error)
}
})
.eraseToAnyPublisher()
let isLoading: RoomsFetchOuput = .just(.loading)
let initialState: RoomsFetchOuput = .just(.noResults)
let idle: RoomsFetchOuput = Publishers.Merge(isLoading, initialState).eraseToAnyPublisher()
return Publishers.Merge(idle, rooms).removeDuplicates().eraseToAnyPublisher()
}
#objc func refresh() {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
fetch()
.sink { [weak self] state in
guard let self = self else { return }
switch state {
case let .success(rooms):
self.rooms = rooms
case .failure: print("failure")
// show error alert to user
case .noResults: print("no result")
self.rooms = []
// hide spinner
case .loading: print(".loading")
// show spinner
}
}
.store(in: &cancellables)
}
}
SwfitUI has a pattern for this. It needs to conform custom view to Equatable protocol
struct CustomView: View, Equatable {
static func == (lhs: CustomView, rhs: CustomView) -> Bool {
// << return yes on view properties which identifies that the
// view is equal and should not be refreshed (ie. `body` is not rebuilt)
}
...
and in place of construction add modifier .equatable(), like
var body: some View {
CustomView().equatable()
}
yes, new value of CustomView will be constructed every time as superview refreshing (so don't make init heavy), but body will be called only if newly constructed view is not equal of previously constructed
Finally, it is seen that it is very useful to break UI hierarchy to many views, it would allow to optimise refresh a lot (but not only good design, maintainability, reusability, etc. :^) ).