Post

Replies

Boosts

Views

Activity

Reply to Is it possible to call openWindow() in a navigationDestination in a NavigationStack?
After doing more research it appears that this is not possible. So I converted the navigation stack to a series of buttons. It works. Below is an example button and my App struct. Button { openWindow(id: "summary") } label: { Text("Summary") .font(Font.custom("Arial", size: 14.0)) .foregroundColor(Color.blue) .background(Color.clear) .padding(.leading, 35) } .focusable(false) .buttonStyle(.link) @main struct WindowGroupsApp: App { var body: some Scene { WindowGroup ("Home") { ContentView() .hostingWindowPosition(window: "Home") } Window("Summary", id: "summary") { SummaryView() .hostingWindowPosition(window: "Summary") } WindowGroup ("Table", id: "table", for: String.self) { $fundName in NavigationDestinationView(fundName: fundName!, numYears: 1) .hostingWindowPosition(window: "Table") } WindowGroup ("Chart", id: "chart1", for: String.self) { $fundName in CustomChartView(fundName: fundName!, numYears: 1) .hostingWindowPosition(window: "Chart") } WindowGroup ("Chart", id: "chart5", for: String.self) { $fundName in CustomChartView(fundName: fundName!, numYears: 5) .hostingWindowPosition(window: "Chart") } } }
Oct ’23
Reply to Set Content View size/position upon retuning from navigation stack destination view
I finally figured out what to do. I created 3 WindowGroups in @main. One for the home view, one for the table view and one for the chart view. I then simplified the position logic as well as added logic to account for each view (they are all of the different size). I manually determined the origin and size of all three windows and assign the appropriate values in an NSRect which is used in the setFrame function of NSWindow. The position logic is assigned to each WindowGroup in @main. All works. Below is the updated code. // @main code struct WindowGroupsApp: App { var body: some Scene { WindowGroup ("Home") { ContentView() .hostingWindowPosition(window: "Home") } WindowGroup ("Table", id: "table", for: String.self) { $fundName in TableView(fundName: fundName!) .hostingWindowPosition(window: "Table") } WindowGroup ("Chart", id: "chart", for: String.self) { $fundName in ChartView(fundName: fundName!) .hostingWindowPosition(window: "Chart") } } } // position logic extension View { func hostingWindowPosition(window: String) -> some View { modifier( WindowPositionModifier(window: window) ) } } private struct WindowPositionModifier: ViewModifier { let window: String func body(content: Content) -> some View { content.background( HostingWindowFinder { $0?.setPosition(window: window) } ) } } struct HostingWindowFinder: NSViewRepresentable { // the system calls the methods at appropriate times var callback: (NSWindow?) -> () func makeNSView(context: Self.Context) -> NSView { let view = NSView() DispatchQueue.main.async { self.callback(view.window) } return view } func updateNSView(_ nsView: NSView, context: Context) { DispatchQueue.main.async { self.callback(nsView.window) } } } extension NSWindow { func setPosition(window: String) { var nsRectangle: NSRect = NSRect(x: 100, y: 100, width: 100, height: 100) switch window { case "Home": nsRectangle = NSRect(x: 1055.0, y: 370.0, width: 450, height: 700) case "Table": nsRectangle = NSRect(x: 1055.0, y: 320.0, width: 450, height: 800) // 390 case "Chart": nsRectangle = NSRect(x: 680.0, y: 245.0, width: 1200, height: 950) default: print("problem in window logic set position") } setFrame(nsRectangle, display: true) } } // content view struct ContentView: View { @Environment (\.openWindow) private var openWindow var body: some View { VStack(alignment: .leading) { ZStack (alignment: .leading) { RoundedRectangle(cornerRadius: 10.0) .fill(Color.white) .frame(width: 200, height: 80) .padding(.leading, 20) VStack(alignment: .leading, spacing: 10) { Button { openWindow(id: "table", value: "Table1") } label: { Text("Table") .font(Font.custom("Arial", size: 14.0)) .foregroundColor(Color.blue) .background(Color.clear) .padding(.leading, 35) } .focusable(false) .buttonStyle(.link) Button { openWindow(id: "chart", value: "Chart1") } label: { Text("Chart - 1 Year") .font(Font.custom("Arial", size: 14.0)) .foregroundColor(Color.blue) .background(Color.clear) .padding(.leading, 35) } .focusable(false) .buttonStyle(.link) Button { openWindow(id: "chart", value: "Chart5") } label: { Text("Chart - 5 Years") .font(Font.custom("Arial", size: 14.0)) .foregroundColor(Color.blue) .background(Color.clear) .padding(.leading, 35) } .focusable(false) .buttonStyle(.link) } // end v stack } // end zstack } // end v stack .frame(width: 450, height: 600, alignment: .topLeading) } // end some view }
Oct ’23
Reply to Set Content View size/position upon retuning from navigation stack destination view
After some additional testing, discovered that I just have to associate the code with the home view (ContentView). With this in place the home view initially displays centered and the correct size. When I go to a destination view (both are larger) the view stays centered and its size adjusts correctly. But when I return to the home view. the size/position of the view does not change. To get the home view to be the correct width and height initially I set the default size in the initial call to ContentView (see code below). So it makes sense to me that I all I need to do upon returning to the home view is reset the default size. Unfortunately I have not yet figured out how to do that. If. someone has any ideas I am all ears. If I figure it out, I will post the solution. @main struct TableViewTestApp: App { var body: some Scene { WindowGroup { ContentView() .hostingWindowPosition( vertical: .center, horizontal: .center, screen: .main ) } .defaultSize(width: 450, height: 700) } } extension NSWindow { struct Position { static let defaultPadding: CGFloat = 16 var vertical: Vertical var horizontal: Horizontal var padding = Self.defaultPadding } } extension NSWindow.Position { enum Horizontal { case left, center, right } enum Vertical { case top, center, bottom } } extension View { func hostingWindowPosition( vertical: NSWindow.Position.Vertical, horizontal: NSWindow.Position.Horizontal, padding: CGFloat = NSWindow.Position.defaultPadding, screen: NSScreen? = nil ) -> some View { modifier( WindowPositionModifier( position: NSWindow.Position( vertical: vertical, horizontal: horizontal, padding: padding ), screen: screen ) ) } } private struct WindowPositionModifier: ViewModifier { let position: NSWindow.Position let screen: NSScreen? func body(content: Content) -> some View { content.background( HostingWindowFinder { $0.setPosition(position, in: screen) } ) } } private struct HostingWindowFinder: NSViewRepresentable { var callback: (NSWindow) -> () func makeNSView(context: Self.Context) -> NSView { let view = BridgingView() view.callback = callback return view } func updateNSView(_ nsView: NSView, context: Context) {} } private class BridgingView: NSView { var callback: ((NSWindow) -> ())? override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) if let window = window { callback?(window) } } } extension NSWindow { func setPosition(_ position: Position, in screen: NSScreen?) { guard let visibleFrame = (screen ?? self.screen)?.visibleFrame else { return } let origin = position.value(forWindow: frame, inScreen: visibleFrame) setFrameOrigin(origin) } } extension NSWindow.Position { func value(forWindow windowRect: CGRect, inScreen screenRect: CGRect) -> CGPoint { let xPosition = horizontal.valueFor( screenRange: screenRect.minX..<screenRect.maxX, width: windowRect.width, padding: padding ) let yPosition = vertical.valueFor( screenRange: screenRect.minY..<screenRect.maxY, height: windowRect.height, padding: padding ) return CGPoint(x: xPosition, y: yPosition) } } extension NSWindow.Position.Horizontal { func valueFor( screenRange: Range<CGFloat>, width: CGFloat, padding: CGFloat) -> CGFloat { switch self { case .left: return screenRange.lowerBound + padding case .center: return (screenRange.upperBound + screenRange.lowerBound - width) / 2 case .right: return screenRange.upperBound - width - padding } } } extension NSWindow.Position.Vertical { func valueFor( screenRange: Range<CGFloat>, height: CGFloat, padding: CGFloat) -> CGFloat { switch self { case .top: return screenRange.upperBound - height - padding case .center: return (screenRange.upperBound + screenRange.lowerBound - height) / 2 case .bottom: return screenRange.lowerBound + padding } } }
Sep ’23
Reply to Set Content View size/position upon retuning from navigation stack destination view
I updated the HostingWindowFinder*** struct (where *** is replaced with Home, Table or Chart). With this code the home view (content view) is correctly positioned and the right size. When I select a destination view (chart or table) it appears as if the origin of the home view does not change and that the window size increases to just fit the table or the chart. When I return to the Content View it goes back to the original size/position. When I step through the code, the setPosition function for the chart view for example is executed but it does not appear that the setFrame call is doing anything. I am clearly misunderstanding something. Probably something very obvious. Below is the updated HostingWindowFinderHome code. private struct HostingWindowFinderHome: NSViewRepresentable { var callback: (NSWindow) -> () func makeNSView(context: Self.Context) -> NSView { let view = BridgingViewHome() view.callback = callback return view } func updateNSView(_ nsView: NSView, context: Context) {} } private class BridgingViewHome: NSView { var callback: ((NSWindow) -> ())? override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) if let window = window { callback?(window) } } }
Sep ’23
Reply to Chart - Last (right most) x axis label not being displayed
After some additional research, it turns out that there are several preset styles for the Axis Marks. The aligned style is what I needed. I added that preset to the Axis Marks contained in .chartXAxis section of code. Problem solved. Below is a pic of the updated x axis labels and the updated code for .chartXAxis. .chartXAxis { AxisMarks(preset: .aligned, values: xAxisValues) { value in if let date = value.as(Date.self) { AxisValueLabel(horizontalSpacing: -14, verticalSpacing: 10) { VStack(alignment: .leading) { Text(ChartMonthFormatter.string(from: date)) .font(.custom("Arial", size: 14)) Text(ChartYearFormatter.string(from: date)) .font(.custom("Arial", size: 14)) } // end v stack } // end axis label } // end if statement AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1)) .foregroundStyle(Color.black) AxisTick(centered: true, length: 0, stroke: .none) } } // end chart x axis .chartXScale(domain: [xAxisValues[0], xAxisValues[12]])
Sep ’23
Reply to Problem with alignment guides
To utilize an alignment guide it appears that the parent container must indicate the alignment type listed in the guide. Other wise the alignment guide is not called. So I removed all of the alignments and alignment guides. Then stepped down thru the views starting with the outermost. At the outmost view and each subview I set the alignment to leading. As a result, if I set the alignment guide to be HorizontalAlignment.leading, the associated closure is run and the guide is applied. I should note that I had to insert a V Stack directly inside the Natigation Stack to get things to work, and of course set its alignment to leading. I have not yet determined how to replace the spacers with vertical alignment guides but will work on that next. Below is a view of the resutling output and the associated code. The code for sections "Group 2" and "Group 3" are essentially identical to that for "Group 1", so I did not include them. In addition, the Navigation Destination did not change so it was also not included. struct ContentView: View { var body: some View { NavigationStack { VStack (alignment: HorizontalAlignment.leading) { Text("Group1") .font(Font.custom("Arial", size: 16)) .alignmentGuide(HorizontalAlignment.leading, computeValue: { viewDimensions in return viewDimensions[HorizontalAlignment.leading] - 42 }) ZStack (alignment: Alignment(horizontal: .leading, vertical: .center)) { RoundedRectangle(cornerRadius: 10) .fill(.white) .alignmentGuide(HorizontalAlignment.leading, computeValue: { viewDimensions in return viewDimensions[HorizontalAlignment.leading] - 30 }) VStack (alignment: HorizontalAlignment.leading) { NavigationLink(value: "vooTable") { Text("Data Table") .alignmentGuide(HorizontalAlignment.leading, computeValue: { viewDimensions in return viewDimensions[HorizontalAlignment.leading] - 55 }) } .font(Font.custom("Arial", size: 14)) .buttonStyle(.link) .underline() .focusable(false) } // end vstack } // end zStack .frame(width: 200, height: 60, alignment: .leading) Spacer() .frame(width: 200, height: 25, alignment: .leading) } // end navigation destination } // end v stack } // end navigtion stack .frame(width: 750, height: 550, alignment: Alignment(horizontal: .leading, vertical: .center)) }
Sep ’23
Reply to When creating an instance of a custom NSTableCellView how do pass in a value
The problem appears to be that once I create an instance of CustomTableCellView, the variable nsRectangle is assigned a value based on the current rectWidth value in the override init. And of course your can only override a pre-existing init, none of which include rectWidth. So changing the value of rectWidth after the instance has been created appears to be of no value since the init is only run once. My solution was to pull all of the logic out of the CustomTableCellView init and place it into a function in CustomTableCellView. Then I can create an instance of CustomTableCellView in my Coordinators delegate, which only runs a call to super.init(). On the following line I call the function, named Setup(), to which I pass in rectWidth. The side benefit is this allowed me to simplify the Coordinators delegate code. Below are the updated delegate and CustomTableCellView code. Claude31, thank you for your response. func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let dataCell = CustomTableCellView() if (tableColumn?.identifier)!.rawValue == "fund" { dataCell.Setup(rectWidth: 100.0) dataCell.textField?.stringValue = closingValues[row].fundName } else if (tableColumn?.identifier)!.rawValue == "date" { dataCell.Setup(rectWidth: 120.0) dataCell.textField?.stringValue = SQLDateFormatter.string(from: closingValues[row].timeStamp) } else { dataCell.Setup(rectWidth: 100.0) dataCell.textField?.stringValue = String(format: "$%.2f", closingValues[row].close) } return dataCell } class CustomTableCellView: NSTableCellView { override init(frame frameRect: NSRect) { super.init(frame: frameRect) } func Setup(rectWidth: CGFloat) { self.autoresizingMask = .width let nsRectangle = NSMakeRect(0, 0, rectWidth, 24) let customTextField: NSTextField = CustomTextField(frame: nsRectangle) self.textField = customTextField self.addSubview(customTextField) } required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
Aug ’23
Reply to How do I modify height of tableview header cell background color
The solution is to create a subclass of NSTableHeaderCell. In the subclass you override several functions and insert customizations as desired in the drawInterior function. Below is the call to and subclass created. // in func makeNSView let customHeaderCell0 = CustomHeaderCell() customHeaderCell0.stringValue = "Stock" let column0Header = tableView.tableColumns[0] column0Header.minWidth = 90.0 column0Header.headerCell = customHeaderCell0 // subclass final class CustomHeaderCell: NSTableHeaderCell { override init(textCell: String) { super.init(textCell: textCell) } required init(coder: NSCoder) { fatalError("init(coder:) not implemented") } override func draw(withFrame cellFrame: NSRect, in controlView: NSView) { self.drawInterior(withFrame: cellFrame, in: controlView) } override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) { let rect = NSRect(x: cellFrame.origin.x, y: cellFrame.origin.y, width: cellFrame.size.width, height: cellFrame.size.height) NSColor.systemMint.set() NSBezierPath(rect: rect).fill() let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .center let headerCellText = NSAttributedString(string: stringValue, attributes: [NSAttributedString.Key.foregroundColor: NSColor.black, NSAttributedString.Key.font: NSFont(name: "Arial", size: 18)!, NSAttributedString.Key.paragraphStyle: paragraphStyle, NSAttributedString.Key.baselineOffset: -6.0 ]) headerCellText.draw(in: cellFrame) } }
Aug ’23
Reply to NSTableView - How do I remove the horizontal gap between table cells
After some additional review of the NSTableView class, I discovered it contains a var "intercellSpacing" of type NSSize. So I added the line of code below to the "makeNSView" function above and the cell spacing problem is solved. It did not though get rid of the green block to the left of "Stock" or to the right of "Closing Value". If I am able to work that out I will add the solution to this post. tableView.intercellSpacing = NSSize(width: 0.0, height: 0.0)
Aug ’23
Reply to Updating displayed data generates reentrant warning
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 }) } }
Aug ’23
Reply to Convert SQLite Julian date to a specific date string format for display
Great solution. It makes the query simpler and as you indicated, only requires some simple math to convert between Julian Days and Apples seconds. And yes you are absolutely right that it was a mistake for me to declare a date formatter in a loop. Below is the updated code. func AccessSQLiteData(db: OpaquePointer?) { let queryTradingDaysStatement = """ WITH TempTable1 AS ( SELECT max(TimeStamp) - 365.25 as StartingDate FROM TradingDays WHERE FundName = 'Fund1' ) SELECT main.FundName, main.TimeStamp, main.Close FROM TradingDays as main, TempTable1 as temp WHERE main.FundName = 'Fund1' AND main.TimeStamp >= temp.StartingDate ORDER By main.TimeStamp ASC ; """ let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium dateFormatter.timeZone = .gmt let daysBetween4713And2001: Double = 2451910.500000 let secondsPerDay: Double = 86400.00 var queryTradingDaysCPtr: OpaquePointer? if sqlite3_prepare_v2(db, queryTradingDaysStatement, -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) let formattedTimeStamp = dateFormatter.string(from: timeStamp) let closeAsString = String(format: "$%.2f", close) print(fundNameAsString + " - " + formattedTimeStamp + " - " + closeAsString) } // end while loop } else { let errorMessage = String(cString: sqlite3_errmsg(db)) print("\nQuery is not prepared \(errorMessage)") } sqlite3_finalize(queryTradingDaysCPtr) }
Aug ’23
Reply to SQLite - Select statement to extract 1 year of data
Your subquery works but using your suggestion of a With clause appears to me to be the perfect solution. It allows me to get the latest date of available data for a given fund and calculate one year earlier which was my goal. Even better, it is easy to follow. Below is the updated query using a With clause. It works. Thank you. let queryTradingDaysStatement = """ WITH TempTable1 AS ( SELECT max(TimeStamp) as LatestDate FROM TradingDays WHERE FundName = '\(fundName)' ), TempTable2 AS ( SELECT date(LatestDate, 'start of day', '-\(numYears) year') as StartingDate FROM TempTable1 ) SELECT FundName, TimeStamp, Close FROM TradingDays main, TempTable2 temp WHERE main.FundName = '\(fundName)' AND main.TimeStamp >= temp.StartingDate ORDER BY main.TimeStamp ASC ; """
Jul ’23
Reply to SQLite - Select statement to extract 1 year of data
Turns out that SQLite supports a case statement. So I incorporated that and strftime into my query to adjust the date back to Friday if the current date is Saturday '6' or Sunday '0'. Works. let queryTradingDaysStatement = """ Select FundName, TimeStamp, Close FROM TradingDays WHERE FundName = '\(fundName)' AND CASE strftime('%w') WHEN '0' then TimeStamp >= date('now', '-2 days', 'start of day', '-\(numYears) year') WHEN '6' then TimeStamp >= date('now', '-1 days','start of day', '-\(numYears) year') ELSE TimeStamp >= date('now', 'start of day', '-\(numYears) year') END ORDER By TimeStamp ASC ; """
Jul ’23