Make RSTree a local package. Fix concurrency warnings in the package.

This commit is contained in:
Brent Simmons
2024-03-21 17:18:01 -07:00
parent ce2b2112d1
commit 9759d7dcd5
10 changed files with 530 additions and 69 deletions

8
Tree/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

21
Tree/Package.swift Normal file
View File

@@ -0,0 +1,21 @@
// swift-tools-version: 5.10
import PackageDescription
let package = Package(
name: "Tree",
products: [
.library(
name: "Tree",
targets: ["Tree"]),
],
targets: [
.target(
name: "Tree",
dependencies: [],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
)
]
)

View File

@@ -0,0 +1,58 @@
//
// NSOutlineView+RSTree.swift
// RSTree
//
// Created by Brent Simmons on 9/5/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
#if os(OSX)
import AppKit
public extension NSOutlineView {
@discardableResult
func revealAndSelectNodeAtPath(_ nodePath: NodePath) -> Bool {
// Returns true on success. Expands folders on the way. May succeed partially (returns false, in that case).
let numberOfNodes = nodePath.components.count
if numberOfNodes < 2 {
return false
}
let indexOfNodeToSelect = numberOfNodes - 1
for i in 1...indexOfNodeToSelect { // Start at 1 to skip root node.
let oneNode = nodePath.components[i]
let oneRow = row(forItem: oneNode)
if oneRow < 0 {
return false
}
if i == indexOfNodeToSelect {
selectRowIndexes(NSIndexSet(index: oneRow) as IndexSet, byExtendingSelection: false)
scrollRowToVisible(oneRow)
return true
}
else {
expandItem(oneNode)
}
}
return false
}
@discardableResult
func revealAndSelectRepresentedObject(_ representedObject: AnyObject, _ treeController: TreeController) -> Bool {
guard let nodePath = NodePath(representedObject: representedObject, treeController: treeController) else {
return false
}
return revealAndSelectNodeAtPath(nodePath)
}
}
#endif

View File

@@ -0,0 +1,224 @@
//
// Node.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/21/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Main thread only.
@MainActor public final class Node: Hashable {
public weak var parent: Node?
public let representedObject: AnyObject
public var canHaveChildNodes = false
public var isGroupItem = false
public var childNodes = [Node]()
public let uniqueID: Int
private static var incrementingID = 0
public var isRoot: Bool {
if let _ = parent {
return false
}
return true
}
public var numberOfChildNodes: Int {
return childNodes.count
}
public var indexPath: IndexPath {
if let parent = parent {
let parentPath = parent.indexPath
if let childIndex = parent.indexOfChild(self) {
return parentPath.appending(childIndex)
}
preconditionFailure("A Nodes parent must contain it as a child.")
}
return IndexPath(index: 0) //root node
}
public var level: Int {
if let parent = parent {
return parent.level + 1
}
return 0
}
public var isLeaf: Bool {
return numberOfChildNodes < 1
}
public init(representedObject: AnyObject, parent: Node?) {
precondition(Thread.isMainThread)
self.representedObject = representedObject
self.parent = parent
self.uniqueID = Node.incrementingID
Node.incrementingID += 1
}
public class func genericRootNode() -> Node {
let node = Node(representedObject: TopLevelRepresentedObject(), parent: nil)
node.canHaveChildNodes = true
return node
}
public func existingOrNewChildNode(with representedObject: AnyObject) -> Node {
if let node = childNodeRepresentingObject(representedObject) {
return node
}
return createChildNode(representedObject)
}
public func createChildNode(_ representedObject: AnyObject) -> Node {
// Just creates doesnt add it.
return Node(representedObject: representedObject, parent: self)
}
public func childAtIndex(_ index: Int) -> Node? {
if index >= childNodes.count || index < 0 {
return nil
}
return childNodes[index]
}
public func indexOfChild(_ node: Node) -> Int? {
return childNodes.firstIndex{ (oneChildNode) -> Bool in
oneChildNode === node
}
}
public func childNodeRepresentingObject(_ obj: AnyObject) -> Node? {
return findNodeRepresentingObject(obj, recursively: false)
}
public func descendantNodeRepresentingObject(_ obj: AnyObject) -> Node? {
return findNodeRepresentingObject(obj, recursively: true)
}
public func descendantNode(where test: (Node) -> Bool) -> Node? {
return findNode(where: test, recursively: true)
}
public func hasAncestor(in nodes: [Node]) -> Bool {
for node in nodes {
if node.isAncestor(of: self) {
return true
}
}
return false
}
public func isAncestor(of node: Node) -> Bool {
if node == self {
return false
}
var nomad = node
while true {
guard let parent = nomad.parent else {
return false
}
if parent == self {
return true
}
nomad = parent
}
}
public class func nodesOrganizedByParent(_ nodes: [Node]) -> [Node: [Node]] {
let nodesWithParents = nodes.filter { $0.parent != nil }
return Dictionary(grouping: nodesWithParents, by: { $0.parent! })
}
public class func indexSetsGroupedByParent(_ nodes: [Node]) -> [Node: IndexSet] {
let d = nodesOrganizedByParent(nodes)
let indexSetDictionary = d.mapValues { (nodes) -> IndexSet in
var indexSet = IndexSet()
if nodes.isEmpty {
return indexSet
}
let parent = nodes.first!.parent!
for node in nodes {
if let index = parent.indexOfChild(node) {
indexSet.insert(index)
}
}
return indexSet
}
return indexSetDictionary
}
// MARK: - Hashable
nonisolated public func hash(into hasher: inout Hasher) {
hasher.combine(uniqueID)
}
// MARK: - Equatable
nonisolated public class func ==(lhs: Node, rhs: Node) -> Bool {
return lhs === rhs
}
}
public extension Array where Element == Node {
@MainActor func representedObjects() -> [AnyObject] {
return self.map{ $0.representedObject }
}
}
private extension Node {
func findNodeRepresentingObject(_ obj: AnyObject, recursively: Bool = false) -> Node? {
for childNode in childNodes {
if childNode.representedObject === obj {
return childNode
}
if recursively, let foundNode = childNode.descendantNodeRepresentingObject(obj) {
return foundNode
}
}
return nil
}
func findNode(where test: (Node) -> Bool, recursively: Bool = false) -> Node? {
for childNode in childNodes {
if test(childNode) {
return childNode
}
if recursively, let foundNode = childNode.findNode(where: test, recursively: recursively) {
return foundNode
}
}
return nil
}
}

View File

@@ -0,0 +1,42 @@
//
// NodePath.swift
// RSTree
//
// Created by Brent Simmons on 9/5/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
@MainActor public struct NodePath {
let components: [Node]
public init(node: Node) {
var tempArray = [node]
var nomad: Node = node
while true {
if let parent = nomad.parent {
tempArray.append(parent)
nomad = parent
}
else {
break
}
}
self.components = tempArray.reversed()
}
public init?(representedObject: AnyObject, treeController: TreeController) {
if let node = treeController.nodeInTreeRepresentingObject(representedObject) {
self.init(node: node)
}
else {
return nil
}
}
}

View File

@@ -0,0 +1,3 @@
struct RSTree {
var text = "Hello, World!"
}

View File

@@ -0,0 +1,15 @@
//
// TopLevelRepresentedObject.swift
// RSTree
//
// Created by Brent Simmons on 8/10/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Handy to use as the represented object for a root node. Not required to use it, though.
final class TopLevelRepresentedObject {
}

View File

@@ -0,0 +1,135 @@
//
// TreeController.swift
// NetNewsWire
//
// Created by Brent Simmons on 5/29/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public protocol TreeControllerDelegate: AnyObject {
func treeController(treeController: TreeController, childNodesFor: Node) -> [Node]?
}
public typealias NodeVisitBlock = (_ : Node) -> Void
@MainActor public final class TreeController {
private weak var delegate: TreeControllerDelegate?
public let rootNode: Node
public init(delegate: TreeControllerDelegate, rootNode: Node) {
self.delegate = delegate
self.rootNode = rootNode
rebuild()
}
public convenience init(delegate: TreeControllerDelegate) {
self.init(delegate: delegate, rootNode: Node.genericRootNode())
}
@discardableResult
public func rebuild() -> Bool {
// Rebuild and re-sort. Return true if any changes in the entire tree.
return rebuildChildNodes(node: rootNode)
}
public func visitNodes(_ visitBlock: NodeVisitBlock) {
visitNode(rootNode, visitBlock)
}
public func nodeInArrayRepresentingObject(nodes: [Node], representedObject: AnyObject, recurse: Bool = false) -> Node? {
for oneNode in nodes {
if oneNode.representedObject === representedObject {
return oneNode
}
if recurse, oneNode.canHaveChildNodes {
if let foundNode = nodeInArrayRepresentingObject(nodes: oneNode.childNodes, representedObject: representedObject, recurse: recurse) {
return foundNode
}
}
}
return nil
}
public func nodeInTreeRepresentingObject(_ representedObject: AnyObject) -> Node? {
return nodeInArrayRepresentingObject(nodes: [rootNode], representedObject: representedObject, recurse: true)
}
public func normalizedSelectedNodes(_ nodes: [Node]) -> [Node] {
// An array of nodes might include a leaf node and its parent. Remove the leaf node.
var normalizedNodes = [Node]()
for node in nodes {
if !node.hasAncestor(in: nodes) {
normalizedNodes += [node]
}
}
return normalizedNodes
}
}
private extension TreeController {
func visitNode(_ node: Node, _ visitBlock: NodeVisitBlock) {
visitBlock(node)
node.childNodes.forEach{ (oneChildNode) in
visitNode(oneChildNode, visitBlock)
}
}
func nodeArraysAreEqual(_ nodeArray1: [Node]?, _ nodeArray2: [Node]?) -> Bool {
if nodeArray1 == nil && nodeArray2 == nil {
return true
}
if nodeArray1 != nil && nodeArray2 == nil {
return false
}
if nodeArray1 == nil && nodeArray2 != nil {
return false
}
return nodeArray1! == nodeArray2!
}
func rebuildChildNodes(node: Node) -> Bool {
if !node.canHaveChildNodes {
return false
}
var childNodesDidChange = false
let childNodes = delegate?.treeController(treeController: self, childNodesFor: node) ?? [Node]()
childNodesDidChange = !nodeArraysAreEqual(childNodes, node.childNodes)
if (childNodesDidChange) {
node.childNodes = childNodes
}
childNodes.forEach{ (oneChildNode) in
if rebuildChildNodes(node: oneChildNode) {
childNodesDidChange = true
}
}
return childNodesDidChange
}
}