Nikolai Nagornyi
Nikolai Nagornyi

Reputation: 1461

NSCollectionView inside NSMenu don‘t update properly when menu not shown

I want to put an NSCollectionView inside a menu. It's pretty simple:

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)
    }

The 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
    }
}

The size of the CollectionView set in the .xib file means two rows of four items. Adding items to the CollectionView happens when the menu is closed, meaning the CollectionView is not on the screen. As long as the number of elements does not exceed eight, everything is fine, when you open the menu, the added elements are displayed. Adding the 9th item should add a new row and allow the CollectionView to scroll, but it doesn't. I was trying to update the CollectionView right after opening my menu:

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

but that didn't solve the problem. Interestingly, if the extension is restarted (when reading the playlist library from the database with more than 8 items), the CollectionView is displayed correctly and scrolling is possible. Please help me solve this problem. I've already broken my brain.

Test app on github

Upvotes: 0

Views: 66

Answers (0)

Related Questions