You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

嵌入NSMenu的NSCollectionView在菜单未显示时无法正常更新

NSCollectionView嵌入NSMenu后滚动失效问题

我希望将NSCollectionView嵌入NSMenu中,实现方式非常简单:

private func configurePlaylistLibraryMenu() {
        let playlistlibraryvievController = PlaylistLabraryViewController(player: player, extensionHandler: extensionHandler)
        let item = NSMenuItem()
        item.view = playlistlibraryvievController.view
        playlistLibraryMenu.addItem(item)
    }

@IBAction func showPlaylists(_ sender: NSButton) {
        let position = NSPoint(x: sender.frame.origin.x, y: sender.frame.origin.y - 5)
        playlistLibraryMenu.popUp(positioning: playlistLibraryMenu.item(at: 0), at: position, in: view)
    }

对应的PlaylistLabraryViewController代码如下:

import Cocoa
import Combine
import SDWebImage

class PlaylistLabraryViewController: NSViewController {

    struct ElementKind {
        static let overlay = "overlay-element-kind"
    }
    
    enum Section {
        case main
    }
    
    @IBOutlet weak var collectionView: NSCollectionView!
    
    private var dataSource: NSCollectionViewDiffableDataSource<Section, SourceInfo>! = nil
    private var publishers = Set<AnyCancellable>()
    private var playlistLibrary: [SourceInfo] = []
    private var extensionHandler: SafariExtensionHandler?
    private var player: Player

    init(player: Player, extensionHandler: SafariExtensionHandler?) {
        self.player = player
        self.extensionHandler = extensionHandler
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented") 
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configurePlaylistCollectionView()
        configurePublishers()
    }

    private func configurePlaylistCollectionView() {
        collectionView.backgroundColors = [.clear]
        configureHierarchy()
        configureDataSource()
    }
    
    private func configurePublishers() {     
        player.publisher(for: \.playlistDataArray).sink { [self] playlistDataArray in
            var newPlaylistLibrary: [SourceInfo] = []
            playlistDataArray.forEach {
                do {
                    let playlistInfo = try PropertyListDecoder().decode(SourceInfo.self, from: $0)
                    newPlaylistLibrary.append(playlistInfo)
                } catch {
                    NSApp.presentError(error)
                }
            }
            let difference = newPlaylistLibrary.difference(from: playlistLibrary, by: { $0.link == $1.link })
            var currentSnapshot = dataSource.snapshot()
            var animate = true
            for change in difference {
                switch change {
                case .insert(_ , let playlist, _):
                    playlistLibrary.append(playlist)
                    currentSnapshot.appendItems([playlist])
                    animate = false
                case .remove(_ ,let playlist, _):
                    playlistLibrary.remove(playlist)
                    currentSnapshot.deleteItems([playlist])
                    animate = true
                }
            }
            dataSource.apply(currentSnapshot, animatingDifferences: animate)
        }.store(in: &publishers)
    }
}

// MARK: - Playlist Library actions

extension PlaylistLabraryViewController {

    private func deletePlaylist(at indexPath: IndexPath) {
        let _ = player.playlistDataArray.remove(at: indexPath.item)
        player.savePlaylistDataArray()
    }
    
    private func pausePlayer() {
        extensionHandler?.performPlayerAction(.togglePause, actionProperties: [PlayerAction.togglePause.string: 1])
    }
    
    private func playPlaylist(at indexPath: IndexPath) {
        let playlist = playlistLibrary[indexPath.item]
        extensionHandler?.performPlayerAction(.playPlaylist, actionProperties: [PlayerAction.playPlaylist.string: playlist.link])
    }
    
    private func toggleLike() {
        log(#function)
    }
}

// MARK: - CollectionView methods

extension PlaylistLabraryViewController {

    private func configureHierarchy() {
        let itemNib = NSNib(nibNamed: "PlaylistItem", bundle: nil)
        collectionView.register(itemNib, forItemWithIdentifier: PlaylistItem.reuseIdentifier)
        collectionView.collectionViewLayout = createLayout()
    }

    private func configureDataSource() {
        dataSource = NSCollectionViewDiffableDataSource<Section, SourceInfo>(collectionView: collectionView, itemProvider: {
                (collectionView: NSCollectionView, indexPath: IndexPath, item: SourceInfo) -> NSCollectionViewItem? in
            let playlistItem = collectionView.makeItem(withIdentifier: PlaylistItem.reuseIdentifier, for: indexPath) as? PlaylistItem
            playlistItem?.textField?.stringValue = item.title
            playlistItem?.imageView?.image = item.coverImage
            return playlistItem
        })
        var snapshot = NSDiffableDataSourceSnapshot<Section, SourceInfo>()
        snapshot.appendSections([Section.main])
        snapshot.appendItems(playlistLibrary)
        dataSource.apply(snapshot, animatingDifferences: false)
    }
    
    private func createLayout() -> NSCollectionViewLayout {
        let width:  CGFloat = 120
        let height: CGFloat = 139
        let inset:  CGFloat = 5
 
        let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(width), heightDimension: .absolute(height))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(width * 4), heightDimension: .absolute(height))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset * 2, bottom: inset, trailing: inset * 2)
        let layout = NSCollectionViewCompositionalLayout(section: section)
        return layout
    }
}

在.xib文件中设置的CollectionView尺寸对应两行四列共8个项。添加项的操作在菜单关闭(即CollectionView未显示)时进行:当项数不超过8个时,打开菜单后新增项能正常显示;但添加第9个项时,本应新增一行并支持滚动,实际却未生效。

我尝试在菜单打开后立即更新CollectionView:

override func viewDidAppear() {
    super.viewDidAppear()
    var snapshot = NSDiffableDataSourceSnapshot<Section, SourceInfo>()
    snapshot.appendSections([Section.main])
    snapshot.appendItems(playlistLibrary)
    dataSource.apply(snapshot, animatingDifferences: false)
    collectionView.invalidateIntrinsicContentSize()
}

但这并未解决问题。有趣的是,若重启扩展(从数据库读取超过8个项),CollectionView能正常显示并支持滚动。


内容的提问来源于stack exchange,提问作者Nikolai Nagornyi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 02:02:11