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")
}
}
}
Post
Replies
Boosts
Views
Activity
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
}
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
}
}
}
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)
}
}
}
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]])
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))
}
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")
}
}
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)
}
}
To solve the final problem, the borders (green blocks) on the left and right of the table, I inserted the code "tableView.style = ..plain".
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)
My apologies.
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
})
}
}
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)
}
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
;
"""
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
;
"""