From 5da036f67e01fbba226fc49234d84bfdfffa954b Mon Sep 17 00:00:00 2001 From: RCCoop Date: Fri, 30 Dec 2022 09:38:02 -0500 Subject: [PATCH 1/6] Implemented force-reload of OutlineView data --- Sources/OutlineView/OutlineView.swift | 18 +++++-- .../OutlineView/OutlineViewController.swift | 23 ++++++++- .../OutlineView/OutlineViewReloading.swift | 50 +++++++++++++++++++ 3 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 Sources/OutlineView/OutlineViewReloading.swift diff --git a/Sources/OutlineView/OutlineView.swift b/Sources/OutlineView/OutlineView.swift index dfc0892..9c97ace 100644 --- a/Sources/OutlineView/OutlineView.swift +++ b/Sources/OutlineView/OutlineView.swift @@ -2,10 +2,11 @@ import SwiftUI import Cocoa @available(macOS 10.15, *) -public struct OutlineView: NSViewControllerRepresentable +public struct OutlineView: NSViewControllerRepresentable where Data.Element: Identifiable { - public typealias NSViewControllerType = OutlineViewController + public typealias NSViewControllerType = OutlineViewController + let id: ID let data: Data let children: KeyPath @Binding var selection: Data.Element? @@ -48,6 +49,8 @@ where Data.Element: Identifiable { /// /// - Parameters: /// - data: A collection of tree-structured, identified data. + /// - id: A unique identifier that can be used to force reloads of the + /// outlineView data by calling `triggerReloadOfOutlineView(id:)` /// - children: A key path to a property whose non-`nil` value gives the /// children of `data`. A non-`nil` but empty value denotes an element /// capable of having children that's currently childless, such as an @@ -61,10 +64,12 @@ where Data.Element: Identifiable { /// as it is used to determine the height of the cell. public init( _ data: Data, + id: ID, children: KeyPath, selection: Binding, content: @escaping (Data.Element) -> NSView ) { + self.id = id self.data = data self.children = children self._selection = selection @@ -92,6 +97,8 @@ where Data.Element: Identifiable { /// /// - Parameters: /// - data: A collection of tree-structured, identified data. + /// - id: A unique identifier that can be used to force reloads of the + /// outlineView data by calling `triggerReloadOfOutlineView(id:)` /// - children: A key path to a property whose non-`nil` value gives the /// children of `data`. A non-`nil` but empty value denotes an element /// capable of having children that's currently childless, such as an @@ -106,11 +113,13 @@ where Data.Element: Identifiable { @available(macOS 11.0, *) public init( _ data: Data, + id: ID, children: KeyPath, selection: Binding, separatorInsets: @escaping (Data.Element) -> NSEdgeInsets, content: @escaping (Data.Element) -> NSView ) { + self.id = id self.data = data self.children = children self._selection = selection @@ -119,8 +128,9 @@ where Data.Element: Identifiable { self.content = content } - public func makeNSViewController(context: Context) -> OutlineViewController { + public func makeNSViewController(context: Context) -> NSViewControllerType { let controller = OutlineViewController( + id: id, data: data, children: children, content: content, @@ -134,7 +144,7 @@ where Data.Element: Identifiable { } public func updateNSViewController( - _ outlineController: OutlineViewController, + _ outlineController: NSViewControllerType, context: Context ) { outlineController.updateData(newValue: data) diff --git a/Sources/OutlineView/OutlineViewController.swift b/Sources/OutlineView/OutlineViewController.swift index dbdc391..041f6e0 100644 --- a/Sources/OutlineView/OutlineViewController.swift +++ b/Sources/OutlineView/OutlineViewController.swift @@ -1,18 +1,22 @@ import Cocoa +import Combine @available(macOS 10.15, *) -public class OutlineViewController: NSViewController +public class OutlineViewController: NSViewController where Data.Element: Identifiable { + let id: ID let outlineView = NSOutlineView() let scrollView = NSScrollView(frame: NSRect(x: 0, y: 0, width: 400, height: 400)) let dataSource: OutlineViewDataSource let delegate: OutlineViewDelegate let updater = OutlineViewUpdater() + var reloadListener: AnyCancellable? let childrenPath: KeyPath init( + id: ID, data: Data, children: KeyPath, content: @escaping (Data.Element) -> NSView, @@ -44,6 +48,7 @@ where Data.Element: Identifiable { childrenPath = children + self.id = id super.init(nibName: nil, bundle: nil) view.addSubview(scrollView) @@ -54,6 +59,13 @@ where Data.Element: Identifiable { scrollView.topAnchor.constraint(equalTo: view.topAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) + + reloadListener = NotificationCenter + .default + .publisher(for: .OutlineViewReload) + .compactMap { $0.outlineId(as: ID.self) } + .filter { $0 == id } + .sink(receiveValue: { _ in self.reloadRowData(itemIds: nil) }) } required init?(coder: NSCoder) { @@ -124,4 +136,13 @@ extension OutlineViewController { outlineView.gridColor = color outlineView.reloadData() } + + /// Calls `reloadData()` on the outlineView. Currently does not + /// use individual itemIds, although if implemented you could + /// reload individual rows. OutlineViewController and DataSource + /// would need to be changed in that case. + func reloadRowData(itemIds: [Data.Element.ID]?) { + outlineView.reloadData() + } + } diff --git a/Sources/OutlineView/OutlineViewReloading.swift b/Sources/OutlineView/OutlineViewReloading.swift new file mode 100644 index 0000000..b7abd84 --- /dev/null +++ b/Sources/OutlineView/OutlineViewReloading.swift @@ -0,0 +1,50 @@ + +import Combine +import Foundation + +extension Notification.Name { + static var OutlineViewReload: Self { + .init("ReloadOutlineViewNotification") + } +} + +/// Manually forces an OutlineView to reload its row data, which +/// may be necessary if normal state property changes don't cause +/// data updates. +/// - Parameter id: The id of the OutlineView to be reloaded. +public func triggerReloadOfOutlineView(id: K) { + NotificationCenter.default.post( + name: .OutlineViewReload, + object: nil, + userInfo: ["id" : id] + ) +} + +/* + // Could be implemented if OutlineView data source was able to look up + // data items by ID +public func triggerReloadOfOutlineView(id: K, itemIds: [L]) { + NotificationCenter.default.post( + name: .OutlineViewReload, + object: nil, + userInfo: [ + "id" : id, + "items" : itemIds + ] + ) +} + */ + +internal extension Notification { + + func outlineId(as type: K.Type) -> K? { + userInfo?["id"] as? K + } + + /* + // See `triggerReloadOfOutlineView(id:itemIds:)` + func outlineItemIds(as type: L.Type) -> [L]? { + userInfo?["items"] as? [L] + } + */ +} From 662cdca663fe00a813c03d172d723959c11afcd2 Mon Sep 17 00:00:00 2001 From: RCCoop Date: Sun, 1 Jan 2023 14:12:32 -0500 Subject: [PATCH 2/6] Added force-reload for individual items --- .../OutlineView/OutlineViewController.swift | 36 ++++++++++++++++--- .../OutlineView/OutlineViewReloading.swift | 16 +++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/Sources/OutlineView/OutlineViewController.swift b/Sources/OutlineView/OutlineViewController.swift index 041f6e0..f7449ea 100644 --- a/Sources/OutlineView/OutlineViewController.swift +++ b/Sources/OutlineView/OutlineViewController.swift @@ -63,9 +63,11 @@ where Data.Element: Identifiable { reloadListener = NotificationCenter .default .publisher(for: .OutlineViewReload) - .compactMap { $0.outlineId(as: ID.self) } - .filter { $0 == id } - .sink(receiveValue: { _ in self.reloadRowData(itemIds: nil) }) + .filter { $0.outlineId(as: ID.self) == id } + .sink(receiveValue: { note in + let itemIds = note.outlineItemIds(as: Data.Element.ID.self) + self.reloadRowData(itemIds: itemIds) + }) } required init?(coder: NSCoder) { @@ -142,7 +144,33 @@ extension OutlineViewController { /// reload individual rows. OutlineViewController and DataSource /// would need to be changed in that case. func reloadRowData(itemIds: [Data.Element.ID]?) { - outlineView.reloadData() + + // If no itemIds are given, reload everything + guard let itemIds, + !itemIds.isEmpty + else { + outlineView.reloadData() + return + } + + var itemsToReload = Set(itemIds) + let rowCount = outlineView.numberOfRows + var currentRow = 0 + + // Reload individual rows while IDs are available + while currentRow < rowCount, + !itemsToReload.isEmpty + { + if let rowItem = outlineView.item(atRow: currentRow), + let identifiableItem = rowItem as? any Identifiable, + let itemId = identifiableItem.id as? Data.Element.ID, + itemsToReload.contains(itemId) + { + outlineView.reloadItem(rowItem) + itemsToReload.remove(itemId) + } + currentRow += 1 + } } } diff --git a/Sources/OutlineView/OutlineViewReloading.swift b/Sources/OutlineView/OutlineViewReloading.swift index b7abd84..60841a2 100644 --- a/Sources/OutlineView/OutlineViewReloading.swift +++ b/Sources/OutlineView/OutlineViewReloading.swift @@ -20,9 +20,14 @@ public func triggerReloadOfOutlineView(id: K) { ) } -/* - // Could be implemented if OutlineView data source was able to look up - // data items by ID +/// Manually forces an OutlineView to reload its row data for a given +/// group of rows by id, which may be necessary if normal state property +/// changes don't cause data updates. +/// +/// - Parameters: +/// - id: The id of the OutlineView to be reloaded. +/// - itemIds: Array of ids of items in the `OutlineView` that need to +/// be reloaded. public func triggerReloadOfOutlineView(id: K, itemIds: [L]) { NotificationCenter.default.post( name: .OutlineViewReload, @@ -33,7 +38,6 @@ public func triggerReloadOfOutlineView(id: K, itemIds: ] ) } - */ internal extension Notification { @@ -41,10 +45,8 @@ internal extension Notification { userInfo?["id"] as? K } - /* - // See `triggerReloadOfOutlineView(id:itemIds:)` func outlineItemIds(as type: L.Type) -> [L]? { userInfo?["items"] as? [L] } - */ + } From fe3f03ab63571e61a2bad72f02cb363aa693243b Mon Sep 17 00:00:00 2001 From: RCCoop Date: Sun, 1 Jan 2023 14:40:52 -0500 Subject: [PATCH 3/6] Added data reload to example project --- .../project.pbxproj | 4 ++-- .../OutlineViewExample/ContentView.swift | 24 ++++++++++++++++++- .../OutlineViewExample/FileItemView.swift | 3 ++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Example/OutlineViewExample/OutlineViewExample.xcodeproj/project.pbxproj b/Example/OutlineViewExample/OutlineViewExample.xcodeproj/project.pbxproj index 7797e73..d242755 100644 --- a/Example/OutlineViewExample/OutlineViewExample.xcodeproj/project.pbxproj +++ b/Example/OutlineViewExample/OutlineViewExample.xcodeproj/project.pbxproj @@ -291,7 +291,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"OutlineViewExample/Preview Content\""; - DEVELOPMENT_TEAM = 47YH33JD86; + DEVELOPMENT_TEAM = AMSDNQC8GP; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = OutlineViewExample/Info.plist; @@ -315,7 +315,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_ASSET_PATHS = "\"OutlineViewExample/Preview Content\""; - DEVELOPMENT_TEAM = 47YH33JD86; + DEVELOPMENT_TEAM = AMSDNQC8GP; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = OutlineViewExample/Info.plist; diff --git a/Example/OutlineViewExample/OutlineViewExample/ContentView.swift b/Example/OutlineViewExample/OutlineViewExample/ContentView.swift index a54c45d..c0d61b6 100644 --- a/Example/OutlineViewExample/OutlineViewExample/ContentView.swift +++ b/Example/OutlineViewExample/OutlineViewExample/ContentView.swift @@ -54,6 +54,11 @@ struct ContentView: View { @State var separatorColor: Color = Color(NSColor.separatorColor) @State var separatorEnabled = false + @State var rootTextColor: Color = Color(NSColor.textColor) + @State var childTextColor: Color = Color(NSColor.textColor) + + let outlineId = UUID() + var body: some View { VStack { outlineView @@ -66,10 +71,15 @@ struct ContentView: View { : Color.clear ) } + + var rootIds: [UUID] { + data.map(\.id) + } var outlineView: some View { OutlineView( data, + id: outlineId, children: \.children, selection: $selection, separatorInsets: { fileItem in @@ -80,16 +90,28 @@ struct ContentView: View { right: 0) } ) { fileItem in - FileItemView(fileItem: fileItem) + FileItemView(fileItem: fileItem, textColor: NSColor(rootIds.contains(fileItem.id) ? rootTextColor : childTextColor)) } .outlineViewStyle(.inset) .outlineViewIndentation(20) .rowSeparator(separatorEnabled ? .visible : .hidden) .rowSeparatorColor(NSColor(separatorColor)) + .onChange(of: rootTextColor) { _ in + triggerReloadOfOutlineView(id: outlineId, itemIds: rootIds) + } + .onChange(of: childTextColor) { _ in + triggerReloadOfOutlineView(id: outlineId) + } } var configBar: some View { HStack { + ColorPicker( + "Root Text Color:", + selection: $rootTextColor) + Button( + "Set Child Text Color", + action: { self.childTextColor = self.rootTextColor }) Spacer() ColorPicker( "Set separator color:", diff --git a/Example/OutlineViewExample/OutlineViewExample/FileItemView.swift b/Example/OutlineViewExample/OutlineViewExample/FileItemView.swift index 9212fea..a1ca511 100644 --- a/Example/OutlineViewExample/OutlineViewExample/FileItemView.swift +++ b/Example/OutlineViewExample/OutlineViewExample/FileItemView.swift @@ -8,7 +8,7 @@ import Cocoa class FileItemView: NSTableCellView { - init(fileItem: FileItem) { + init(fileItem: FileItem, textColor: NSColor = .textColor) { let field = NSTextField(string: fileItem.description) field.isEditable = false field.isSelectable = false @@ -17,6 +17,7 @@ class FileItemView: NSTableCellView { field.usesSingleLineMode = false field.cell?.wraps = true field.cell?.isScrollable = false + field.textColor = textColor super.init(frame: .zero) From 3f0a7c63146de10c0f68bd010391dcbdb0f4044f Mon Sep 17 00:00:00 2001 From: RCCoop Date: Wed, 11 Jan 2023 16:30:15 -0500 Subject: [PATCH 4/6] Cleaning and retain cycle fix --- Sources/OutlineView/OutlineViewController.swift | 8 +++----- Sources/OutlineView/OutlineViewReloading.swift | 4 +--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Sources/OutlineView/OutlineViewController.swift b/Sources/OutlineView/OutlineViewController.swift index f7449ea..8a2fc02 100644 --- a/Sources/OutlineView/OutlineViewController.swift +++ b/Sources/OutlineView/OutlineViewController.swift @@ -64,10 +64,10 @@ where Data.Element: Identifiable { .default .publisher(for: .OutlineViewReload) .filter { $0.outlineId(as: ID.self) == id } - .sink(receiveValue: { note in + .sink { [weak self] note in let itemIds = note.outlineItemIds(as: Data.Element.ID.self) - self.reloadRowData(itemIds: itemIds) - }) + self?.reloadRowData(itemIds: itemIds) + } } required init?(coder: NSCoder) { @@ -144,7 +144,6 @@ extension OutlineViewController { /// reload individual rows. OutlineViewController and DataSource /// would need to be changed in that case. func reloadRowData(itemIds: [Data.Element.ID]?) { - // If no itemIds are given, reload everything guard let itemIds, !itemIds.isEmpty @@ -172,5 +171,4 @@ extension OutlineViewController { currentRow += 1 } } - } diff --git a/Sources/OutlineView/OutlineViewReloading.swift b/Sources/OutlineView/OutlineViewReloading.swift index 60841a2..794dd04 100644 --- a/Sources/OutlineView/OutlineViewReloading.swift +++ b/Sources/OutlineView/OutlineViewReloading.swift @@ -39,8 +39,7 @@ public func triggerReloadOfOutlineView(id: K, itemIds: ) } -internal extension Notification { - +extension Notification { func outlineId(as type: K.Type) -> K? { userInfo?["id"] as? K } @@ -48,5 +47,4 @@ internal extension Notification { func outlineItemIds(as type: L.Type) -> [L]? { userInfo?["items"] as? [L] } - } From a536b715e6ad96da59029a6720350b9756e18f0b Mon Sep 17 00:00:00 2001 From: RCCoop Date: Sat, 1 Apr 2023 10:15:48 -0400 Subject: [PATCH 5/6] Simplified Reload code and added example project --- .../OutlineViewExample/ContentView.swift | 18 +- .../project.pbxproj | 361 ++++++++++++++++++ .../OutlineViewReloadExample.xcscheme | 77 ++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 58 +++ .../Assets.xcassets/Contents.json | 6 + .../ContentView.swift | 131 +++++++ .../FileItemView.swift | 38 ++ .../OutlineViewReloadExample/Info.plist | 24 ++ .../OutlineViewReloadExample.entitlements | 10 + .../OutlineViewReloadExampleApp.swift | 17 + .../Preview Assets.xcassets/Contents.json | 6 + Sources/OutlineView/OutlineView.swift | 26 +- .../OutlineView/OutlineViewController.swift | 14 +- .../OutlineView/OutlineViewReloading.swift | 21 +- 15 files changed, 775 insertions(+), 43 deletions(-) create mode 100644 Examples/OutlineViewReloadExample/OutlineViewReloadExample.xcodeproj/project.pbxproj create mode 100644 Examples/OutlineViewReloadExample/OutlineViewReloadExample.xcodeproj/xcshareddata/xcschemes/OutlineViewReloadExample.xcscheme create mode 100644 Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/Contents.json create mode 100644 Examples/OutlineViewReloadExample/OutlineViewReloadExample/ContentView.swift create mode 100644 Examples/OutlineViewReloadExample/OutlineViewReloadExample/FileItemView.swift create mode 100644 Examples/OutlineViewReloadExample/OutlineViewReloadExample/Info.plist create mode 100644 Examples/OutlineViewReloadExample/OutlineViewReloadExample/OutlineViewReloadExample.entitlements create mode 100644 Examples/OutlineViewReloadExample/OutlineViewReloadExample/OutlineViewReloadExampleApp.swift create mode 100644 Examples/OutlineViewReloadExample/OutlineViewReloadExample/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/Examples/OutlineViewExample/OutlineViewExample/ContentView.swift b/Examples/OutlineViewExample/OutlineViewExample/ContentView.swift index 1f6b784..159dce9 100644 --- a/Examples/OutlineViewExample/OutlineViewExample/ContentView.swift +++ b/Examples/OutlineViewExample/OutlineViewExample/ContentView.swift @@ -56,9 +56,7 @@ struct ContentView: View { @State var rootTextColor: Color = Color(NSColor.textColor) @State var childTextColor: Color = Color(NSColor.textColor) - - let outlineId = UUID() - + var body: some View { VStack { outlineView @@ -89,28 +87,16 @@ struct ContentView: View { right: 0) } ) { fileItem in - FileItemView(fileItem: fileItem, textColor: NSColor(rootIds.contains(fileItem.id) ? rootTextColor : childTextColor)) + FileItemView(fileItem: fileItem) } .outlineViewStyle(.inset) .outlineViewIndentation(20) .rowSeparator(separatorEnabled ? .visible : .hidden) .rowSeparatorColor(NSColor(separatorColor)) - .onChange(of: rootTextColor) { _ in - triggerReloadOfOutlineView(id: outlineId, itemIds: rootIds) - } - .onChange(of: childTextColor) { _ in - triggerReloadOfOutlineView(id: outlineId) - } } var configBar: some View { HStack { - ColorPicker( - "Root Text Color:", - selection: $rootTextColor) - Button( - "Set Child Text Color", - action: { self.childTextColor = self.rootTextColor }) Spacer() ColorPicker( "Set separator color:", diff --git a/Examples/OutlineViewReloadExample/OutlineViewReloadExample.xcodeproj/project.pbxproj b/Examples/OutlineViewReloadExample/OutlineViewReloadExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9cf3f89 --- /dev/null +++ b/Examples/OutlineViewReloadExample/OutlineViewReloadExample.xcodeproj/project.pbxproj @@ -0,0 +1,361 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 336C5FE225CC9F1600230C37 /* OutlineViewReloadExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336C5FE125CC9F1600230C37 /* OutlineViewReloadExampleApp.swift */; }; + 336C5FE425CC9F1600230C37 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336C5FE325CC9F1600230C37 /* ContentView.swift */; }; + 336C5FE625CC9F1800230C37 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 336C5FE525CC9F1800230C37 /* Assets.xcassets */; }; + 336C5FE925CC9F1800230C37 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 336C5FE825CC9F1800230C37 /* Preview Assets.xcassets */; }; + 336C5FFD25CCA8DA00230C37 /* FileItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336C5FFC25CCA8D900230C37 /* FileItemView.swift */; }; + 336C600325CCAEE700230C37 /* OutlineView in Frameworks */ = {isa = PBXBuildFile; productRef = 336C600225CCAEE700230C37 /* OutlineView */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 336C5FDE25CC9F1600230C37 /* OutlineViewReloadExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OutlineViewReloadExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 336C5FE125CC9F1600230C37 /* OutlineViewReloadExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlineViewReloadExampleApp.swift; sourceTree = ""; }; + 336C5FE325CC9F1600230C37 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 336C5FE525CC9F1800230C37 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 336C5FE825CC9F1800230C37 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 336C5FEA25CC9F1800230C37 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 336C5FEB25CC9F1800230C37 /* OutlineViewReloadExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OutlineViewReloadExample.entitlements; sourceTree = ""; }; + 336C5FFC25CCA8D900230C37 /* FileItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileItemView.swift; sourceTree = ""; }; + 336C600025CCAEDA00230C37 /* OutlineView */ = {isa = PBXFileReference; lastKnownFileType = folder; name = OutlineView; path = ../..; sourceTree = ""; }; + 94C6EB1229D870C6007A19EB /* OutlineViewReloadExample.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = OutlineViewReloadExample.xcconfig; path = ../../../../../../../../../Desktop/OutlineViewReloadExample.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 336C5FDB25CC9F1500230C37 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 336C600325CCAEE700230C37 /* OutlineView in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 336C5FD525CC9F1500230C37 = { + isa = PBXGroup; + children = ( + 336C600025CCAEDA00230C37 /* OutlineView */, + 336C5FE025CC9F1600230C37 /* OutlineViewReloadExample */, + 336C5FDF25CC9F1600230C37 /* Products */, + 336C5FF825CC9F7600230C37 /* Frameworks */, + ); + sourceTree = ""; + }; + 336C5FDF25CC9F1600230C37 /* Products */ = { + isa = PBXGroup; + children = ( + 336C5FDE25CC9F1600230C37 /* OutlineViewReloadExample.app */, + ); + name = Products; + sourceTree = ""; + }; + 336C5FE025CC9F1600230C37 /* OutlineViewReloadExample */ = { + isa = PBXGroup; + children = ( + 336C5FE125CC9F1600230C37 /* OutlineViewReloadExampleApp.swift */, + 336C5FE325CC9F1600230C37 /* ContentView.swift */, + 336C5FFC25CCA8D900230C37 /* FileItemView.swift */, + 336C5FE525CC9F1800230C37 /* Assets.xcassets */, + 336C5FEA25CC9F1800230C37 /* Info.plist */, + 336C5FEB25CC9F1800230C37 /* OutlineViewReloadExample.entitlements */, + 336C5FE725CC9F1800230C37 /* Preview Content */, + 94C6EB1229D870C6007A19EB /* OutlineViewReloadExample.xcconfig */, + ); + path = OutlineViewReloadExample; + sourceTree = ""; + }; + 336C5FE725CC9F1800230C37 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 336C5FE825CC9F1800230C37 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 336C5FF825CC9F7600230C37 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 336C5FDD25CC9F1500230C37 /* OutlineViewReloadExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 336C5FEE25CC9F1800230C37 /* Build configuration list for PBXNativeTarget "OutlineViewReloadExample" */; + buildPhases = ( + 336C5FDA25CC9F1500230C37 /* Sources */, + 336C5FDB25CC9F1500230C37 /* Frameworks */, + 336C5FDC25CC9F1500230C37 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OutlineViewReloadExample; + packageProductDependencies = ( + 336C600225CCAEE700230C37 /* OutlineView */, + ); + productName = OutlineViewExample; + productReference = 336C5FDE25CC9F1600230C37 /* OutlineViewReloadExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 336C5FD625CC9F1500230C37 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1220; + LastUpgradeCheck = 1220; + TargetAttributes = { + 336C5FDD25CC9F1500230C37 = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = 336C5FD925CC9F1500230C37 /* Build configuration list for PBXProject "OutlineViewReloadExample" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 336C5FD525CC9F1500230C37; + productRefGroup = 336C5FDF25CC9F1600230C37 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 336C5FDD25CC9F1500230C37 /* OutlineViewReloadExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 336C5FDC25CC9F1500230C37 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 336C5FE925CC9F1800230C37 /* Preview Assets.xcassets in Resources */, + 336C5FE625CC9F1800230C37 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 336C5FDA25CC9F1500230C37 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 336C5FFD25CCA8DA00230C37 /* FileItemView.swift in Sources */, + 336C5FE425CC9F1600230C37 /* ContentView.swift in Sources */, + 336C5FE225CC9F1600230C37 /* OutlineViewReloadExampleApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 336C5FED25CC9F1800230C37 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 94C6EB1229D870C6007A19EB /* OutlineViewReloadExample.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 336C5FF025CC9F1800230C37 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 94C6EB1229D870C6007A19EB /* OutlineViewReloadExample.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = OutlineViewReloadExample/OutlineViewReloadExample.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_ASSET_PATHS = "\"OutlineViewReloadExample/Preview Content\""; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = OutlineViewReloadExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.OutlineViewReloadExample${SAMPLE_CODE_DISAMBIGUATOR}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 94C6EB1029D86FFB007A19EB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 94C6EB1229D870C6007A19EB /* OutlineViewReloadExample.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Debug; + }; + 94C6EB1129D86FFB007A19EB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 94C6EB1229D870C6007A19EB /* OutlineViewReloadExample.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = OutlineViewReloadExample/OutlineViewReloadExample.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_ASSET_PATHS = "\"OutlineViewReloadExample/Preview Content\""; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = OutlineViewReloadExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.OutlineViewReloadExample${SAMPLE_CODE_DISAMBIGUATOR}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 336C5FD925CC9F1500230C37 /* Build configuration list for PBXProject "OutlineViewReloadExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 336C5FED25CC9F1800230C37 /* Release */, + 94C6EB1029D86FFB007A19EB /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 336C5FEE25CC9F1800230C37 /* Build configuration list for PBXNativeTarget "OutlineViewReloadExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 336C5FF025CC9F1800230C37 /* Release */, + 94C6EB1129D86FFB007A19EB /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 336C600225CCAEE700230C37 /* OutlineView */ = { + isa = XCSwiftPackageProductDependency; + productName = OutlineView; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 336C5FD625CC9F1500230C37 /* Project object */; +} diff --git a/Examples/OutlineViewReloadExample/OutlineViewReloadExample.xcodeproj/xcshareddata/xcschemes/OutlineViewReloadExample.xcscheme b/Examples/OutlineViewReloadExample/OutlineViewReloadExample.xcodeproj/xcshareddata/xcschemes/OutlineViewReloadExample.xcscheme new file mode 100644 index 0000000..754ee61 --- /dev/null +++ b/Examples/OutlineViewReloadExample/OutlineViewReloadExample.xcodeproj/xcshareddata/xcschemes/OutlineViewReloadExample.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/Contents.json b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/OutlineViewReloadExample/OutlineViewReloadExample/ContentView.swift b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/ContentView.swift new file mode 100644 index 0000000..87ee777 --- /dev/null +++ b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/ContentView.swift @@ -0,0 +1,131 @@ +// +// ContentView.swift +// OutlineViewExample +// +// Created by Samar Sunkaria on 2/4/21. +// + +import SwiftUI +import OutlineView +import Cocoa + +struct FileItem: Hashable, Identifiable, CustomStringConvertible { + var id = UUID() + var name: String + var children: [FileItem]? = nil + var description: String { + switch children { + case nil: + return "📄 \(name)" + case .some(let children): + return children.isEmpty ? "📂 \(name)" : "📁 \(name)" + } + } +} + +let data = [ + FileItem(name: "doc001.txt"), + FileItem( + name: "users", + children: [ + FileItem( + name: "user1234", + children: [ + FileItem( + name: "Photos", + children: [ + FileItem(name: "photo001.jpg"), + FileItem(name: "photo002.jpg")]), + FileItem( + name: "Movies", + children: [FileItem(name: "movie001.mp4")]), + FileItem(name: "Documents", children: [])]), + FileItem( + name: "newuser", + children: [FileItem(name: "Documents", children: [])]) + ] + ) +] + +struct ContentView: View { + @Environment(\.colorScheme) var colorScheme + + @State var selection: FileItem? + @State var separatorColor: Color = Color(NSColor.separatorColor) + @State var separatorEnabled = false + + @State var rootTextColor: Color = Color(NSColor.textColor) + @State var childTextColor: Color = Color(NSColor.textColor) + + @State var outlineId = UUID() + + var body: some View { + VStack { + outlineView + Divider() + configBar + } + .background( + colorScheme == .light + ? Color(NSColor.textBackgroundColor) + : Color.clear + ) + } + + var rootIds: [UUID] { + data.map(\.id) + } + + var outlineView: some View { + OutlineView( + data, + selection: $selection, + children: \.children, + separatorInsets: { fileItem in + NSEdgeInsets( + top: 0, + left: 23, + bottom: 0, + right: 0) + } + ) { fileItem in + FileItemView(fileItem: fileItem, textColor: NSColor(rootIds.contains(fileItem.id) ? rootTextColor : childTextColor)) + } + .outlineViewStyle(.inset) + .outlineViewIndentation(20) + .rowSeparator(separatorEnabled ? .visible : .hidden) + .rowSeparatorColor(NSColor(separatorColor)) + .reloadIdentifier(outlineId) + .onChange(of: rootTextColor) { _ in + triggerReloadOfOutlineView(id: outlineId, itemIds: rootIds) + } + .onChange(of: childTextColor) { _ in + triggerReloadOfOutlineView(id: outlineId) + } + } + + var configBar: some View { + HStack { + ColorPicker( + "Root Text Color:", + selection: $rootTextColor) + Button( + "Set Child Text Color", + action: { self.childTextColor = self.rootTextColor }) + Spacer() + ColorPicker( + "Set separator color:", + selection: $separatorColor) + Button( + "Toggle separator", + action: { separatorEnabled.toggle() }) + } + .padding([.leading, .bottom, .trailing], 8) + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Examples/OutlineViewReloadExample/OutlineViewReloadExample/FileItemView.swift b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/FileItemView.swift new file mode 100644 index 0000000..8f1c2b9 --- /dev/null +++ b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/FileItemView.swift @@ -0,0 +1,38 @@ +// +// FileItemView.swift +// OutlineViewExample +// +// Created by Samar Sunkaria on 2/4/21. +// + +import Cocoa + +class FileItemView: NSTableCellView { + init(fileItem: FileItem, textColor: NSColor) { + let field = NSTextField(string: fileItem.description) + field.isEditable = false + field.isSelectable = false + field.isBezeled = false + field.drawsBackground = false + field.usesSingleLineMode = false + field.cell?.wraps = true + field.cell?.isScrollable = false + field.textColor = textColor + + super.init(frame: .zero) + + addSubview(field) + field.translatesAutoresizingMaskIntoConstraints = false + field.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + NSLayoutConstraint.activate([ + field.leadingAnchor.constraint(equalTo: leadingAnchor), + field.trailingAnchor.constraint(equalTo: trailingAnchor), + field.topAnchor.constraint(equalTo: topAnchor, constant: 4), + field.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Info.plist b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Info.plist new file mode 100644 index 0000000..69c84ae --- /dev/null +++ b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + + diff --git a/Examples/OutlineViewReloadExample/OutlineViewReloadExample/OutlineViewReloadExample.entitlements b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/OutlineViewReloadExample.entitlements new file mode 100644 index 0000000..f2ef3ae --- /dev/null +++ b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/OutlineViewReloadExample.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Examples/OutlineViewReloadExample/OutlineViewReloadExample/OutlineViewReloadExampleApp.swift b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/OutlineViewReloadExampleApp.swift new file mode 100644 index 0000000..a01f178 --- /dev/null +++ b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/OutlineViewReloadExampleApp.swift @@ -0,0 +1,17 @@ +// +// OutlineViewExampleApp.swift +// OutlineViewExample +// +// Created by Samar Sunkaria on 2/4/21. +// + +import SwiftUI + +@main +struct OutlineViewExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Examples/OutlineViewReloadExample/OutlineViewReloadExample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/OutlineView/OutlineView.swift b/Sources/OutlineView/OutlineView.swift index 673cdfe..4664493 100644 --- a/Sources/OutlineView/OutlineView.swift +++ b/Sources/OutlineView/OutlineView.swift @@ -16,16 +16,16 @@ enum ChildSource { } @available(macOS 10.15, *) -public struct OutlineView: NSViewControllerRepresentable +public struct OutlineView: NSViewControllerRepresentable where Drop.DataElement == Data.Element { - public typealias NSViewControllerType = OutlineViewController + public typealias NSViewControllerType = OutlineViewController - let id: ID let data: Data let childSource: ChildSource @Binding var selection: Data.Element? var content: (Data.Element) -> NSView var separatorInsets: ((Data.Element) -> NSEdgeInsets)? + var reloadID = UUID() /// Outline view style is unavailable on macOS 10.15 and below. /// Stored as `Any` to make the property available on all platforms. @@ -76,6 +76,8 @@ where Drop.DataElement == Data.Element { outlineController.setDragSourceWriter(dragDataSource) outlineController.setDropReceiver(dropReceiver) outlineController.setAcceptedDragTypes(acceptedDropTypes) + + outlineController.setReloadID(reloadID) } } @@ -142,6 +144,15 @@ public extension OutlineView { mutableSelf.dragDataSource = writer return mutableSelf } + + /// Sets the unique reload identifier for the `OutlineView`, which can be + /// used to force data reloads using the static function + /// `triggerReloadOfOutlineView(id:itemIds:)` + func reloadIdentifier(_ reloadID: UUID) -> Self { + var mutableSelf = self + mutableSelf.reloadID = reloadID + return mutableSelf + } } // MARK: - Initializers for macOS 10.15 and higher. @@ -166,8 +177,6 @@ public extension OutlineView { /// /// - Parameters: /// - data: A collection of tree-structured, identified data. - /// - id: A unique identifier that can be used to force reloads of the - /// outlineView data by calling `triggerReloadOfOutlineView(id:)` /// - children: A key path to a property whose non-`nil` value gives the /// children of `data`. A non-`nil` but empty value denotes an element /// capable of having children that's currently childless, such as an @@ -181,12 +190,10 @@ public extension OutlineView { /// as it is used to determine the height of the cell. init( _ data: Data, - id: ID, children: KeyPath, selection: Binding, content: @escaping (Data.Element) -> NSView ) { - self.id = id self.data = data self.childSource = .keyPath(children) self._selection = selection @@ -259,8 +266,6 @@ public extension OutlineView where Drop == NoDropReceiver { /// /// - Parameters: /// - data: A collection of tree-structured, identified data. - /// - id: A unique identifier that can be used to force reloads of the - /// outlineView data by calling `triggerReloadOfOutlineView(id:)` /// - children: A key path to a property whose non-`nil` value gives the /// children of `data`. A non-`nil` but empty value denotes an element /// capable of having children that's currently childless, such as an @@ -274,12 +279,10 @@ public extension OutlineView where Drop == NoDropReceiver { /// as it is used to determine the height of the cell. init( _ data: Data, - id: ID, children: KeyPath, selection: Binding, content: @escaping (Data.Element) -> NSView ) { - self.id = id self.data = data self.childSource = .keyPath(children) self._selection = selection @@ -317,7 +320,6 @@ public extension OutlineView where Drop == NoDropReceiver { /// The `NSView` should return the correct `fittingSize` /// as it is used to determine the height of the cell. init( - id: id, _ data: Data, selection: Binding, children: @escaping (Data.Element) -> Data?, diff --git a/Sources/OutlineView/OutlineViewController.swift b/Sources/OutlineView/OutlineViewController.swift index c3830f5..3398e16 100644 --- a/Sources/OutlineView/OutlineViewController.swift +++ b/Sources/OutlineView/OutlineViewController.swift @@ -2,12 +2,12 @@ import Cocoa import Combine @available(macOS 10.15, *) -public class OutlineViewController: NSViewController +public class OutlineViewController: NSViewController where Drop.DataElement == Data.Element { let outlineView = NSOutlineView() let scrollView = NSScrollView(frame: NSRect(x: 0, y: 0, width: 400, height: 400)) - let id: ID + var reloadID = UUID() let dataSource: OutlineViewDataSource let delegate: OutlineViewDelegate let updater = OutlineViewUpdater() @@ -16,7 +16,6 @@ where Drop.DataElement == Data.Element { let childrenSource: ChildSource init( - id: ID, data: Data, childrenSource: ChildSource, content: @escaping (Data.Element) -> NSView, @@ -48,7 +47,6 @@ where Drop.DataElement == Data.Element { outlineView.dataSource = dataSource outlineView.delegate = delegate - self.id = id self.childrenSource = childrenSource super.init(nibName: nil, bundle: nil) @@ -65,7 +63,9 @@ where Drop.DataElement == Data.Element { reloadListener = NotificationCenter .default .publisher(for: .OutlineViewReload) - .filter { $0.outlineId(as: ID.self) == id } + .filter { [weak self] in + $0.outlineId == self?.reloadID + } .sink { [weak self] note in let itemIds = note.outlineItemIds(as: Data.Element.ID.self) self?.reloadRowData(itemIds: itemIds) @@ -192,4 +192,8 @@ extension OutlineViewController { outlineView.registerForDraggedTypes(acceptedTypes) } } + + func setReloadID(_ newID: UUID) { + self.reloadID = newID + } } diff --git a/Sources/OutlineView/OutlineViewReloading.swift b/Sources/OutlineView/OutlineViewReloading.swift index 794dd04..70ea28b 100644 --- a/Sources/OutlineView/OutlineViewReloading.swift +++ b/Sources/OutlineView/OutlineViewReloading.swift @@ -8,11 +8,12 @@ extension Notification.Name { } } -/// Manually forces an OutlineView to reload its row data, which -/// may be necessary if normal state property changes don't cause +/// Forces an `OutlineView` to reload its row data, which may +/// be necessary if normal state property changes don't cause /// data updates. -/// - Parameter id: The id of the OutlineView to be reloaded. -public func triggerReloadOfOutlineView(id: K) { +/// +/// - Parameter id: The reloadIdentifier of the `OutlineView` to reload. +public func triggerReloadOfOutlineView(id: UUID) { NotificationCenter.default.post( name: .OutlineViewReload, object: nil, @@ -20,15 +21,15 @@ public func triggerReloadOfOutlineView(id: K) { ) } -/// Manually forces an OutlineView to reload its row data for a given -/// group of rows by id, which may be necessary if normal state property +/// Forces an `OutlineView` to reload its row data for a given group +/// of rows by id, which may be necessary if normal state property /// changes don't cause data updates. /// /// - Parameters: -/// - id: The id of the OutlineView to be reloaded. +/// - id: The reloadIdentifier of the `OutlineView` to reload. /// - itemIds: Array of ids of items in the `OutlineView` that need to /// be reloaded. -public func triggerReloadOfOutlineView(id: K, itemIds: [L]) { +public func triggerReloadOfOutlineView(id: UUID, itemIds: [L]) { NotificationCenter.default.post( name: .OutlineViewReload, object: nil, @@ -40,8 +41,8 @@ public func triggerReloadOfOutlineView(id: K, itemIds: } extension Notification { - func outlineId(as type: K.Type) -> K? { - userInfo?["id"] as? K + var outlineId: UUID? { + userInfo?["id"] as? UUID } func outlineItemIds(as type: L.Type) -> [L]? { From e650297707d911d5b9e30f1804d68ad1f0f9bc1d Mon Sep 17 00:00:00 2001 From: RCCoop Date: Sat, 1 Apr 2023 10:55:35 -0400 Subject: [PATCH 6/6] Fixed an issue with reloading single items --- Sources/OutlineView/OutlineViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OutlineView/OutlineViewController.swift b/Sources/OutlineView/OutlineViewController.swift index 3398e16..b01c6ab 100644 --- a/Sources/OutlineView/OutlineViewController.swift +++ b/Sources/OutlineView/OutlineViewController.swift @@ -169,7 +169,7 @@ extension OutlineViewController { let itemId = identifiableItem.id as? Data.Element.ID, itemsToReload.contains(itemId) { - outlineView.reloadItem(rowItem) + outlineView.reloadItem(rowItem, reloadChildren: outlineView.isItemExpanded(rowItem)) itemsToReload.remove(itemId) } currentRow += 1