Skip to content

Commit 24c40bf

Browse files
committed
Simplify codebase with NotificationObserver helper and convenience methods
Add NotificationObserver helper class to eliminate repetitive notification observer management patterns across MenuController and ModelManager. This consolidates observer lifecycle handling (registration, removal, cleanup) into a single reusable component. Add convenience methods to ModelManager (isInstalled, isDownloading, downloadProgress) to reduce pattern matching boilerplate in view layer. These methods provide cleaner, more readable alternatives to repeatedly checking ModelStatus enums. Remove duplicate isModelDownloaded method in favor of the new isInstalled method for consistency. Changes: - Add Common/NotificationObserver.swift with automatic cleanup in deinit - Refactor MenuController to use NotificationObserver (removed deinit, simplified addObservers) - Refactor ModelManager to use NotificationObserver and add convenience methods - Update InstalledModelItemView to use convenience methods instead of status pattern matching - Update Sections.swift to use convenience methods in filter logic Net result: -27 lines, improved readability, reduced duplication.
1 parent 182e916 commit 24c40bf

File tree

5 files changed

+101
-98
lines changed

5 files changed

+101
-98
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
3+
/// Manages notification observers with automatic cleanup.
4+
/// Eliminates boilerplate for adding/removing NotificationCenter observers.
5+
final class NotificationObserver {
6+
private var tokens: [NSObjectProtocol] = []
7+
8+
/// Adds an observer for the specified notification name.
9+
func observe(
10+
_ name: Notification.Name, object: Any? = nil, using block: @escaping (Notification) -> Void
11+
) {
12+
let token = NotificationCenter.default.addObserver(
13+
forName: name,
14+
object: object,
15+
queue: .main,
16+
using: block
17+
)
18+
tokens.append(token)
19+
}
20+
21+
/// Removes all registered observers.
22+
func removeAll() {
23+
tokens.forEach { NotificationCenter.default.removeObserver($0) }
24+
tokens.removeAll()
25+
}
26+
27+
deinit {
28+
tokens.forEach { NotificationCenter.default.removeObserver($0) }
29+
}
30+
}

LlamaBarn/Menu/InstalledModelItemView.swift

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -153,29 +153,24 @@ final class InstalledModelItemView: ItemView, NSGestureRecognizerDelegate {
153153
}
154154

155155
private func toggle() {
156-
let status = modelManager.status(for: model)
157-
switch status {
158-
case .installed:
156+
if modelManager.isInstalled(model) {
159157
if server.isActive(model: model) { server.stop() } else { server.start(model: model) }
160-
case .downloading:
158+
} else if modelManager.isDownloading(model) {
161159
modelManager.cancelModelDownload(model)
162160
membershipChanged(model)
163-
case .available:
164-
break
165161
}
166162
refresh()
167163
}
168164

169165
func refresh() {
170-
let status = modelManager.status(for: model)
171166
let isActive = server.isActive(model: model)
172167
let isLoading = isActive && server.isLoading
173168

174169
modelNameLabel.stringValue = model.fullName
175170
metadataLabel.attributedStringValue = ModelMetadataFormatters.makeMetadataTextOnly(for: model)
176171

177172
// Progress and cancel button only for downloading
178-
if case .downloading(let progress) = status {
173+
if let progress = modelManager.downloadProgress(for: model) {
179174
modelNameLabel.textColor = Typography.secondaryColor
180175
progressLabel.stringValue = ProgressFormatters.percentText(progress)
181176
cancelImageView.isHidden = false
@@ -188,7 +183,7 @@ final class InstalledModelItemView: ItemView, NSGestureRecognizerDelegate {
188183
}
189184

190185
// Delete button only for installed models on hover
191-
deleteLabel.isHidden = status != .installed || !isHighlighted
186+
deleteLabel.isHidden = !modelManager.isInstalled(model) || !isHighlighted
192187

193188
// Update icon state
194189
iconView.setLoading(isLoading)
@@ -198,8 +193,7 @@ final class InstalledModelItemView: ItemView, NSGestureRecognizerDelegate {
198193
}
199194

200195
override func highlightDidChange(_ highlighted: Bool) {
201-
let status = modelManager.status(for: model)
202-
deleteLabel.isHidden = status != .installed || !highlighted
196+
deleteLabel.isHidden = !modelManager.isInstalled(model) || !highlighted
203197
}
204198

205199
override func viewDidChangeEffectiveAppearance() {
@@ -212,8 +206,7 @@ final class InstalledModelItemView: ItemView, NSGestureRecognizerDelegate {
212206
}
213207

214208
@objc private func performDelete() {
215-
let status = modelManager.status(for: model)
216-
guard case .installed = status else { return }
209+
guard modelManager.isInstalled(model) else { return }
217210
modelManager.deleteDownloadedModel(model)
218211
membershipChanged(model)
219212
}

LlamaBarn/Menu/MenuController.swift

Lines changed: 39 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ final class MenuController: NSObject, NSMenuDelegate {
2626

2727
private var isSettingsVisible = false
2828
private var menuWidth: CGFloat = 260
29-
private var observers: [NSObjectProtocol] = []
29+
private let observer = NotificationObserver()
3030
private weak var currentlyHighlightedView: ItemView?
3131

3232
init(modelManager: ModelManager? = nil, server: LlamaServer? = nil) {
@@ -37,10 +37,6 @@ final class MenuController: NSObject, NSMenuDelegate {
3737
configureStatusItem()
3838
}
3939

40-
deinit {
41-
observers.forEach { NotificationCenter.default.removeObserver($0) }
42-
}
43-
4440
private func configureStatusItem() {
4541
if let button = statusItem.button {
4642
button.image =
@@ -72,7 +68,7 @@ final class MenuController: NSObject, NSMenuDelegate {
7268
guard menu === statusItem.menu else { return }
7369
currentlyHighlightedView?.setHighlight(false)
7470
currentlyHighlightedView = nil
75-
removeObservers()
71+
observer.removeAll()
7672
isSettingsVisible = false
7773
}
7874

@@ -125,80 +121,62 @@ final class MenuController: NSObject, NSMenuDelegate {
125121

126122
// Observe server and download changes while the menu is open.
127123
private func addObservers() {
128-
removeObservers()
129-
let center = NotificationCenter.default
124+
observer.removeAll()
130125

131126
// Server started/stopped - update icon and views
132-
observers.append(
133-
center.addObserver(forName: .LBServerStateDidChange, object: nil, queue: .main) {
134-
[weak self] _ in
135-
MainActor.assumeIsolated {
136-
self?.refresh()
137-
}
138-
})
127+
observer.observe(.LBServerStateDidChange) { [weak self] _ in
128+
MainActor.assumeIsolated {
129+
self?.refresh()
130+
}
131+
}
139132

140133
// Server memory usage changed - update running model stats
141-
observers.append(
142-
center.addObserver(forName: .LBServerMemoryDidChange, object: nil, queue: .main) {
143-
[weak self] _ in
144-
MainActor.assumeIsolated {
145-
self?.refresh()
146-
}
147-
})
134+
observer.observe(.LBServerMemoryDidChange) { [weak self] _ in
135+
MainActor.assumeIsolated {
136+
self?.refresh()
137+
}
138+
}
148139

149140
// Download progress updated - refresh progress indicators
150-
observers.append(
151-
center.addObserver(forName: .LBModelDownloadsDidChange, object: nil, queue: .main) {
152-
[weak self] _ in
153-
MainActor.assumeIsolated {
154-
self?.refresh()
155-
}
156-
})
141+
observer.observe(.LBModelDownloadsDidChange) { [weak self] _ in
142+
MainActor.assumeIsolated {
143+
self?.refresh()
144+
}
145+
}
157146

158147
// Model downloaded or deleted - rebuild both installed and catalog sections
159-
observers.append(
160-
center.addObserver(forName: .LBModelDownloadedListDidChange, object: nil, queue: .main) {
161-
[weak self] _ in
162-
MainActor.assumeIsolated {
163-
if let menu = self?.statusItem.menu {
164-
self?.installedSection.rebuild(in: menu)
165-
self?.catalogSection.rebuild(in: menu)
166-
}
167-
self?.refresh()
148+
observer.observe(.LBModelDownloadedListDidChange) { [weak self] _ in
149+
MainActor.assumeIsolated {
150+
if let menu = self?.statusItem.menu {
151+
self?.installedSection.rebuild(in: menu)
152+
self?.catalogSection.rebuild(in: menu)
168153
}
169-
})
154+
self?.refresh()
155+
}
156+
}
170157

171158
// Settings visibility toggled - rebuild menu
172-
observers.append(
173-
center.addObserver(forName: .LBToggleSettingsVisibility, object: nil, queue: .main) {
174-
[weak self] _ in
175-
MainActor.assumeIsolated {
176-
self?.isSettingsVisible.toggle()
177-
if let menu = self?.statusItem.menu {
178-
self?.rebuildMenu(menu)
179-
}
159+
observer.observe(.LBToggleSettingsVisibility) { [weak self] _ in
160+
MainActor.assumeIsolated {
161+
self?.isSettingsVisible.toggle()
162+
if let menu = self?.statusItem.menu {
163+
self?.rebuildMenu(menu)
180164
}
181-
})
165+
}
166+
}
182167

183168
// User settings changed (e.g., show quantized models) - rebuild menu
184-
observers.append(
185-
center.addObserver(forName: .LBUserSettingsDidChange, object: nil, queue: .main) {
186-
[weak self] _ in
187-
MainActor.assumeIsolated {
188-
if let menu = self?.statusItem.menu {
189-
self?.rebuildMenu(menu)
190-
}
169+
observer.observe(.LBUserSettingsDidChange) { [weak self] _ in
170+
MainActor.assumeIsolated {
171+
if let menu = self?.statusItem.menu {
172+
self?.rebuildMenu(menu)
191173
}
192-
})
174+
}
175+
}
193176

194177
refresh()
195178
}
196179

197-
private func removeObservers() {
198-
observers.forEach { NotificationCenter.default.removeObserver($0) }
199-
observers.removeAll()
200-
}
201-
202180
private func refresh() {
203181
if let button = statusItem.button {
204182
let running = server.isRunning

LlamaBarn/Menu/Sections.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,7 @@ final class InstalledSection {
136136
}
137137

138138
private func installedModels() -> [CatalogEntry] {
139-
let downloading = Catalog.allEntries().filter {
140-
if case .downloading = modelManager.status(for: $0) { return true }
141-
return false
142-
}
139+
let downloading = Catalog.allEntries().filter { modelManager.isDownloading($0) }
143140
return (modelManager.downloadedModels + downloading)
144141
.sorted(by: CatalogEntry.displayOrder(_:_:))
145142
}
@@ -261,8 +258,7 @@ final class CatalogSection {
261258
private func filterAvailableModels() -> [CatalogEntry] {
262259
let showQuantized = UserSettings.showQuantizedModels
263260
return Catalog.allEntries().filter { model in
264-
let status = modelManager.status(for: model)
265-
let isAvailable = status == .available
261+
let isAvailable = !modelManager.isInstalled(model) && !modelManager.isDownloading(model)
266262
let isCompatible = Catalog.isModelCompatible(model)
267263
return isAvailable && isCompatible && (showQuantized || model.isFullPrecision)
268264
}

LlamaBarn/Models/ModelManager.swift

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,14 @@ class ModelManager: NSObject {
3535

3636
private let downloader = ModelDownloader.shared
3737
private let logger = Logger(subsystem: Logging.subsystem, category: "ModelManager")
38-
private var observers: [NSObjectProtocol] = []
38+
private let observer = NotificationObserver()
3939

4040
override init() {
4141
super.init()
4242
refreshDownloadedModels()
4343
addObservers()
4444
}
4545

46-
deinit {
47-
observers.forEach { NotificationCenter.default.removeObserver($0) }
48-
}
49-
5046
/// Downloads a model by delegating to the downloader.
5147
func downloadModel(_ model: CatalogEntry) throws {
5248
try downloader.downloadModel(model)
@@ -64,11 +60,6 @@ class ModelManager: NSObject {
6460
return .available
6561
}
6662

67-
/// Checks if a model has been completely downloaded.
68-
func isModelDownloaded(_ model: CatalogEntry) -> Bool {
69-
downloadedModelIds.contains(model.id)
70-
}
71-
7263
/// Safely deletes a downloaded model and its associated files.
7364
func deleteDownloadedModel(_ model: CatalogEntry) {
7465
let llamaServer = LlamaServer.shared
@@ -128,17 +119,32 @@ class ModelManager: NSObject {
128119
downloader.cancelModelDownload(model)
129120
}
130121

122+
// MARK: - Convenience Methods
123+
124+
/// Returns true if the model is installed (fully downloaded).
125+
func isInstalled(_ model: CatalogEntry) -> Bool {
126+
status(for: model) == .installed
127+
}
128+
129+
/// Returns true if the model is currently downloading.
130+
func isDownloading(_ model: CatalogEntry) -> Bool {
131+
if case .downloading = status(for: model) { return true }
132+
return false
133+
}
134+
135+
/// Returns the download progress if the model is currently downloading, nil otherwise.
136+
func downloadProgress(for model: CatalogEntry) -> Progress? {
137+
if case .downloading(let progress) = status(for: model) { return progress }
138+
return nil
139+
}
140+
131141
private func addObservers() {
132-
let center = NotificationCenter.default
133142
// When the downloader finishes a set of files for a model, it posts this notification.
134143
// We observe it to refresh our list of fully downloaded models.
135-
observers.append(
136-
center.addObserver(forName: .LBModelDownloadFinished, object: downloader, queue: .main) {
137-
[weak self] _ in
138-
MainActor.assumeIsolated {
139-
self?.refreshDownloadedModels()
140-
}
144+
observer.observe(.LBModelDownloadFinished, object: downloader) { [weak self] _ in
145+
MainActor.assumeIsolated {
146+
self?.refreshDownloadedModels()
141147
}
142-
)
148+
}
143149
}
144150
}

0 commit comments

Comments
 (0)