From 0ed3495e79bd5c49cab6788b23dae8754677f966 Mon Sep 17 00:00:00 2001 From: Kishikawa Katsumi Date: Tue, 17 Sep 2024 17:57:52 +0900 Subject: [PATCH] Performance tuning --- .../ServersViewController.swift | 7 +- .../FileBrowser (iOS)/ServiceDiscovery.swift | 43 ------- .../FileBrowser (macOS)/DataRepository.swift | 6 +- .../DirectoryStructure.swift | 120 ++++++++--------- .../FilesViewController.swift | 20 ++- .../FileBrowser (macOS)/Localizable.xcstrings | 21 --- .../SharesViewController.swift | 8 +- .../FileBrowser (macOS)/SidebarManager.swift | 91 +++++-------- .../SidebarViewController.swift | 29 +++-- .../FileBrowser (macOS)/Tree.swift | 121 +++++++++++++----- .../WindowController.swift | 2 +- .../FileBrowser.xcodeproj/project.pbxproj | 18 ++- Examples/FileBrowser/Shared/ID.swift | 4 +- Examples/FileBrowser/Shared/Service.swift | 7 +- .../ServiceDiscovery.swift | 12 +- 15 files changed, 230 insertions(+), 279 deletions(-) delete mode 100644 Examples/FileBrowser/FileBrowser (iOS)/ServiceDiscovery.swift rename Examples/FileBrowser/{FileBrowser (macOS) => Shared}/ServiceDiscovery.swift (74%) diff --git a/Examples/FileBrowser/FileBrowser (iOS)/ServersViewController.swift b/Examples/FileBrowser/FileBrowser (iOS)/ServersViewController.swift index e9e8a03..e066a90 100644 --- a/Examples/FileBrowser/FileBrowser (iOS)/ServersViewController.swift +++ b/Examples/FileBrowser/FileBrowser (iOS)/ServersViewController.swift @@ -4,6 +4,8 @@ import SMBClient class ServersViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { private let tableView = UITableView(frame: .zero, style: .insetGrouped) + + private var services = [Service]() private var sessions = [String: SMBClient]() override func viewDidLoad() { @@ -40,6 +42,7 @@ class ServersViewController: UIViewController, UITableViewDataSource, UITableVie @objc private func serviceDidDiscover(_ notification: Notification) { + services = ServiceDiscovery.shared.services.sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } tableView.reloadData() } @@ -117,11 +120,9 @@ class ServersViewController: UIViewController, UITableViewDataSource, UITableVie switch indexPath.section { case 0: - let services = ServiceDiscovery.shared.services let service = services[indexPath.row] cell.textLabel?.text = service.name - if let _ = sessions[service.id.rawValue] { cell.accessoryType = .disclosureIndicator } else { @@ -132,7 +133,6 @@ class ServersViewController: UIViewController, UITableViewDataSource, UITableVie let server = servers[indexPath.row] cell.textLabel?.text = server.displayName - if let _ = sessions[server.id.rawValue] { cell.accessoryType = .disclosureIndicator } else { @@ -149,7 +149,6 @@ class ServersViewController: UIViewController, UITableViewDataSource, UITableVie Task { @MainActor in switch indexPath.section { case 0: - let services = ServiceDiscovery.shared.services let service = services[indexPath.row] if let client = sessions[service.id.rawValue] { diff --git a/Examples/FileBrowser/FileBrowser (iOS)/ServiceDiscovery.swift b/Examples/FileBrowser/FileBrowser (iOS)/ServiceDiscovery.swift deleted file mode 100644 index 90c2a89..0000000 --- a/Examples/FileBrowser/FileBrowser (iOS)/ServiceDiscovery.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -import Network - -class ServiceDiscovery { - static let shared = ServiceDiscovery() - static let serviceDidDiscover = Notification.Name("ServiceDiscoveryServiceDidDiscover") - - private let browser: NWBrowser - private(set) var services: [Service] - - private init() { - let params = NWParameters() - params.includePeerToPeer = true - - let descriptor = NWBrowser.Descriptor.bonjour(type: "_smb._tcp", domain: nil) - browser = NWBrowser(for: descriptor, using: params) - - services = [Service]() - } - - func start() { - browser.browseResultsChangedHandler = { (results, changes) in - for result in results { - switch result.endpoint { - case .service(name: let name, type: let type, domain: let domain, interface: let interface): - let service = Service(name: name, type: type, domain: domain, interface: interface) - guard !self.services.contains(service) else { return } - - self.services.append(service) - self.services.sort { $0.name.localizedStandardCompare($1.name) == .orderedAscending } - - NotificationCenter.default.post(name: Self.serviceDidDiscover, object: self) - case .hostPort, .unix, .url, .opaque: - break - @unknown default: - break - } - } - } - - browser.start(queue: .main) - } -} diff --git a/Examples/FileBrowser/FileBrowser (macOS)/DataRepository.swift b/Examples/FileBrowser/FileBrowser (macOS)/DataRepository.swift index 0c03881..1f07163 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/DataRepository.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/DataRepository.swift @@ -6,11 +6,11 @@ class DataRepository { private init() {} - func set(_ path: String, nodes: [Node]) { + func set(_ path: String, nodes: [Item]) { data[path] = nodes.map { $0.detach() } } - func nodes(_ path: String) -> [Node]? { - data[path] + func nodes(_ path: String) -> [Item]? { + data[path] as? [Item] } } diff --git a/Examples/FileBrowser/FileBrowser (macOS)/DirectoryStructure.swift b/Examples/FileBrowser/FileBrowser (macOS)/DirectoryStructure.swift index d51ae9a..44bd148 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/DirectoryStructure.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/DirectoryStructure.swift @@ -6,29 +6,34 @@ class DirectoryStructure { private let server: String private let path: String - private var tree = Tree() - private var viewTree = Tree() + private var tree = Tree() + private var viewTree = Tree() private var searchText = "" private var sortDescriptor = NSSortDescriptor(key: "NameColumn", ascending: true) private let client: SMBClient + var useCache = false { + didSet { + if !useCache { + cache.removeAll() + } + } + } + private var cache = [FileNode: [FileNode]]() + init(server: String, path: String, client: SMBClient) { self.server = server self.path = path self.client = client } - func viewTree(_ tree: Tree) -> Tree { + func viewTree(_ tree: Tree) -> Tree { let nodes = tree.nodes - .compactMap { - guard let node = $0 as? FileNode else { return nil } - return node - } .sorted(sortDescriptor) - let viewTree: Tree + let viewTree: Tree if !searchText.isEmpty { let filteredNodes = nodes.filter { $0.name.localizedCaseInsensitiveContains(searchText) @@ -43,9 +48,7 @@ class DirectoryStructure { func reload() async { let nodes = await listDirectory(path: path, parent: nil) - - tree.nodes.removeAll() - tree.nodes.append(contentsOf: nodes) + tree.nodes = nodes viewTree = viewTree(tree) } @@ -57,11 +60,7 @@ class DirectoryStructure { let nodes = await listDirectory(path: path, parent: nil) let rootNodes = tree.rootNodes() - let newNodes = mergeNodes(oldNodes: rootNodes, newNodes: nodes) - - let set = Set(rootNodes) - tree.nodes = tree.nodes.filter { !set.contains($0) } + newNodes - + tree.nodes = Array(Set(rootNodes).union(nodes)) viewTree = viewTree(tree) outlineView.reloadData() @@ -72,52 +71,41 @@ class DirectoryStructure { let path = resolvePath(fileNode) let nodes = await listDirectory(path: path, parent: fileNode) - let childlen = children(of: fileNode) - - let (deleted, inserted) = nodeDelta(oldNodes: childlen, newNodes: nodes) - let newNodes = mergeNodes(oldNodes: childlen, newNodes: nodes) + let children = children(of: fileNode) - let set = Set(childlen) - tree.nodes = tree.nodes.filter { !set.contains($0) } + newNodes + let (deleted, inserted) = nodeDelta(oldNodes: children, newNodes: nodes) + tree.nodes = Array( + Set(tree.nodes) + .subtracting(children) + .union(nodes) + ) viewTree = viewTree(tree) outlineView.beginUpdates() outlineView.removeItems(at: IndexSet(deleted), inParent: fileNode) - outlineView.insertItems(at: IndexSet(inserted), inParent: fileNode, withAnimation: childlen.isEmpty ? .slideDown : []) + outlineView.insertItems(at: IndexSet(inserted), inParent: fileNode, withAnimation: children.isEmpty ? .slideDown : []) outlineView.endUpdates() } func update(_ outlineView: NSOutlineView) { - guard let newRootNodes = DataRepository.shared.nodes(join(server, path)) else { + guard let rootNodes: [FileNode] = DataRepository.shared.nodes(join(server, path)) else { return } - let expandedNodes = tree.nodes - .compactMap { - $0 as? FileNode - } + + let childNodes = tree.nodes .filter { return $0.isDirectory && outlineView.isItemExpanded($0) } - var newChildNodes = [Node]() - for expandedNode in expandedNodes { - if let nodes = DataRepository.shared.nodes(join(server, expandedNode.path)) { - for case let node as FileNode in nodes { - newChildNodes.append(FileNode(path: node.path, file: node.file, parent: ID(expandedNode.path))) + .reduce(into: [FileNode]()) { + guard let nodes: [FileNode] = DataRepository.shared.nodes(join(server, $1.path)) else { + return } + let parent = $1.id + $0 += nodes.map { FileNode(path: $0.path, file: $0.file, parent: parent) } } - } - - let oldNodes = Set(tree.nodes) - let newNodes = Set(newRootNodes + newChildNodes) - let common = oldNodes.intersection(newNodes) - let added = newNodes.subtracting(oldNodes) - - var merged = Array(common) - merged.append(contentsOf: added) - - tree.nodes = merged + tree.nodes = rootNodes + childNodes viewTree = viewTree(tree) outlineView.reloadData() @@ -133,9 +121,9 @@ class DirectoryStructure { viewTree = viewTree(tree) } - func resolvePath(_ node: Node) -> String { + func resolvePath(_ node: FileNode) -> String { var subpath = node.name - var current: Node = node + var current: FileNode = node while let parent = tree.parent(of: current) { subpath = join(parent.name, subpath) current = parent @@ -148,16 +136,24 @@ class DirectoryStructure { viewTree.nodes.count } - func parent(of node: Node) -> Node? { + func parent(of node: FileNode) -> FileNode? { viewTree.parent(of: node) } - func children(of node: Node) -> [Node] { - viewTree.children(of: node) + func children(of node: FileNode) -> [FileNode] { + if useCache, let children = cache[node] { + return children + } else { + let children = viewTree.children(of: node) + if useCache { + cache[node] = children + } + return children + } } func node(_ id: ID) -> FileNode? { - return viewTree.nodes.first { $0.id == id } as? FileNode + viewTree.nodes.first { $0.id == id } } func node(_ fileURL: URL) -> FileNode? { @@ -168,7 +164,7 @@ class DirectoryStructure { func numberOfChildren(of item: Any?) -> Int { if let node = item as? FileNode { if node.isExpandable { - return viewTree.children(of: node).count + return children(of: node).count } else { return 0 } @@ -179,7 +175,8 @@ class DirectoryStructure { func child(index: Int, of item: Any?) -> Any { if let node = item as? FileNode { - return viewTree.children(of: node)[index] + let children = children(of: node) + return children[index] } else { return viewTree.rootNodes()[index] } @@ -192,7 +189,7 @@ class DirectoryStructure { return false } - private func listDirectory(path: String, parent: FileNode?) async -> [Node] { + private func listDirectory(path: String, parent: FileNode?) async -> [FileNode] { do { let files = try await client.listDirectory(path: path) .filter { $0.name != "." && $0.name != ".." && !$0.isHidden } @@ -210,7 +207,7 @@ class DirectoryStructure { return DataRepository.shared.nodes(join(server, path)) ?? [] } - private func nodeDelta(oldNodes: [Node], newNodes: [Node]) -> (deleted: [Int], inserted: [Int]) { + private func nodeDelta(oldNodes: [FileNode], newNodes: [FileNode]) -> (deleted: [Int], inserted: [Int]) { let oldSet = Set(oldNodes) let newSet = Set(newNodes) @@ -224,21 +221,6 @@ class DirectoryStructure { return (deleted, inserted) } - - private func mergeNodes(oldNodes: [Node], newNodes: [Node]) -> [Node] { - var mergedNodes = [Node]() - let oldDict = Dictionary(uniqueKeysWithValues: oldNodes.map { ($0, $0) }) - - for newNode in newNodes { - if let oldNode = oldDict[newNode] { - mergedNodes.append(oldNode) - } else { - mergedNodes.append(newNode) - } - } - - return mergedNodes - } } private extension Array where Element == FileNode { diff --git a/Examples/FileBrowser/FileBrowser (macOS)/FilesViewController.swift b/Examples/FileBrowser/FileBrowser (macOS)/FilesViewController.swift index ecb17a1..68c3c94 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/FilesViewController.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/FilesViewController.swift @@ -20,8 +20,6 @@ class FilesViewController: NSViewController { private var tabGroupObserving: NSKeyValueObservation? private var scrollViewObserving: NSKeyValueObservation? - private let semaphore = Semaphore(value: 1) - private var dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium @@ -198,7 +196,7 @@ class FilesViewController: NSViewController { Task { let name = NSLocalizedString("untitled folder", comment: "") if let parent { - await dirTree.reload(directory: join(parent, name), outlineView) + await dirTree.reload(directory: parent, outlineView) } else { try await client.createDirectory(path: join(path, name)) await dirTree.reload(directory: path, outlineView) @@ -338,7 +336,7 @@ class FilesViewController: NSViewController { navigationController.push(filesViewController) } else { - guard let shares = DataRepository.shared.nodes(serverNode.path) else { return } + guard let shares: [ShareNode] = DataRepository.shared.nodes(serverNode.path) else { return } let sharesViewController = SharesViewController.instantiate(serverNode: serverNode, shares: Tree(nodes: shares)) navigationController.push(sharesViewController) } @@ -585,16 +583,26 @@ extension FilesViewController: NSOutlineViewDelegate { guard let fileNode = userInfo["NSObject"] as? FileNode else { return } guard fileNode.isDirectory else { return } + dirTree.useCache = true + Task { @MainActor in - await semaphore.wait() - defer { Task { await semaphore.signal() } } + dirTree.useCache = false await dirTree.expand(fileNode, outlineView) updateItemCount() } } + func outlineViewItemDidExpand(_ notification: Notification) { + updateItemCount() + } + + func outlineViewItemWillCollapse(_ notification: Notification) { + dirTree.useCache = true + } + func outlineViewItemDidCollapse(_ notification: Notification) { + dirTree.useCache = false updateItemCount() } } diff --git a/Examples/FileBrowser/FileBrowser (macOS)/Localizable.xcstrings b/Examples/FileBrowser/FileBrowser (macOS)/Localizable.xcstrings index a0356c6..8376b78 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/Localizable.xcstrings +++ b/Examples/FileBrowser/FileBrowser (macOS)/Localizable.xcstrings @@ -1,21 +1,6 @@ { "sourceLanguage" : "en", "strings" : { - "(completedFiles) files uploaded" : { - - }, - "(fileBeingTransferred.lastPathComponent) in (transfer.name)" : { - - }, - "(outlineView.numberOfRows) items" : { - - }, - "(progressBytes) of (totalBytes)" : { - - }, - "(tree.rootNodes().count) items" : { - - }, "Activities" : { }, @@ -27,12 +12,6 @@ }, "Queued" : { "comment" : "(progressBytes) of (totalBytes)" - }, - "Servers" : { - - }, - "Services" : { - }, "untitled folder" : { diff --git a/Examples/FileBrowser/FileBrowser (macOS)/SharesViewController.swift b/Examples/FileBrowser/FileBrowser (macOS)/SharesViewController.swift index ab6a9b1..de06db8 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/SharesViewController.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/SharesViewController.swift @@ -8,12 +8,12 @@ class SharesViewController: NSViewController { @IBOutlet private var statusBarView: StatusBarView! private let serverNode: ServerNode - private var tree: Tree + private var tree: Tree private var tabGroupObserving: NSKeyValueObservation? private var scrollViewObserving: NSKeyValueObservation? - static func instantiate(serverNode: ServerNode, shares: Tree) -> Self { + static func instantiate(serverNode: ServerNode, shares: Tree) -> Self { let storyboard = NSStoryboard(name: storyboardID, bundle: nil) return storyboard.instantiateController(identifier: storyboardID) { (coder) in Self(coder: coder, serverNode: serverNode, shares: shares) @@ -24,7 +24,7 @@ class SharesViewController: NSViewController { return nil } - required init?(coder: NSCoder, serverNode: ServerNode, shares: Tree) { + required init?(coder: NSCoder, serverNode: ServerNode, shares: Tree) { self.serverNode = serverNode self.tree = shares super.init(coder: coder) @@ -72,7 +72,7 @@ class SharesViewController: NSViewController { tabGroupObserving?.invalidate() tabGroupObserving = tabGroup.observe(\.selectedWindow) { (tabGroup, change) in if window == tabGroup.selectedWindow { - guard let shares = DataRepository.shared.nodes(serverNode.path) else { return } + guard let shares: [ShareNode] = DataRepository.shared.nodes(serverNode.path) else { return } tree.nodes.removeAll() tree.nodes.append(contentsOf: shares) outlineView.reloadData() diff --git a/Examples/FileBrowser/FileBrowser (macOS)/SidebarManager.swift b/Examples/FileBrowser/FileBrowser (macOS)/SidebarManager.swift index 93e7a03..003b27d 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/SidebarManager.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/SidebarManager.swift @@ -3,10 +3,9 @@ import SMBClient class SidebarManager { static let shared = SidebarManager() - static let sidebarDidUpdate = Notification.Name("SidebarManagerSidebarDidUpdate") - private var tree = Tree() + private var tree = Tree() private init() { NotificationCenter.default.addObserver( @@ -45,17 +44,13 @@ class SidebarManager { } private func updateTree() { - let serviceHeader = HeaderNode(id: "Services", name: NSLocalizedString("Services", comment: "")) let services = ServiceDiscovery.shared.services let serviceNodes = services + .map { SidebarNode(ServerNode(id: $0.id, name: $0.name)) } .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } - .map { ServerNode(id: $0.id, name: $0.name) } - - let serverHeader = HeaderNode(id: "Servers", name: NSLocalizedString("Servers", comment: "")) let servers = ServerManager.shared.servers let serverNodes = servers - .sorted { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending } .map { let name: String if $0.displayName.isEmpty { @@ -63,72 +58,51 @@ class SidebarManager { } else { name = $0.displayName } - return ServerNode(id: $0.id, name: name) + return SidebarNode(ServerNode(id: $0.id, name: name)) } + .sorted { $0.name.localizedStandardCompare(($1 as SidebarNode).name) == .orderedAscending } - let nodes = [serviceHeader] + serviceNodes + [serverHeader] + serverNodes - var newNodes = [Node]() - - for node in nodes { - if let index = tree.nodes.firstIndex(of: node){ - let oldNode = tree.nodes[index] - newNodes.append(oldNode) - if SessionManager.shared.sessionExists(for: oldNode.id) { - newNodes.append(contentsOf: tree.children(of: oldNode)) - } - } else { - newNodes.append(node) + let children = tree.nodes.reduce(into: [SidebarNode]()) { + if $1.content is ServerNode { + $0 += tree.children(of: $1) } } - tree.nodes = newNodes - NotificationCenter.default.post(name: Self.sidebarDidUpdate, object: self) + tree.nodes = [SidebarNode(HeaderNode("Services"))] + serviceNodes + [SidebarNode(HeaderNode("Servers"))] + serverNodes + tree.nodes.append(contentsOf: children) - for node in tree.nodes { - guard let serverNode = node as? ServerNode else { - continue - } - guard SessionManager.shared.sessionExists(for: serverNode.id) else { - continue - } - if tree.hasChildren(serverNode) { - continue - } - - Task { @MainActor in - await updateChildren(of: serverNode) - } - } + NotificationCenter.default.post(name: Self.sidebarDidUpdate, object: self) } } extension SidebarManager { func isItemSelectable(_ item: Any) -> Bool { - item is ServerNode || item is ShareNode + guard let node = item as? SidebarNode else { return false } + return node.content is ServerNode || node.content is ShareNode } - func selectRow(_ node: Node) -> Node? { - if let serverNode = node as? ServerNode { + func selectRow(_ node: SidebarNode) -> SidebarNode? { + if let serverNode = node.content as? ServerNode { let authManager: AuthManager - if serverNode.path.hasSuffix("._smb._tcp.local") { + if serverNode.path.hasSuffix("._smb._tcp.local.") { authManager = ServiceAuthManager(id: serverNode.id, service: serverNode.name) } else { authManager = ServerAuthManager(id: serverNode.id) } if let _ = authManager.authenticate() { - return serverNode + return node } else { return nil } - } else if let shareNode = node as? ShareNode { - return shareNode + } else if node.content is ShareNode { + return node } return nil } - func updateChildren(of node: Node) async { - switch node { + func updateChildren(of node: SidebarNode) async { + switch node.content { case let serverNode as ServerNode: if let session = SessionManager.shared.session(for: serverNode.id) { let serverRoot = serverNode.path @@ -141,27 +115,27 @@ extension SidebarManager { DataRepository.shared.set(serverRoot, nodes: shares) - let childlen = tree.children(of: serverNode) - tree.nodes.removeAll(where: { childlen.contains($0) }) - tree.nodes.append(contentsOf: shares) + let children = tree.children(of: node) + tree.nodes.removeAll(where: { children.contains($0) }) + tree.nodes.append(contentsOf: shares.map { SidebarNode($0, parent: node.id) }) } catch { _ = await MainActor.run { NSAlert(error: error).runModal() } } } else { - let childlen = tree.children(of: serverNode) - tree.nodes.removeAll(where: { childlen.contains($0) }) + let children = tree.children(of: node) + tree.nodes.removeAll(where: { children.contains($0) }) } default: break } } - func logoff(_ serverNode: ServerNode) async { - await SessionManager.shared.logoff(id: serverNode.id) + func logoff(_ node: SidebarNode) async { + await SessionManager.shared.logoff(id: node.id) - let children = tree.children(of: serverNode) + let children = tree.children(of: node) tree.nodes.removeAll(where: { children.contains($0) }) await MainActor.run { @@ -169,14 +143,14 @@ extension SidebarManager { } } - func parent(of node: Node) -> Node? { + func parent(of node: SidebarNode) -> SidebarNode? { tree.parent(of: node) } } extension SidebarManager { func numberOfChildrenOfItem(_ item: Any?) -> Int { - if let node = item as? ServerNode { + if let node = item as? SidebarNode { if tree.hasChildren(node) { return tree.children(of: node).count } else { @@ -188,7 +162,7 @@ extension SidebarManager { } func child(_ index: Int, ofItem item: Any?) -> Any { - if let node = item as? ServerNode { + if let node = item as? SidebarNode { return tree.children(of: node)[index] } else { return tree.rootNodes()[index] @@ -196,6 +170,7 @@ extension SidebarManager { } func isItemExpandable(_ item: Any) -> Bool { - item is ServerNode + guard let node = item as? SidebarNode else { return false } + return node.content is ServerNode } } diff --git a/Examples/FileBrowser/FileBrowser (macOS)/SidebarViewController.swift b/Examples/FileBrowser/FileBrowser (macOS)/SidebarViewController.swift index 28fed84..6b6ef45 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/SidebarViewController.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/SidebarViewController.swift @@ -63,7 +63,9 @@ extension SidebarViewController: NSOutlineViewDataSource { extension SidebarViewController: NSOutlineViewDelegate { func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { - switch item { + guard let node = item as? SidebarNode else { return nil } + + switch node.content { case let headerNode as HeaderNode: let cellIdentifier = NSUserInterfaceItemIdentifier("HeaderCell") guard let cell = outlineView.makeView(withIdentifier: cellIdentifier, owner: nil) as? NSTableCellView else { return nil } @@ -83,7 +85,7 @@ extension SidebarViewController: NSOutlineViewDelegate { cell.ejectButton.isEnabled = false Task { @MainActor in - await SidebarManager.shared.logoff(serverNode) + await SidebarManager.shared.logoff(node) cell.ejectButton.isEnabled = true } } @@ -108,36 +110,39 @@ extension SidebarViewController: NSOutlineViewDelegate { } func outlineViewSelectionDidChange(_ notification: Notification) { - guard let item = sourceList.item(atRow: sourceList.selectedRow) as? Node else { return } + guard let item = sourceList.item(atRow: sourceList.selectedRow) as? SidebarNode else { return } let sidebarManager = SidebarManager.shared let sessionManager = SessionManager.shared - let node = sidebarManager.selectRow(item) + guard let node = sidebarManager.selectRow(item) else { + sourceList.deselectRow(sourceList.selectedRow) + return + } - switch node { + switch node.content { case let serverNode as ServerNode: guard let _ = sessionManager.session(for: serverNode.id) else { return } Task { @MainActor in - await sidebarManager.updateChildren(of: serverNode) + await sidebarManager.updateChildren(of: node) - sourceList.reloadItem(serverNode, reloadChildren: true) - sourceList.expandItem(serverNode) + sourceList.reloadItem(item, reloadChildren: true) + sourceList.expandItem(item) - let row = sourceList.row(forItem: serverNode) + let row = sourceList.row(forItem: item) if let rowView = sourceList.rowView(atRow: row, makeIfNecessary: false) { rowView.isSelected = true } guard let navigationController = navigationController() else { return } - guard let shares = DataRepository.shared.nodes(serverNode.path) else { return } + guard let shares: [ShareNode] = DataRepository.shared.nodes(serverNode.path) else { return } let sharesViewController = SharesViewController.instantiate(serverNode: serverNode, shares: Tree(nodes: shares)) navigationController.push(sharesViewController) } case let shareNode as ShareNode: - guard let serverNode = sidebarManager.parent(of: shareNode) as? ServerNode else { return } + guard let serverNode = sidebarManager.parent(of: node)?.content as? ServerNode else { return } guard let session = sessionManager.session(for: serverNode.id) else { return } Task { @MainActor in @@ -160,7 +165,7 @@ extension SidebarViewController: NSOutlineViewDelegate { } } default: - sourceList.deselectRow(sourceList.selectedRow) + break } } diff --git a/Examples/FileBrowser/FileBrowser (macOS)/Tree.swift b/Examples/FileBrowser/FileBrowser (macOS)/Tree.swift index d59ff17..391ed1e 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/Tree.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/Tree.swift @@ -1,78 +1,130 @@ import Foundation import SMBClient -struct Tree { - var nodes = [Node]() +struct Tree { + var nodes = [Item]() - func rootNodes() -> [Node] { - return nodes.filter { $0.isRoot } + func rootNodes() -> [Item] { + nodes.filter { $0.isRoot } } - func children(of node: Node) -> [Node] { - return nodes.filter { $0.parent == node.id } + func children(of node: Item) -> [Item] { + nodes.filter { $0.parent == node.id } } - func hasChildren(_ node: Node) -> Bool { - return nodes.contains { $0.parent == node.id } + func hasChildren(_ node: Item) -> Bool { + nodes.contains { $0.parent == node.id } } - func parent(of node: Node) -> Node? { - return nodes.first { $0.id == node.parent } + func parent(of node: Item) -> Item? { + nodes.first { $0.id == node.parent } } } -class Node { +protocol Node { + var id: ID { get } + var name: String { get } + var parent: ID? { get } + + var isRoot: Bool { get } + + func detach() -> Self +} + +extension Node { + var isRoot: Bool { parent == nil } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +struct SidebarNode: Node, Hashable { let id: ID let name: String let parent: ID? - var isRoot: Bool { parent == nil } + let content: Node - init(id: ID, name: String, parent: ID? = nil) { - self.id = id - self.name = name + init(_ content: Node, parent: ID? = nil) { + id = content.id + name = content.name self.parent = parent + + self.content = content } - func detach() -> Node { - Node(id: id, name: name) + func detach() -> Self { + SidebarNode(content) } } -extension Node: Hashable { - static func == (lhs: Node, rhs: Node) -> Bool { - lhs.id == rhs.id +struct HeaderNode: Node, Hashable { + let id: ID + let name: String + let parent: ID? + + init(_ title: String) { + self.init(id: ID(title), name: NSLocalizedString(title, comment: "")) } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) + + private init(id: ID, name: String, parent: ID? = nil) { + self.id = id + self.name = name + self.parent = parent } -} -class HeaderNode: Node {} + func detach() -> Self { + self + } +} -class ServerNode: Node { +struct ServerNode: Node, Hashable { + let id: ID + let name: String + let parent: ID? var path: String { id.rawValue } - override func detach() -> Node { + init(id: ID, name: String, parent: ID? = nil) { + self.id = id + self.name = name + self.parent = parent + } + + func detach() -> Self { ServerNode(id: id, name: name) } } -class ShareNode: Node { +struct ShareNode: Node, Hashable { + let id: ID + let name: String + let parent: ID? + let device: String init(id: ID, device: String, name: String, parent: ID? = nil) { + self.id = id + self.name = name + self.parent = parent + self.device = device - super.init(id: id, name: name, parent: parent) } - override func detach() -> Node { + func detach() -> Self { ShareNode(id: id, device: device, name: name) } } -class FileNode: Node { +struct FileNode: Node, Hashable { + let id: ID + let name: String + let parent: ID? + let path: String let file: File @@ -89,12 +141,15 @@ class FileNode: Node { var isExpandable: Bool { isDirectory } init(path: String, file: File, parent: ID? = nil) { + id = ID(path) + name = file.name + self.parent = parent + self.path = path self.file = file - super.init(id: ID(path), name: file.name, parent: parent) } - override func detach() -> Node { + func detach() -> Self { FileNode(path: path, file: file) } } diff --git a/Examples/FileBrowser/FileBrowser (macOS)/WindowController.swift b/Examples/FileBrowser/FileBrowser (macOS)/WindowController.swift index d74dc3f..6ea500a 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/WindowController.swift +++ b/Examples/FileBrowser/FileBrowser (macOS)/WindowController.swift @@ -147,7 +147,7 @@ class WindowController: NSWindowController { var path = "" if menuItems.reversed().firstIndex(of: sender) == 1 { - guard let shares = DataRepository.shared.nodes(serverNode.path) else { return } + guard let shares: [ShareNode] = DataRepository.shared.nodes(serverNode.path) else { return } let sharesViewController = SharesViewController.instantiate(serverNode: serverNode, shares: Tree(nodes: shares)) navigationController.push(sharesViewController) return diff --git a/Examples/FileBrowser/FileBrowser.xcodeproj/project.pbxproj b/Examples/FileBrowser/FileBrowser.xcodeproj/project.pbxproj index 5603adc..73fba85 100644 --- a/Examples/FileBrowser/FileBrowser.xcodeproj/project.pbxproj +++ b/Examples/FileBrowser/FileBrowser.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 141AF3DF2C4A94D30050C7A5 /* NSOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141AF3DE2C4A94D30050C7A5 /* NSOutlineView.swift */; }; 141AF3E22C4ABA550050C7A5 /* SMBClient in Frameworks */ = {isa = PBXBuildFile; productRef = 141AF3E12C4ABA550050C7A5 /* SMBClient */; }; 141AF3E42C4ABCCA0050C7A5 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141AF3E32C4ABCCA0050C7A5 /* URL.swift */; }; + 141B02612C996599007A2573 /* ServiceDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B02602C996599007A2573 /* ServiceDiscovery.swift */; }; + 141B02622C996599007A2573 /* ServiceDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141B02602C996599007A2573 /* ServiceDiscovery.swift */; }; 1446801B2C347E1F0029A1AB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446801A2C347E1F0029A1AB /* AppDelegate.swift */; }; 1446801F2C347E200029A1AB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1446801E2C347E200029A1AB /* Assets.xcassets */; }; 144680222C347E200029A1AB /* Base in Resources */ = {isa = PBXBuildFile; fileRef = 144680212C347E200029A1AB /* Base */; }; @@ -25,7 +27,6 @@ 1446802C2C347FEB0029A1AB /* SplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446802B2C347FEB0029A1AB /* SplitViewController.swift */; }; 1446802E2C34800B0029A1AB /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446802D2C34800B0029A1AB /* SidebarViewController.swift */; }; 144680302C3482620029A1AB /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1446802F2C3482620029A1AB /* NavigationController.swift */; }; - 144680322C3488770029A1AB /* ServiceDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144680312C3488770029A1AB /* ServiceDiscovery.swift */; }; 144680362C349C0C0029A1AB /* Tree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 144680352C349C0C0029A1AB /* Tree.swift */; }; 145299932C34D523003B035C /* SidebarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 145299922C34D523003B035C /* SidebarManager.swift */; }; 145299962C34D95B003B035C /* SMBClient in Frameworks */ = {isa = PBXBuildFile; productRef = 145299952C34D95B003B035C /* SMBClient */; }; @@ -75,7 +76,6 @@ 14C23CAB2C53966A00CEA8CF /* MediaPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C23C9D2C53966A00CEA8CF /* MediaPlayerViewController.swift */; }; 14C23CAC2C53966A00CEA8CF /* DocumentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C23C9E2C53966A00CEA8CF /* DocumentViewController.swift */; }; 14C23CAD2C53966A00CEA8CF /* ServersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C23C9F2C53966A00CEA8CF /* ServersViewController.swift */; }; - 14C23CAE2C53966A00CEA8CF /* ServiceDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C23CA02C53966A00CEA8CF /* ServiceDiscovery.swift */; }; 14C23CAF2C53966A00CEA8CF /* ServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14C23CA12C53966A00CEA8CF /* ServerManager.swift */; }; 14C23CB22C5396C600CEA8CF /* FlyingFox in Frameworks */ = {isa = PBXBuildFile; productRef = 14C23CB12C5396C600CEA8CF /* FlyingFox */; }; 14C23CB42C5396C600CEA8CF /* FlyingSocks in Frameworks */ = {isa = PBXBuildFile; productRef = 14C23CB32C5396C600CEA8CF /* FlyingSocks */; }; @@ -89,6 +89,7 @@ 1414C4D42C630F29004E2E36 /* SMBAVAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMBAVAsset.swift; sourceTree = ""; }; 141AF3DE2C4A94D30050C7A5 /* NSOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSOutlineView.swift; sourceTree = ""; }; 141AF3E32C4ABCCA0050C7A5 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; + 141B02602C996599007A2573 /* ServiceDiscovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceDiscovery.swift; sourceTree = ""; }; 144680172C347E1F0029A1AB /* File Browser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "File Browser.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 1446801A2C347E1F0029A1AB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 1446801E2C347E200029A1AB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -98,7 +99,6 @@ 1446802B2C347FEB0029A1AB /* SplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitViewController.swift; sourceTree = ""; }; 1446802D2C34800B0029A1AB /* SidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewController.swift; sourceTree = ""; }; 1446802F2C3482620029A1AB /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; - 144680312C3488770029A1AB /* ServiceDiscovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceDiscovery.swift; sourceTree = ""; }; 144680352C349C0C0029A1AB /* Tree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tree.swift; sourceTree = ""; }; 145299922C34D523003B035C /* SidebarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarManager.swift; sourceTree = ""; }; 145299972C34E5FF003B035C /* ConnectService.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ConnectService.storyboard; sourceTree = ""; }; @@ -148,7 +148,6 @@ 14C23C9D2C53966A00CEA8CF /* MediaPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerViewController.swift; sourceTree = ""; }; 14C23C9E2C53966A00CEA8CF /* DocumentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentViewController.swift; sourceTree = ""; }; 14C23C9F2C53966A00CEA8CF /* ServersViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServersViewController.swift; sourceTree = ""; }; - 14C23CA02C53966A00CEA8CF /* ServiceDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceDiscovery.swift; sourceTree = ""; }; 14C23CA12C53966A00CEA8CF /* ServerManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -180,9 +179,10 @@ 1414C4D52C630F29004E2E36 /* Shared */ = { isa = PBXGroup; children = ( - 1414C4D12C630F29004E2E36 /* ID.swift */, - 1414C4D22C630F29004E2E36 /* Server.swift */, + 141B02602C996599007A2573 /* ServiceDiscovery.swift */, 1414C4D32C630F29004E2E36 /* Service.swift */, + 1414C4D22C630F29004E2E36 /* Server.swift */, + 1414C4D12C630F29004E2E36 /* ID.swift */, 1414C4D42C630F29004E2E36 /* SMBAVAsset.swift */, ); path = Shared; @@ -230,7 +230,6 @@ 14529A3D2C3E8B5D003B035C /* SessionManager.swift */, 145299922C34D523003B035C /* SidebarManager.swift */, 144680352C349C0C0029A1AB /* Tree.swift */, - 144680312C3488770029A1AB /* ServiceDiscovery.swift */, 147EC5B32C3FCF5B00C08ED3 /* ServerManager.swift */, 145299AB2C3612C6003B035C /* CredentialStore.swift */, 14529A392C3E23C6003B035C /* PathBarView.swift */, @@ -273,7 +272,6 @@ 14C23C9D2C53966A00CEA8CF /* MediaPlayerViewController.swift */, 14C23C9E2C53966A00CEA8CF /* DocumentViewController.swift */, 14C23CA12C53966A00CEA8CF /* ServerManager.swift */, - 14C23CA02C53966A00CEA8CF /* ServiceDiscovery.swift */, 14C23C962C53966900CEA8CF /* CredentialStore.swift */, 14C23C882C53962F00CEA8CF /* Main.storyboard */, 14C23C8D2C53962F00CEA8CF /* LaunchScreen.storyboard */, @@ -433,10 +431,10 @@ 1414C4D82C630F2E004E2E36 /* Service.swift in Sources */, 1414C4D92C630F2E004E2E36 /* ID.swift in Sources */, 14529A3C2C3E7724003B035C /* SidebarCellView.swift in Sources */, - 144680322C3488770029A1AB /* ServiceDiscovery.swift in Sources */, 144680302C3482620029A1AB /* NavigationController.swift in Sources */, 1452999A2C34F896003B035C /* ConnectServiceWindowController.swift in Sources */, 14529A3E2C3E8B5D003B035C /* SessionManager.swift in Sources */, + 141B02622C996599007A2573 /* ServiceDiscovery.swift in Sources */, 147EC5C02C41590400C08ED3 /* FileTransfer.swift in Sources */, 145299E72C3AC3DD003B035C /* DocumentWindowController.swift in Sources */, 144680362C349C0C0029A1AB /* Tree.swift in Sources */, @@ -467,8 +465,8 @@ 14C23CA32C53966A00CEA8CF /* ConnectServiceView.swift in Sources */, 14C23C852C53962F00CEA8CF /* SceneDelegate.swift in Sources */, 14C23CAB2C53966A00CEA8CF /* MediaPlayerViewController.swift in Sources */, + 141B02612C996599007A2573 /* ServiceDiscovery.swift in Sources */, 14C23CA62C53966A00CEA8CF /* ConnectServerView.swift in Sources */, - 14C23CAE2C53966A00CEA8CF /* ServiceDiscovery.swift in Sources */, 1414C4DA2C630F2E004E2E36 /* Server.swift in Sources */, 1414C4DB2C630F2E004E2E36 /* SMBAVAsset.swift in Sources */, 1414C4DC2C630F2E004E2E36 /* Service.swift in Sources */, diff --git a/Examples/FileBrowser/Shared/ID.swift b/Examples/FileBrowser/Shared/ID.swift index 841956e..4d9b952 100644 --- a/Examples/FileBrowser/Shared/ID.swift +++ b/Examples/FileBrowser/Shared/ID.swift @@ -1,10 +1,10 @@ import Foundation struct ID: RawRepresentable, Codable { - var rawValue: String + let rawValue: String init(_ rawValue: String) { - self.rawValue = rawValue + self.init(rawValue: rawValue) } init(rawValue: String) { diff --git a/Examples/FileBrowser/Shared/Service.swift b/Examples/FileBrowser/Shared/Service.swift index 61b2e3d..a978730 100644 --- a/Examples/FileBrowser/Shared/Service.swift +++ b/Examples/FileBrowser/Shared/Service.swift @@ -1,19 +1,16 @@ import Foundation -import Network struct Service { let id: ID let name: String let type: String let domain: String - let interface: NWInterface? - init(name: String, type: String, domain: String, interface: NWInterface?) { - id = ID("smb://\(name)._smb._tcp.local") + init(name: String, type: String, domain: String) { + id = ID("smb://\(name).\(type).\(domain)") self.name = name self.type = type self.domain = domain - self.interface = interface } } diff --git a/Examples/FileBrowser/FileBrowser (macOS)/ServiceDiscovery.swift b/Examples/FileBrowser/Shared/ServiceDiscovery.swift similarity index 74% rename from Examples/FileBrowser/FileBrowser (macOS)/ServiceDiscovery.swift rename to Examples/FileBrowser/Shared/ServiceDiscovery.swift index 2ef8efb..a96c83f 100644 --- a/Examples/FileBrowser/FileBrowser (macOS)/ServiceDiscovery.swift +++ b/Examples/FileBrowser/Shared/ServiceDiscovery.swift @@ -6,7 +6,7 @@ class ServiceDiscovery { static let serviceDidDiscover = Notification.Name("ServiceDiscoveryServiceDidDiscover") private let browser: NWBrowser - private(set) var services: [Service] + private(set) var services = Set() private init() { let params = NWParameters() @@ -14,19 +14,15 @@ class ServiceDiscovery { let descriptor = NWBrowser.Descriptor.bonjour(type: "_smb._tcp", domain: nil) browser = NWBrowser(for: descriptor, using: params) - - services = [Service]() } func start() { browser.browseResultsChangedHandler = { (results, changes) in for result in results { switch result.endpoint { - case .service(name: let name, type: let type, domain: let domain, interface: let interface): - let service = Service(name: name, type: type, domain: domain, interface: interface) - guard !self.services.contains(service) else { return } - - self.services.append(service) + case let .service(name, type, domain, _): + let service = Service(name: name, type: type, domain: domain) + self.services.insert(service) NotificationCenter.default.post(name: Self.serviceDidDiscover, object: self) case .hostPort, .unix, .url, .opaque: