Updating displayed data generates reentrant warning

I have a program that displays data in a table in the main view. It gets this data by running a C API Select statement. On the main view there is also a button which allows me to change the sort order to descending. To do so, this button calls the same function initially called to display data but with a different order by string for the Select statement. Selecting the button causes the data to be displayed in descending order and it is very fast. Unfortunately selecting the button also causes a warning to be displayed in Xcode "Application performed a reentrant operation in its NSTableView delegate". I do not understand what to do to resolve the issue. Any assistance will be appreciated. Below is the Context View and the View Model. The QueryDatabase function called in the view model is just the C api calls including Prepare, Step in a while loop and Finalize. If it would be helpful I can add it.

// Main View:
struct ContentView: View {
    @ObservedObject var vm: SQLiteViewModel = SQLiteViewModel()
    var body: some View {
        if vm.loadingData == true {
            ProgressView()
                .task {
                    vm.GetSQLData(fundName: "Fund1", numYears: 1, sortOrder: "main.TimeStamp ASC")
                }
        } else {
            HStack {
                Spacer()
                    .frame(width: 135, height: 200, alignment: .center)
                Table(vm.fundData) {
                    TableColumn("Fund") { record in
                        Text(record.fundName)
                            .frame(width: 60, height: 15, alignment: .center)
                    }
                    .width(60)
                    TableColumn("Date") { record  in
                        Text(sqlDateFormatter.string(from: record.timeStamp))
                            .frame(width: 120, height: 15, alignment: .center)
                    }
                    .width(120)
                    TableColumn("Close") { record in
                        Text(String(format: "$%.2f", record.close))
                            .frame(width: 65, height: 15, alignment: .trailing)
                    }
                    .width(65)
                } // end table
                .font(.system(size: 14, weight: .regular, design: .monospaced))
                .frame(width: 330, height: 565, alignment: .center)
                Spacer()
                    .frame(width: 30, height: 30, alignment: .center)
                VStack {
                    Button(action: {
                        vm.GetSQLData(fundName: "Fund1", numYears: 1, sortOrder: "main.TimeStamp DESC")
                    }) {
                        Text("Date Descending")
                    } // end button
                    .frame(width: 140)
                } // end v stack
                .font(.system(size: 12, weight: .regular, design: .monospaced))

            } // end horizontal stack
            .frame(width: 600, height: 600, alignment: .center)
        } // end else statement
    } // end view
}

// View Model:
class SQLiteViewModel: ObservableObject {
    var db: OpaquePointer?
    @Published var fundData: [TradingDay] = []
    @Published var loadingData: Bool = true
    init() {
        db = OpenDatabase()
    }
    func GetSQLData(fundName: String, numYears: Int, sortOrder: String) {
        DispatchQueue.main.async {
            self.fundData = QueryDatabase(db: self.db, fundName: fundName, numYears: numYears, sortOrder: sortOrder)
            self.loadingData = false
        }
    }
}

Accepted Reply

It appears that the problem is related to displaying the table before the variable it is based on, fundData, is updated / populated. I solved the problem as follows. 1) I moved the initial sql query from the task tied to Progress View in the main view to the view models init (I could have probably just added isLoading = true to the ProgressView task). This along with the isLoading variable assure that initially fundData is populated with data before the table is displayed. 2) I added isLoading = true to the button action. This once again assures that that fundData is updated before the table is displayed (fundData and isLoading are updated on the main actor at the end of the view models function). Along with these two changes I pulled QueryDatabase into the view model but it is not part of the solution. It just seemed cleaner. Oh, and I made QueryDatabase (function in view model) async since I changed from Dispatch.main.async to MainActor(run:. I do not think this is part of the solution. Below is the updated main view and view model. The button works and it is very fast.

// main view
struct ContentView: View {
    @ObservedObject var vm: SQLiteViewModel = SQLiteViewModel()
    var body: some View {
        if vm.isLoading == true {
            ProgressView()
        } else {
            HStack {
                Spacer()
                    .frame(width: 135, height: 200, alignment: .center)
                Table(vm.fundData) {
                    TableColumn("Fund") { record in
                        Text(record.fundName)
                            .frame(width: 60, height: 15, alignment: .center)
                    }
                    .width(60)
                    TableColumn("Date") { record  in
                        Text(sqlDateFormatter.string(from: record.timeStamp))
                            .frame(width: 120, height: 15, alignment: .center)
                    }
                    .width(120)
                    TableColumn("Close") { record in
                        Text(String(format: "$%.2f", record.close))
                            .frame(width: 65, height: 15, alignment: .trailing)
                    }
                    .width(65)
                } // end table
                .font(.system(size: 14, weight: .regular, design: .monospaced))
                .frame(width: 330, height: 565, alignment: .center)
                Spacer()
                    .frame(width: 30, height: 30, alignment: .center)
                VStack {
                    Button(action: {
                        Task {
                            vm.isLoading = true
                            await vm.QueryDatabase(fundName: "Fund1", numYears: 1, sortOrder: "main.TimeStamp DESC")
                        }
                    }) {
                        Text("Date Descending")
                    } // end button
                    .frame(width: 140)

                } // end v stack
                .font(.system(size: 12, weight: .regular, design: .monospaced))
            } // end horizontal stack
            .frame(width: 600, height: 600, alignment: .center)
        }
    } // end view
}

// view model
class SQLiteViewModel: ObservableObject {
    
    var db: OpaquePointer?
    @Published var fundData: [TradingDay] = []
    @Published var isLoading: Bool = true
    var tempTradingDays: [TradingDay] = []
    
    init() {
        db = OpenDatabase()
        Task {
            await QueryDatabase(fundName: "Fund1", numYears: 1, sortOrder: "main.TimeStamp ASC")
        }
    }
    func QueryDatabase(fundName: String, numYears: Int, sortOrder: String) async {
        
        tempTradingDays = []
        let daysBetween4713And2001: Double = 2451910.500000
        let secondsPerDay: Double = 86400.00
        var queryTradingDaysCPtr: OpaquePointer?
        sqlite3_exec(db, SQLStmts.beginTransaction, nil, nil, nil);
        if sqlite3_prepare_v2(db, SQLStmts.QueryTradingDays(fundName: fundName, numYears: numYears, sortOrder: sortOrder), -1, &queryTradingDaysCPtr, nil) == SQLITE_OK {
            
            while (sqlite3_step(queryTradingDaysCPtr) == SQLITE_ROW) {
                let fundName = sqlite3_column_text(queryTradingDaysCPtr, 0)
                let daysSince4713BC = sqlite3_column_double(queryTradingDaysCPtr, 1)
                let close = sqlite3_column_double(queryTradingDaysCPtr, 2)
                
                let fundNameAsString = String(cString: fundName!)
                let daysSinceJanOne2001 = daysSince4713BC - daysBetween4713And2001
                let secondsSinceJanOne2001 = daysSinceJanOne2001 * secondsPerDay
                let timeStamp = Date(timeIntervalSinceReferenceDate: secondsSinceJanOne2001)

                var tempTradingDay: TradingDay = TradingDay(fundName: "", timeStamp: Date(), close: 0.0)
                tempTradingDay.fundName = fundNameAsString
                tempTradingDay.timeStamp = timeStamp
                tempTradingDay.close = close
                tempTradingDays.append(tempTradingDay)
            } // end while loop
        } else {
            let errorMessage = String(cString: sqlite3_errmsg(db))
            print("\nQuery is not prepared \(errorMessage)")
        }
        sqlite3_finalize(queryTradingDaysCPtr)
        sqlite3_exec(db, SQLStmts.commitTransaction, nil, nil, nil);
        
        await MainActor.run(body: {
            self.fundData = self.tempTradingDays
            self.isLoading = false
        })
    }
}
  • I noticed that occasionally when I selected the button, the table would flash. To eliminate the problem I removed vm.isLoading = true from the button action and added it to the MainActor call in the view model. This stopped the occasional flashing.

Add a Comment

Replies

It appears that the problem is related to displaying the table before the variable it is based on, fundData, is updated / populated. I solved the problem as follows. 1) I moved the initial sql query from the task tied to Progress View in the main view to the view models init (I could have probably just added isLoading = true to the ProgressView task). This along with the isLoading variable assure that initially fundData is populated with data before the table is displayed. 2) I added isLoading = true to the button action. This once again assures that that fundData is updated before the table is displayed (fundData and isLoading are updated on the main actor at the end of the view models function). Along with these two changes I pulled QueryDatabase into the view model but it is not part of the solution. It just seemed cleaner. Oh, and I made QueryDatabase (function in view model) async since I changed from Dispatch.main.async to MainActor(run:. I do not think this is part of the solution. Below is the updated main view and view model. The button works and it is very fast.

// main view
struct ContentView: View {
    @ObservedObject var vm: SQLiteViewModel = SQLiteViewModel()
    var body: some View {
        if vm.isLoading == true {
            ProgressView()
        } else {
            HStack {
                Spacer()
                    .frame(width: 135, height: 200, alignment: .center)
                Table(vm.fundData) {
                    TableColumn("Fund") { record in
                        Text(record.fundName)
                            .frame(width: 60, height: 15, alignment: .center)
                    }
                    .width(60)
                    TableColumn("Date") { record  in
                        Text(sqlDateFormatter.string(from: record.timeStamp))
                            .frame(width: 120, height: 15, alignment: .center)
                    }
                    .width(120)
                    TableColumn("Close") { record in
                        Text(String(format: "$%.2f", record.close))
                            .frame(width: 65, height: 15, alignment: .trailing)
                    }
                    .width(65)
                } // end table
                .font(.system(size: 14, weight: .regular, design: .monospaced))
                .frame(width: 330, height: 565, alignment: .center)
                Spacer()
                    .frame(width: 30, height: 30, alignment: .center)
                VStack {
                    Button(action: {
                        Task {
                            vm.isLoading = true
                            await vm.QueryDatabase(fundName: "Fund1", numYears: 1, sortOrder: "main.TimeStamp DESC")
                        }
                    }) {
                        Text("Date Descending")
                    } // end button
                    .frame(width: 140)

                } // end v stack
                .font(.system(size: 12, weight: .regular, design: .monospaced))
            } // end horizontal stack
            .frame(width: 600, height: 600, alignment: .center)
        }
    } // end view
}

// view model
class SQLiteViewModel: ObservableObject {
    
    var db: OpaquePointer?
    @Published var fundData: [TradingDay] = []
    @Published var isLoading: Bool = true
    var tempTradingDays: [TradingDay] = []
    
    init() {
        db = OpenDatabase()
        Task {
            await QueryDatabase(fundName: "Fund1", numYears: 1, sortOrder: "main.TimeStamp ASC")
        }
    }
    func QueryDatabase(fundName: String, numYears: Int, sortOrder: String) async {
        
        tempTradingDays = []
        let daysBetween4713And2001: Double = 2451910.500000
        let secondsPerDay: Double = 86400.00
        var queryTradingDaysCPtr: OpaquePointer?
        sqlite3_exec(db, SQLStmts.beginTransaction, nil, nil, nil);
        if sqlite3_prepare_v2(db, SQLStmts.QueryTradingDays(fundName: fundName, numYears: numYears, sortOrder: sortOrder), -1, &queryTradingDaysCPtr, nil) == SQLITE_OK {
            
            while (sqlite3_step(queryTradingDaysCPtr) == SQLITE_ROW) {
                let fundName = sqlite3_column_text(queryTradingDaysCPtr, 0)
                let daysSince4713BC = sqlite3_column_double(queryTradingDaysCPtr, 1)
                let close = sqlite3_column_double(queryTradingDaysCPtr, 2)
                
                let fundNameAsString = String(cString: fundName!)
                let daysSinceJanOne2001 = daysSince4713BC - daysBetween4713And2001
                let secondsSinceJanOne2001 = daysSinceJanOne2001 * secondsPerDay
                let timeStamp = Date(timeIntervalSinceReferenceDate: secondsSinceJanOne2001)

                var tempTradingDay: TradingDay = TradingDay(fundName: "", timeStamp: Date(), close: 0.0)
                tempTradingDay.fundName = fundNameAsString
                tempTradingDay.timeStamp = timeStamp
                tempTradingDay.close = close
                tempTradingDays.append(tempTradingDay)
            } // end while loop
        } else {
            let errorMessage = String(cString: sqlite3_errmsg(db))
            print("\nQuery is not prepared \(errorMessage)")
        }
        sqlite3_finalize(queryTradingDaysCPtr)
        sqlite3_exec(db, SQLStmts.commitTransaction, nil, nil, nil);
        
        await MainActor.run(body: {
            self.fundData = self.tempTradingDays
            self.isLoading = false
        })
    }
}
  • I noticed that occasionally when I selected the button, the table would flash. To eliminate the problem I removed vm.isLoading = true from the button action and added it to the MainActor call in the view model. This stopped the occasional flashing.

Add a Comment