A library that provides boilerplate for an Xcode-like macOS app UI.
Some behaviour and code extracted from AuroraEditor.
- Customisable navigation sidebar with pages
- Customisable inspector sidebar with pages
- Tabular main content
- Tab Bar
- Custmisable using
TabBarID
andTabBarRepresentable
protocols - Drag to rearrange
- Resize/scroll on tab excess
- Animation on close/open/scroll
- Custmisable using
- Customisable toolbar
- Easy-to-use OutlineView template
- Settings Page
- About Page
- Window and Sidebar behaviour:
MainWindowController
: A Window Controller that creates the basic UINavigatorProtocol
: A protocol which defines the behaviour of the Navigator sidebar (on the left)InspectorProtocol
: A protocol which defines the behaviour of the Inspector sidebar (on the right)WorkspaceProtocol
: A protocol which defines the behaviour of the main tab content (in the center)
- Outline Views:
OutlineViewController
: An NSViewController that controls an NSOutlineView in an NSScrollViewOutlineElement
: A protocol for an item inOutlineViewController
, for use inStandardTableViewCell
StandardTableViewCell
: A class for a row inOutlineViewController
. Intended to be subclassed.
- Tab Bar:
TabBarID
: A protocol that includes an ID. Intended to be implemented on an Enum, so that each tab has an assignedcase
.TabBarRepresentable
: A protocol that provides information like the title and icon to show for a tab. Intended to be implemented on a class or struct.
See the example repo for reference
- Create a class conforming to
NavigatorProtocol
orInspectorProtocol
. Add new pages by creating newSidebarDockIcon
instances innavigatorItems
orinspectorItems
like so:
.init(imageName: "symbolName", title: "hoverTitle", id: 0)
- Override the
viewForNavigationSidebar(selection: Int) -> AnyView
orviewForInspectorSidebar(selection: Int) -> AnyView
functions. UseMainContentWrapper
to wrap your View so that it is formatted properly, eg:
func viewForNavigationSidebar(selection: Int) -> AnyView {
MainContentWrapper {
switch selection {
case 0:
// things to show on page 0
Text("PAGE ZERO")
// add more cases as needed
default:
Text("Not Implemented Yet")
}
}
}
Add an extra case to the selection
switch statement (the number you use is the id
of your SidebarDockIcon
). Put your view there, and it will be visible when that tab is
selected.
- Override the default
NavigatorProtocol
in yourMainWindowController
subclass by overriding thefunc getNavigatorProtocol() -> any NavigatorProtocol
orfunc getInspectorProtocol() -> any InspectorProtocol
function, eg:
override func getNavigatorProtocol() -> any NavigatorProtocol {
return MyNavigatorProtocol()
}
- Create an enum conforming to
TabBarItemID
. You should ONLY MAKE ONE in the entire project, and add cases for other tabs - Add a case to your enum. This case can hold information (eg. by using
case myCase(String)
to hold a string). - To store data for the tab type, create a class conforming to
TabBarItemRepresentable
. See thetest
case and theTestElement
class as an example in the example repo
- To open a new tab, use the
openTab
function of theTabManager
EnvironmentObject, available in all SwiftUI subviews of the sidebars and workspace view. It is also accessible via theMainWindowController
subclass instance astabManager
. - Similarly, use the
closeTab
function to close your tab.
- Create a class conforming to
WorkspaceProtocol
- Implement the
viewForTab(tab: TabBarItemID) -> AnyView
function. You can use the following as a base:
func viewForTab(tab: TabBarItemID) -> AnyView {
MainContentWrapper {
if let tab = tab as? MyTabBarItemID {
switch tab {
case .myCase(let string):
// things to show on page 0
Text("My Case: \(string)")
// add more cases as needed
default:
Text("Not Implemented Yet")
}
}
}
}
- Override the default
WorkspaceProtocol
in yourMainViewController
subclass by overriding thefuncc getWorkspaceProtocol() -> any WorkspaceProtocol
function, eg:
override func getWorkspaceProtocol() -> any WorkspaceProtocol {
return MyWorkspaceProtocol()
}
- Create a subclass of
OutlineElement
to hold the information in each view in the OutlineView - Create a subclass of the
StandardTableViewCell
- Create a subclass of
OutlineViewController
. You NEED to override the following functions:
loadContent()
, which returns an array of anOutlineElement
outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any)
, which returns an NSView for a given element. This will be your custom instance ofStandardTableViewCell
.
- Use
OutlineView
if you plan to use your custom outline view within SwiftUI.
See TestElement
, TestOutlineViewController
, and TestTableViewCell
for an example in the example repo
Override the toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier]
,
toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier]
, and
open func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem?
functions in your MainWindowController
subclass.
Extend NSToolbarItem.Identifier
to add custom identifiers. See the apple developer documentation for more details
Remember that you can use defaultLeadingItems()
and defaultTrailingItems
to get the default leading and trailing items, which are the toolbar items
for the sidebar show/hide buttons. You can also use the builtinDefaultToolbar( _ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool ) -> NSToolbarItem?
function to get the NSToolbarItem
for a given identifier, or nil if it is not a toolbar item that MainWindowController implements.