SwiftUI unwrapping optionals inside View (pyramid of doom) - swift

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

Related

Efficiently refactoring piece of Swift code to be less redundant

In the code below, A key is remapped to B key, and vice versa. The remapping is activated via a SwiftUI toggle switch.
In example presented here the same block of code is used in three different functions.
Additionally, the loop that iterates through the function call is also used in all three of these functions.
I've been struggling to simplify this code and make it less redundant for more than a day. Any help would be greatly appreciated.
let aKey: UInt64 = 0x700000004
let bKey: UInt64 = 0x700000005
func isKeyboardServiceClientForUsagePage(_ serviceClient: IOHIDServiceClient, _ usagePage: UInt32, _ usage: UInt32) -> Bool {
return IOHIDServiceClientConformsTo(serviceClient, usagePage, usage) == 1
}
func updateKeyboardKeyMapping(_ keyMap: [[String: UInt64]]) {
let eventSystemClient = IOHIDEventSystemClientCreateSimpleClient(kCFAllocatorDefault)
guard let serviceClients = IOHIDEventSystemClientCopyServices(eventSystemClient) as? [IOHIDServiceClient] else {
return
}
for serviceClient in serviceClients {
let usagePage = UInt32(kHIDPage_GenericDesktop)
let usage = UInt32(kHIDUsage_GD_Keyboard)
if isKeyboardServiceClientForUsagePage(serviceClient, usagePage, usage) {
IOHIDServiceClientSetProperty(serviceClient, kIOHIDUserKeyUsageMapKey as CFString, keyMap as CFArray)
}
}
}
func areKeysMappedOnAnyServiceClient() -> Bool {
let eventSystemClient = IOHIDEventSystemClientCreateSimpleClient(kCFAllocatorDefault)
guard let serviceClients = IOHIDEventSystemClientCopyServices(eventSystemClient) as? [IOHIDServiceClient] else {
return false
}
for serviceClient in serviceClients {
let usagePage = UInt32(kHIDPage_GenericDesktop)
let usage = UInt32(kHIDUsage_GD_Keyboard)
if isKeyboardServiceClientForUsagePage(serviceClient, usagePage, usage) {
guard let keyMapping = IOHIDServiceClientCopyProperty(serviceClient, kIOHIDUserKeyUsageMapKey as CFString) as? [[String: UInt64]] else {
return false
}
if keyMapping.contains(where: { $0[kIOHIDKeyboardModifierMappingSrcKey] == aKey && $0[kIOHIDKeyboardModifierMappingDstKey] == bKey }) &&
keyMapping.contains(where: { $0[kIOHIDKeyboardModifierMappingSrcKey] == bKey && $0[kIOHIDKeyboardModifierMappingDstKey] == aKey })
{
return true
}
}
}
return false
}
func remapABBA() {
let keyMap: [[String: UInt64]] = [
[
kIOHIDKeyboardModifierMappingSrcKey: aKey,
kIOHIDKeyboardModifierMappingDstKey: bKey,
],
[
kIOHIDKeyboardModifierMappingSrcKey: bKey,
kIOHIDKeyboardModifierMappingDstKey: aKey,
],
]
updateKeyboardKeyMapping(keyMap)
}
func resetKeyMapping() {
updateKeyboardKeyMapping([])
}
And here’s the SwiftUI part if you would like to try the app:
import SwiftUI
struct ContentView: View {
#State private var remapKeys = areKeysMappedOnAnyServiceClient()
var body: some View {
HStack {
Spacer()
Toggle(isOn: $remapKeys, label: { Text("Remap A → B and B → A.") })
.toggleStyle(SwitchToggleStyle())
.onChange(of: remapKeys, perform: toggleKeyboardRemapping)
Spacer()
}
}
}
private func toggleKeyboardRemapping(_ remapKeys: Bool) {
if remapKeys {
remapABBA()
} else {
resetKeyMapping()
}
}
OK... this is going to take some time to answer.
It seems like you're lacking in a place to store things. That's why you have to use the same block of code over and over. We can solve that with a view model...
In here I'm going to hide away the logic of what is happening from the view and only expose what the view needs access to in order to display itself.
// we make it observable so the view can subscribe to it.
class KeyMappingViewModel: ObservableObject {
private let aKey: UInt64 = 0x700000004
private let bKey: UInt64 = 0x700000005
private let srcKey = kIOHIDKeyboardModifierMappingSrcKey
private let dstKey = kIOHIDKeyboardModifierMappingDstKey
private var keyMap: [[String: UInt64]] {
[
[
srcKey: aKey,
dstKey: bKey,
],
[
srcKey: bKey,
dstKey: aKey,
],
]
}
// A more concise way to get hold of the client ref
private var client: IOHIDEventSystemClientRef {
IOHIDEventSystemClientCreateSimpleClient(kCFAllocatorDefault)
}
// Making this published means the view can use it as state in the Toggle
#Published var toggleState: Bool {
didSet {
if toggleState {
client.updateKeyMapping(keyMap)
} else {
client.updateKeyMapping([])
}
}
}
init() {
// set the initial value by asking the client if it has any keys mapped
toggleState = client.areKeysMappedOnAnyServiceClient(aKey: aKey, bKey: bKey)
}
}
I'm going to make extensions of IOHIDServiceClient and IOHIDEventSystemClientRef to encapsulate your logic...
extension IOHIDEventSystemClientRef {
private var srcKey: String { kIOHIDKeyboardModifierMappingSrcKey }
private var dstKey: String { kIOHIDKeyboardModifierMappingDstKey }
// Make this an optional var on the client ref itself.
private var serviceClients: [IOHIDServiceClient]? {
IOHIDEventSystemClientCopyServices(self) as? [IOHIDServiceClient]
}
func areKeysMappedOnAnyServiceClient(aKey: UInt64, bKey: UInt64) -> Bool {
// Nice Swift 5.7 syntax with the optional var
guard let serviceClients else {
return false
}
// I made this more concise with a filter and map.
// Also, using the extension we can make use of keyPaths to get the values.
return serviceClients.filter(\.isForGDKeyboard)
.compactMap(\.keyMapping)
.map { keyMapping in
keyMapping.contains(where: { $0[srcKey] == aKey && $0[dstKey] == bKey }) &&
keyMapping.contains(where: { $0[srcKey] == bKey && $0[dstKey] == aKey })
}
.contains(true)
}
func updateKeyMapping(_ keyMap: [[String: UInt64]]) {
// serviceClients is optional so we can just ? it.
// if it's nil, nothing after the ? happens.
serviceClients?.filter(\.isForGDKeyboard)
.forEach {
IOHIDServiceClientSetProperty($0, kIOHIDUserKeyUsageMapKey as CFString, keyMap as CFArray)
}
}
}
extension IOHIDServiceClient {
var isForGDKeyboard: Bool {
let usagePage = UInt32(kHIDPage_GenericDesktop)
let usage = UInt32(kHIDUsage_GD_Keyboard)
return IOHIDServiceClientConformsTo(self, usagePage, usage) == 1
}
var keyMapping: [[String: UInt64]]? {
IOHIDServiceClientCopyProperty(self, kIOHIDUserKeyUsageMapKey as CFString) as? [[String: UInt64]]
}
}
Doing all of this means that your view can look something like this...
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: KeyMappingViewModel = .init()
var body: some View {
HStack {
Spacer()
Toggle(isOn: $viewModel.toggleState) {
Text("Remap A → B and B → A.")
}
.toggleStyle(SwitchToggleStyle())
Spacer()
}
}
}
This contains all your same logic and TBH wasn't too bad already.
My main changes were to take the free functions and vars and add them to their respective types.
So, the update and areKeysMapped... functions now belong to the IOHIDEventSystemClientRef type.
The isForGDKeyboard and keyMapping vars now belong to the IOHIDServiceClient type.
Doing this removed a lot of the repeated code you had as you no longer had to continuously call free functions. It also meant we unlocked some very Swifty keyPath usage which helped make some of the logic more concise.
Then we made a view model. This allowed us to keep all the moving parts of the view in one place. It had a place to easily get hold of the client. It also meant we could hide a lot of the stuff inside the view model by making it private.
This meant that the view only had one thing it could do. Which is to use the binding to the toggleState. Everything else was behind closed doors to the view.

AsyncStream spams view, where AsyncPublisher does not

I'm running into a behavior with AsyncStream I don't quite understand.
When I have an actor with a published variable, I can "subscribe" to it via an AsyncPublisher and it behaves as expected, updating only when there is a change in value. If I create an AsyncStream with a synchronous context (but with a potential task retention problem) it also behaves as expected.
The weirdness happens when I try to wrap that publisher in an AsyncStream with an asyncronous context. It starts spamming the view with an update per loop it seems, NOT only when there is a change.
What am I missing about the AsyncStream.init(unfolding:oncancel:) which is causing this behavior?
https://developer.apple.com/documentation/swift/asyncstream/init(unfolding:oncancel:)?
import Foundation
import SwiftUI
actor TestService {
static let shared = TestService()
#MainActor #Published var counter:Int = 0
#MainActor public func updateCounter(by delta:Int) async {
counter = counter + delta
}
public func asyncStream() -> AsyncStream<Int> {
return AsyncStream.init(unfolding: unfolding, onCancel: onCancel)
//() async -> _?
func unfolding() async -> Int? {
for await n in $counter.values {
//print("\(location)")
return n
}
return nil
}
//optional
#Sendable func onCancel() -> Void {
print("confirm counter got canceled")
}
}
public func syncStream() -> AsyncStream<Int> {
AsyncStream { continuation in
let streamTask = Task {
for await n in $counter.values {
continuation.yield(n)
}
}
continuation.onTermination = { #Sendable _ in
streamTask.cancel()
print("StreamTask Canceled")
}
}
}
}
struct ContentView: View {
var body: some View {
VStack {
TestActorButton()
HStack {
//TestActorViewA() //<-- uncomment at your own risk.
TestActorViewB()
TestActorViewC()
}
}
.padding()
}
}
struct TestActorButton:View {
var counter = TestService.shared
var body: some View {
Button("increment counter") {
Task { await counter.updateCounter(by: 2) }
}
}
}
struct TestActorViewA:View {
var counter = TestService.shared
#State var counterVal:Int = 0
var body: some View {
Text("\(counterVal)")
.task {
//Fires constantly.
for await value in await counter.asyncStream() {
print("View A Value: \(value)")
counterVal = value
}
}
}
}
struct TestActorViewB:View {
var counter = TestService.shared
#State var counterVal:Int = 0
var body: some View {
Text("\(counterVal)")
.task {
//Behaves like one would expect. Fires once per change.
for await value in await counter.$counter.values {
print("View B Value: \(value)")
counterVal = value
}
}
}
}
struct TestActorViewC:View {
var counter = TestService.shared
#State var counterVal:Int = 0
var body: some View {
Text("\(counterVal)")
.task {
//Also only fires on update
for await value in await counter.syncStream() {
print("View C Value: \(value)")
counterVal = value
}
}
}
}
The real solution to wrapping a publisher appears to be to stick to the synchronous context initializer and have it cancel it's own task:
public func stream() -> AsyncStream<Int> {
AsyncStream { continuation in
let streamTask = Task {
for await n in $counter.values {
//do hard work to transform n
continuation.yield(n)
}
}
continuation.onTermination = { #Sendable _ in
streamTask.cancel()
print("StreamTask Canceled")
}
}
}
From what I can tell the "unfolding" style initializer for AsyncStream is simply not a fit for wrapping an AsyncPublisher. The "unfolding" function will "pull" at the published value from within the stream, so the stream will just keep pushing values from that infinite well.
It seems like the "unfolding" style initializer is best used when processing a finite (but potentially very large) list of items, or when generating ones values from scratch... something like:
struct NumberQueuer {
let numbers:[Int]
public func queueStream() -> AsyncStream<Int> {
var iterator = AsyncArray(values: numbers).makeAsyncIterator()
print("Queue called")
return AsyncStream.init(unfolding: unfolding, onCancel: onCancel)
//() async -> _?
func unfolding() async -> Int? {
do {
if let item = try await iterator.next() {
return item
}
} catch let error {
print(error.localizedDescription)
}
return nil
}
//optional
#Sendable func onCancel() -> Void {
print("confirm NumberQueue got canceled")
}
}
}
public struct AsyncArray<Element>: AsyncSequence, AsyncIteratorProtocol {
let values:[Element]
let delay:TimeInterval
var currentIndex = -1
public init(values: [Element], delay:TimeInterval = 1) {
self.values = values
self.delay = delay
}
public mutating func next() async throws -> Element? {
currentIndex += 1
guard currentIndex < values.count else {
return nil
}
try await Task.sleep(nanoseconds: UInt64(delay * 1E09))
return values[currentIndex]
}
public func makeAsyncIterator() -> AsyncArray {
self
}
}
One can force the unfolding type to work with an #Published by creating a buffer array that is checked repeatedly. The variable wouldn't actually need to be #Published anymore. This approach has a lot of problems but it can be made to work. If interested, I put it in a repo with a bunch of other AsyncStream examples. https://github.com/carlynorama/StreamPublisherTests
This article was very helpful to sorting this out: https://www.raywenderlich.com/34044359-asyncsequence-asyncstream-tutorial-for-ios
As was this video: https://www.youtube.com/watch?v=UwwKJLrg_0U

How to reduce the memory footprint of a large list of images in SwiftUI

I'm playing with SwiftUI and I'm currently struggling with images. Basically, I want to display an list of images with an infinite scroll but I want to keep the memory usage reasonable.
I have the following (truncated) code:
struct HomeView: View {
#State private var wallpapers: Loadable<[Wallpaper]> = .notRequested
#State private var currentPage = 1
#Environment(\.container) private var container
var body: some View {
content
.onAppear { loadWallpapers() }
}
private var content: some View {
VStack {
wallpapersList(data: wallpapers.value ?? [])
// ...
}
}
private func wallpapersList(data: [Wallpaper]) -> some View {
ScrollView {
LazyVStack(spacing: 5) {
ForEach(data) { w in
networkImage(url: w.thumbs.original)
.onAppear { loadNextPage(current: w.id) }
}
}
}
}
private func networkImage(url: String) -> some View {
// I use https://github.com/onevcat/Kingfisher to handle image loading
KFImage(URL(string: url))
// ...
}
private func loadWallpapers() {
container.interactors.wallpapers.load(data: $wallpapers, page: currentPage)
}
private func loadNextPage(current: String) {
// ...
}
}
struct WallpapersInteractor: PWallpapersInteractor {
let state: Store<AppState>
let agent: PWallpapersAgent
func load(data: LoadableSubject<[Wallpaper]>, page: Int) {
let store = CancelBag()
data.wrappedValue.setLoading(store: store)
Just.withErrorType((), Error.self)
.flatMap { _ in
agent.loadWallpapers(page: page) // network call here
}
.map { response in
response.data
}
.sink { subCompletion in
if case let .failure(error) = subCompletion {
data.wrappedValue.setFailed(error: error)
}
} receiveValue: {
if var currentWallpapers = data.wrappedValue.value {
currentWallpapers.append(contentsOf: $0) // /!\
data.wrappedValue.setLoaded(value: currentWallpapers)
} else {
data.wrappedValue.setLoaded(value: $0)
}
}
.store(in: store)
}
}
Because I append the new data to my Binding every time I request a new batch of images, the memory consumption quickly becomes stupidly high.
I tried to remove data from the array using .removeFirst(pageSize) once I get to the third page so that my array contains at most 2 * pageSize elements (pageSize being 64 in this case). But doing so makes my list all jumpy because the content goes up, which creates more problems than it solves.
I tried searching for a solution but I surprisingly didn't find anything on this particular topic, am I missing something obvious ?

SwiftUI Async data fetch in onAppear

I have class getDataFromDatabase which has func readData() thats read data from Firebase.
class getDataFromDatabase : ObservableObject {
var arrayWithQuantity = [Int]()
var arrayWithTime = [Double]()
func readData(completion: #escaping(_ getArray: Array<Int>?,_ getArray: Array<Double>?) -> Void) {
let db = Firestore.firestore()
db.collection("amounts").getDocuments { (querySnapshot, err) in
if let e = err{
print("There's any errors: \(e)")
}
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in querySnapshot!.documents{
let quantityFromDb = i.get("amount") as! Int
let timeFromDb = i.get("averageTimeRecognition") as! Double
self.arrayWithQuantity.append(quantityFromDb)
self.arrayWithTime.append(timeFromDb)
}
completion(self.arrayWithQuantity, self.arrayWithTime)
}
}
}
I use func readData() in onAppear:
struct CheckDatabaseView: View {
#State private var quantityFromDatabase: Array<Int> = []
#State private var timeFromDatabase: Array<Double> = []
#State private var flowersName: Array<String> = ["Carnation", "Daisy", "Hyacinth", "Iris", "Magnolia", "Orchid", "Poppy", "Rose", "Sunflower", "Tulip"]
#State private var isReady: Bool = false
var body: some View {
ScrollView(.vertical, showsIndicators: false){
ZStack(alignment: .top){
VStack(spacing: 40){
Text("Hello, world!")
// BarView(value: CGFloat(timeFromDatabase[0]), name: flowersName[0])
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
}
.navigationBarTitle(Text("Your datas in database").foregroundColor(.blue), displayMode: .inline)
.onAppear{
let gd = getDataFromDatabase()
gd.readData { (quantity, time) in
self.quantityFromDatabase = quantity!
self.timeFromDatabase = time!
}
}
}
}
I cannot use values self.quantityFromDatabase and self.timeFromDatabase because are empty. I know the problem is with the asynchronous retrieval of data. I've tried with DispatchQueue.main.async, but I still not get these values. How is the other method to get it? I need this values, because I want to draw charts in VStack (the comment line there).
EDIT
As #Rexhin Hoxha wrote below, i modified the code but i am not sure if the way is correct. I changed var arrayWithQuantity = [Int]() and var arrayWithTime = [Double]() by adding #Published in class getDataFromDatabase (now it's GetDataFromDatabaseViewModel):
class GetDataFromDatabaseViewModel : ObservableObject {
#Published var arrayWithQuantity = [Int]()
#Published var arrayWithTime = [Double]()
func readData() {
let db = Firestore.firestore()
db.collection("amounts").getDocuments { (querySnapshot, err) in
if let e = err{
print("There's any errors: \(e)")
}
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in querySnapshot!.documents{
let quantityFromDb = i.get("amount") as! Int
let timeFromDb = i.get("averageTimeRecognition") as! Double
self.arrayWithQuantity.append(quantityFromDb)
self.arrayWithTime.append(timeFromDb)
}
print("Array with quantity: \(self.arrayWithQuantity.count)")
}
}
}
also in struct I initialized #ObservedObject var gd = GetDataFromDatabaseViewModel() and onAppear now looks like this:
.onAppear{
self.gd.readData()
print("Quantity after reading: \(self.gd.arrayWithQuantity.count)")
}
but print in onAppear still print an empty Array. Where did I do a mistake?
So the problem is in your completion handler. It returns before you retrieve the data.
Solution is to make your arrays #Published and read the data in real time from the view. You have to remove the completion handler.
Call the function on ‚onAppear()‘ and use #ObservedObject to bind to your ViewModel (getDataFromDatabase). This is how it’s done in SwiftUI.
Please capitalize the first letter and use something more generic like „YouViewName“ViewModel.
Your name is fine for a method/function but not for a Class

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