{"id":21033319,"url":"https://github.com/sameesunkaria/outlineview","last_synced_at":"2025-06-19T17:39:29.461Z","repository":{"id":47530516,"uuid":"316931395","full_name":"Sameesunkaria/OutlineView","owner":"Sameesunkaria","description":"OutlineView for SwiftUI on macOS","archived":false,"fork":false,"pushed_at":"2023-04-01T17:07:15.000Z","size":298,"stargazers_count":73,"open_issues_count":7,"forks_count":8,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-15T13:43:47.362Z","etag":null,"topics":["appkit","cocoa","macos","swift","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Sameesunkaria.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-11-29T11:01:13.000Z","updated_at":"2025-05-14T14:05:07.000Z","dependencies_parsed_at":"2024-11-19T12:54:16.413Z","dependency_job_id":"55a2481e-27fb-44ac-811b-eccf1feb2922","html_url":"https://github.com/Sameesunkaria/OutlineView","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/Sameesunkaria/OutlineView","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Sameesunkaria%2FOutlineView","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Sameesunkaria%2FOutlineView/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Sameesunkaria%2FOutlineView/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Sameesunkaria%2FOutlineView/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Sameesunkaria","download_url":"https://codeload.github.com/Sameesunkaria/OutlineView/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Sameesunkaria%2FOutlineView/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260797653,"owners_count":23064830,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["appkit","cocoa","macos","swift","swiftui"],"created_at":"2024-11-19T12:54:09.260Z","updated_at":"2025-06-19T17:39:24.450Z","avatar_url":"https://github.com/Sameesunkaria.png","language":"Swift","readme":"# OutlineView for SwiftUI on macOS\n\n`OutlineView` is a SwiftUI view for macOS, which allows you to display hierarchical visual layouts (like directories and files) that can be expanded and collapsed. \nIt provides a convenient wrapper around AppKit's `NSOutlineView`, similar to SwiftUI's `OutlineGroup` embedded in a `List` or a `List` with children. `OutlineView` provides it's own scroll view and doesn't have to be embedded in a `List`.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg width=\"606\" alt=\"Screenshot\" src=\"Examples/Screenshot.png\"\u003e\n\u003c/p\u003e\n\n## Installation\n\nYou can install the `OutlineView` package using SwiftPM.\n\n```\nhttps://github.com/Sameesunkaria/OutlineView.git\n```\n\n## Usage\n\nThe API of the `OutlineView` is similar to the native SwiftUI `List` with children. However, there is one notable difference; `OutlineView` requires you to provide an `NSView` (preferably an `NSTableCellView`) as the content view. This API decision is discussed in the [caveats](#Caveats) section.\n\nIn the following example, a tree structure of `FileItem` data offers a simplified view of a file system. Passing a sequence of root elements of this tree and the key path of its children allows you to quickly create a visual representation of the file system.\n\nA macOS app demonstrating this example can be found in the `Example` directory.\n\n```swift\nstruct FileItem: Hashable, Identifiable, CustomStringConvertible {\n  // Each item in the hierarchy should be uniquely identified.\n  var id = UUID()\n  \n  var name: String\n  var children: [FileItem]? = nil\n  var description: String {\n    switch children {\n    case nil:\n      return \"📄 \\(name)\"\n    case .some(let children):\n      return children.isEmpty ? \"📂 \\(name)\" : \"📁 \\(name)\"\n    }\n  }\n}\n\nlet data = [\n  FileItem(\n    name: \"user1234\",\n    children: [\n      FileItem(\n        name: \"Photos\",\n        children: [\n          FileItem(name: \"photo001.jpg\"),\n          FileItem(name: \"photo002.jpg\")]),\n      FileItem(\n        name: \"Movies\",\n        children: [FileItem(name: \"movie001.mp4\")]),\n      FileItem(name: \"Documents\", children: [])]),\n  FileItem(\n    name: \"newuser\",\n    children: [FileItem(name: \"Documents\", children: [])])\n]\n\n@State var selection: FileItem?\n\nOutlineView(data, selection: $selection, children: \\.children) { item in\n  NSTextField(string: item.description)\n}\n```\n\n### Customization\n\n#### Children\nThere are two types of `.children` parameters in the `OutlineView` initializers. You either provide the children for an item using:\n- A `KeyPath` pointing to an optional `Sequence` of the same type as the root data.\n- A closure that returns an optional `Sequence` of the same type as the root data, based on the parent item.\n\n```swift\n// By passing a KeyPath to the children:\nOutlineView(data, children: \\.children, selection: $selection) { item in\n  NSTextField(string: item.description)\n}\n\n// By providing a closure that returns the children:\nOutlineView(data, selection: $selection) { item in \n  dataSource.childrenOfItem(item) \n} content: { item in\n  NSTextField(string: item.description)\n}\n```\n\n#### Style\nYou can customize the look of the `OutlineView` by providing a preferred style (`NSOutlineView.Style`) in the `outlineViewStyle` method. The default value is `.automatic`.\n\n```swift\nOutlineView(data, selection: $selection, children: \\.children) { item in\n  NSTextField(string: item.description)\n}\n.outlineViewStyle(.sourceList)\n```\n\n#### Indentation\n\nYou can customize the indentation width for the `OutlineView`. Each child will be indented by this width, from the parent's leading inset. The default value is `13.0`.\n\n```swift\nOutlineView(data, selection: $selection, children: \\.children) { item in\n  NSTextField(string: item.description)\n}\n.outlineViewIndentation(20)\n```\n\n#### Displaying separators\n\nYou can customize the `OutlineView` to display row separators by using the `rowSeparator` modifier.\n\n```swift\nOutlineView(data, selection: $selection, children: \\.children) { item in\n  NSTextField(string: item.description)\n}\n.rowSeparator(.visible)\n```\n\nBy default, macOS will attempt to draw separators with appropriate insets based on the style of the `OutlineView` and the contents of the cell. To customize the separator insets, you can use the initializer which takes `separatorInsets` as an argument. `separatorInsets` is a closure that returns the edge insets of a separator for the row displaying the provided data element.\n\n\u003eNote: This initializer is only available on macOS 11.0 and higher.\n\n```swift\nlet separatorInset = NSEdgeInsets(top: 0, left: 24, bottom: 0, right: 0)\n\nOutlineView(\n  data, \n  selection: $selection,\n  children: \\.children, \n  separatorInsets: { item in separatorInset }) { item in\n  NSTextField(string: item.description)\n}\n```\n\n#### Row separator color\n\nYou can customize the color of the row separators of the `OutlineView`. The default color is `NSColor.separatorColor`.\n\n```swift\nOutlineView(data, selection: $selection, children: \\.children) { item in\n  NSTextField(string: item.description)\n}\n.rowSeparator(.visible)\n.rowSeparatorColor(.red)\n```\n\n### Drag \u0026 Drop\n\n#### Dragging From `OutlineView`\n\nAdd the `dragDataSource` modifier to the `OutlineView` to allow dragging rows from the `OutlineView`. The `dragDataSource` takes a closure that translates a data element into an optional `NSPasteboardItem`, with a `nil` value meaning the row can't be dragged).\n\n```swift\nextension NSPasteboard.PasteboardType {\n  static var myPasteboardType: Self {\n    PasteboardType(\"MySpecialPasteboardIdentifier\")\n  }\n}\n\noutlineView\n  .dragDataSource { item in\n    let pasteboardItem = NSPasteboardItem()\n        pasteboardItem.setData(item.dataRepresentation, forType: .myPasteboardType)\n    return pasteboardItem\n  }\n```\n\n#### Dropping into `OutlineView`\n\nDrag events on the `OutlineView`, either from the `dragDataSource` modifier or from outside the `OutlineView`, can be handled by adding the `onDrop(of:receiver:)` modifier. This modifier takes  a list of supported `NSPasteboard.PasteboardType`s and a receiver instance conforming to the `DropReceiver` protocol. `DropReceiver` implements functions to validate a drop operation, read items from the dragging pasteboard, and update the data source when a drop is successful.\n\n```swift\noutlineView\n  .onDrop(of: [.myPasteboardType, .fileUrl], receiver: MyDropReceiver())\n  \nclass MyDropReceiver: DropReceiver {\n  func readPasteboard(item: NSPasteboardItem) -\u003e DraggedItem\u003cDataElement\u003e? {\n    guard let pasteboardType = item.availableType(from: pasteboardTypes) else { return nil }\n    \n    switch pasteboardType {\n      case .myPasteboardType:\n        if let draggedData = item.data(forType: .myPasteboardType) {\n          let draggedFileItem = /* instance of OutlineView.Data.Element from draggedData */\n          return (draggedFileItem, .myPasteboardType)\n        } else {\n          return nil\n        }\n      case .fileUrl:\n        if let draggedUrlString = item.string(forType: .fileUrl),\n           draggedUrl = URL(string: draggedUrlString)\n        {\n          let newFileItem = /* instance of OutlineView.Data.Element from draggedUrl */\n          return (newFileItem, .fileUrl)\n        } else {\n          return nil\n        }\n      default:\n        return nil\n    }\n  }\n  \n  func validateDrop(target: DropTarget\u003cDataElement\u003e) -\u003e ValidationResult\u003cDataElement\u003e {\n    let draggedItems = target.draggedItems\n    \n    if draggedItems[0].type == .myPasteboardType {\n      return .move\n    } else if draggedItems[0].type == .fileUrl {\n      return .copy\n    } else {\n      return .deny\n    }\n  }\n  \n  func acceptDrop(target: DropTarget\u003cDataElement\u003e) -\u003e Bool {\n    // update data source to reflect that drop was successful or not\n    return dropWasSuccessful\n  }\n}\n```\n\nFor more details on the various types needed in `onDrop`, see `OutlineViewDragAndDrop.swift`, and the sample app `OutlineViewDraggingExample`.\n\n## Why use `OutlineView` instead of the native `List` with children?\n\n`OutlineView` is meant to serve as a stopgap solution to a few of the quirks of `OutlineGroup`s in a `List` or `List` with children on macOS.\n\n- The current implementation of updates on a list with `OutlineGroup`s is miscalculated, which leads to incorrect cell updates on the UI and crashes due to accessing invalid indices on the internal model. This bug makes the `OutlineGroup` unusable on macOS unless you are working with static content.\n- It is easier to expose more of the built-in features of an `NSOutlineView` as we have full control over the code, which enables bringing over additional features in the future like support for multiple columns.\n- Unlike SwiftUI's native `OutlineGroup` or `List` with children, `OutlineView` supports macOS 10.15 Catalina.\n- `OutlineView` supports row animations for updates by default.\n\n## Caveats\n\n`OutlineView` is implemented using the public API for SwiftUI, leading to some limitations that are hard to workaround.\n\n- The content of the cells has to be represented as an `NSView`. This is required as `NSOutlineView` has internal methods for automatically changing the selected cell's text color. A SwiftUI `Text` is not accessible from AppKit, and therefore, any SwiftUI `Text` views will not be able to adopt the system behavior for the highlighted cell's text color. Providing an `NSView` with `NSTextField`s for displaying text allows us to work around that limitation.\n- Automatic height `NSOutlineView`s still seems to require an initial cell height to be provided. This in itself is not a problem, but the default `fittingSize` of an `NSView` with the correct constraints around a multiline `NSTextField` is miscalculated. The `NSTextField`'s width does not seem to be bounded when the fitting size is calculated (even if a correct max-width constraint was provided to the `NSView`). So, if you have a variable height `NSView`, you have to make sure that the `fittingSize` is computed appropriately. (Setting the `NSTextField.preferredMaxLayoutWidth` to the expected width for fitting size calculations should be sufficient.)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsameesunkaria%2Foutlineview","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsameesunkaria%2Foutlineview","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsameesunkaria%2Foutlineview/lists"}