Focus for UICollectionViewCells on tvOS - objective-c

I'm new to tvOS. I have created UICollectionView using a XIB file in Objective-C. How can I focus UICollectionViewCell?
I have a label and an image view in the cell, and I want to focus both the label and the image view.

UICollectionViewCell are not focus appearance by default, but you can achive this by adding one yourself.
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
if (self.focused)
{
// Apply focused appearence,
// e.g scale both of them using transform or apply background color
}
else
{
// Apply normal appearance
}
}
OR
If you just want to focus ImageView like scaling it up when collection view cell get focus you can do like this in awakeFromNib method
self.imageView.adjustsImageWhenAncestorFocused = YES;
self.clipToBounds = NO;

Add this in custom class of collectionview:
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
if (self.focused) {
collectionView.image.adjustsImageWhenAncestorFocused = true
} else {
collectionView.image.adjustsImageWhenAncestorFocused = false
}
}

Add below methods to your collectionview cell.It will show title of focussed cell only giving complete look and feel of focus.
override func awakeFromNib() {
super.awakeFromNib()
// These properties are also exposed in Interface Builder.
imageView.adjustsImageWhenAncestorFocused = true
imageView.clipsToBounds = false
title.alpha = 0.0
}
// MARK: UICollectionReusableView
override func prepareForReuse() {
super.prepareForReuse()
// Reset the label's alpha value so it's initially hidden.
title.alpha = 0.0
}
// MARK: UIFocusEnvironment
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
/*
Update the label's alpha value using the `UIFocusAnimationCoordinator`.
This will ensure all animations run alongside each other when the focus
changes.
*/
coordinator.addCoordinatedAnimations({
if self.focused {
self.title.alpha = 1.0
}
else {
self.title.alpha = 0.0
}
}, completion: nil)
}

Related

UITextView text content doesn't start from the top

I have a long text coming from my JSON file but when I click the link from my UITableViewCell to go to my UIViewController page, the UITextView text loads the string content but it does not show the content from the beginning and I have to scroll up all the time.
What I need to do?
I had the same problem, and turns out I had to set the content offset in viewDidLayoutSubviews for it to take effect. I'm using this code to display attributed static text.
- (void)viewDidLayoutSubviews {
[self.yourTextView setContentOffset:CGPointZero animated:NO];
}
SWIFT 3:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.textView.setContentOffset(CGPoint.zero, animated: false)
}
This is the only way that worked for me. I disable the scroll of the UITextView before the view is loaded and then i enable it again:
override func viewWillAppear(_ animated: Bool) {
yourTextView.isScrollEnabled = false
}
override func viewDidAppear(_ animated: Bool) {
yourTextView.isScrollEnabled = true
}
[self.textView scrollRangeToVisible:NSMakeRange(0, 1)];
in viewDidLoad
By Programmatically before loading the content disable the scrolling property of textview
textview.scrollenabled = NO;
And after loading enable the scrolling of textview textview.scrollenabled = YES;
As well check the XIB, always non-check the scrolling enabled of Textview.
The answers for the question Blank space at top of UITextView in iOS 10 provide a much cleaner end user experience.
In viewDidLoad of the view controller containing the text view:
self.automaticallyAdjustsScrollViewInsets = false
Setting textView.setContentOffset(CGPointMake(0,0), animated: false) and some of these other suggestions do work when called in the viewDidLayoutSubviews() but on older devices like iPad 2 and older you will actually see the text get scrolled when the screen is displayed. That is not something you want the end user to see.
I was still having problems after using these solutions. The problem definitely seems to relate to having transparent navigation bars and selecting to automatically adjust content insets on the view controller. If you don't care about your text scrolling underneath the navigation bar then it's best to leave these settings off and constrain the top of your textview to the bottom of the navigation bar, rather than to the top of the viewcontroller.
If like me you wanted it to appear underneath your navigation bar when you scroll down; then the solution that worked for me was to add this.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
CGFloat offset = self.navigationController.navigationBar.frame.size.height+[UIApplication sharedApplication].statusBarFrame.size.height;
[self.textView setContentOffset:CGPointMake(0, -offset) animated:NO];
}
This just looks for the height of the navigation bar and status bar and adjusts the content offset accordingly.
Note that one downside of this approach is that when the device rotates you'll end up scrolling back to the top.
For me fine works this code:
textView.attributedText = newText //or textView.text = ...
//this part of code scrolls to top
textView.contentOffset.y = -64 //or = 0 if no Navigation Bar
textView.scrollEnabled = false
textView.layoutIfNeeded()
textView.scrollEnabled = true
For scroll to exact position and show it on top of screen I use this code:
var scrollToLocation = 50 //<needed position>
textView.contentOffset.y = textView.contentSize.height
textView.scrollRangeToVisible(NSRange.init(location: scrollToLocation, length: 1))
Setting contentOffset.y scrolls to the end of text, and then scrollRangeToVisible scrolls up to value of scrollToLocation. Thereby, needed position appears in first line of scrollView.
Similar to some other answers, but with the added benefit that you won't cause a scroll to top on subsequent device rotations. Works well in Swift 2.2
/// Whether view has laid out subviews at least once before.
var viewDidLayoutSubviewsAtLeastOnce = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !viewDidLayoutSubviewsAtLeastOnce {
textView.setContentOffset(CGPoint(x: 0, y: -textView.contentInset.top), animated: false)
}
viewDidLayoutSubviewsAtLeastOnce = true
}
Swift Version
A combination of things will be needed:
1.) Set your outlet
#IBOutlet var textView: UITextView!
2.) In storyboard on View Controller turn off "Adjust Scroll View Insets"
3.) Set content to zero top by adding below to your view controller
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
myUITextView.setContentOffset(CGPointZero, animated: false)
}
Instead of setting content offset from viewDidLayoutSubviews you can write layoutIfNeeded from viewDidLoad to set the proper position of textview as below:
self.textView.layoutIfNeeded()
self.textView.setContentOffset(CGPoint.zero, animated: false)
Cheers !!
In Swift 2, Xcode 7 solution, to leave scroll Enabled as well as have the text start at the top, this is all you need:
#IBOutlet weak var myUITextView: UITextView!
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
//start scroll at top of text
myUITextView.scrollRangeToVisible(NSMakeRange(0, 0))
}
Swift 3.0
override func viewWillAppear(_ animated: Bool) {
privacyText.isScrollEnabled = false
}
override func viewDidAppear(_ animated: Bool) {
privacyText.isScrollEnabled = true
}
This worked the best for me! I placed this within viewDidLoad().
//TextView Scroll starts from the top
myTextView.contentOffset.y = 0
Here's another way to do it that always works for me. Objective-C:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.textView setContentOffset:CGPointZero animated:NO];
}
And in Swift:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
textView.setContentOffset(CGPointZero, animated: false)
}
Swift version:
override func viewDidLayoutSubviews() {
yourTextView.setContentOffset(CGPointZero, animated: false)
}
add the following function to your view controller class...
Swift 3
override func viewDidLayoutSubviews() {
self.mainTextView.setContentOffset(.zero, animated: false)
}
Swift 2.1
override func viewDidLayoutSubviews() {
self.mainTextView.setContentOffset(CGPointZero, animated: false)
}
Objective C
- (void)viewDidLayoutSubviews {
[self.mainTextView setContentOffset:CGPointZero animated:NO];
}
或者 你在ViewDidAppear 里面加上滚动,这样用户会看到他往上滚动到第一行
in swift 4 with attributed text any of answer does not help me and i combine some answers in topic.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
uiTextview.isScrollEnabled = false
}
override func viewDidAppear(_ animated: Bool) {
uiTextview.isScrollEnabled = true
uiTextview.setContentOffset(CGPoint.zero, animated: false)
}
Swift 3, 4, 5 solution:
Steps to solve the issue:
Disable the UITextView scroll
set scrollRectToVisible
enable UITextView scroll
Code:
yourTextView.isScrollEnabled = false
let rect:CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
yourTextView.scrollRectToVisible(rect, animated: false)
yourTextView.isScrollEnabled = true
This Worked for me. Hope that will help!
This is how i did it. I subclassed textview, and:
override func willMoveToSuperview(newSuperview: UIView?) {
self.scrollEnabled = false
}
override func layoutSubviews() {
super.layoutSubviews()
self.scrollEnabled = true
}
From storyboard, select the view controller on which you have you text view placed. In the attributes inspector, uncheck "Adjust Scroll View Insets". That's it.
Put this code on your class
override func viewDidLayoutSubviews() {
self.About_TV.setContentOffset(.zero, animated: false) // About_TV : your text view name)
}
Add code to the viewdidload
self.automaticallyAdjustsScrollViewInsets = NO;

hidesBarsOnSwipe never shows navbar again when scrolling up

So I want to hide the navbar when scrolling down and bring it back when scrolling up. Hiding it works perfectly with
self.navigationController?.hidesBarsOnSwipe = true
But I expect it to be shown again when scrolling up. I made a test project where the view controller just has a single UICollectionView that covers the whole screen. Then showing the navbar is shown again as expected until I add this line to the viewDidLoad (adding cells to the collection view):
self.collectionView.delegate = self
And this is what the whole view controller looks like
class ViewController: UIViewController,UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.collectionView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "Test")
self.navigationController?.hidesBarsOnSwipe = true
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCellWithReuseIdentifier("Test", forIndexPath: indexPath) as UICollectionViewCell
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSizeMake(300, 300)
}
}
So why does showing the navbar stop working when I add cells to my collection view?
I had the same problem but with a web view.
The problem was that the top constraint of the web view was "Top Layout Guide.Top" , after changing the top constraint to "Superview.Top" the problem was solved.
To expand on Oleg's answer...
If you are using Interface Builder to set a constraint to a view controller's primary view, Xcode defaults to showing options to set the vertical constraint against the top layout guide. However, if you press 'Option', you will see an alternate set of constraints. The constraint for 'Top Space to Container' is what you're looking for.
I had same issue. When I added the code for hiding status bar along with navigation bar, it worked.
- (BOOL)prefersStatusBarHidden {
return self.navigationController.isNavigationBarHidden;
}
I tried setting hidesBarsOnSwipe property to true in my ViewController class in ViewDidLoad function as given below, but it didn't work in handling hiding the navigation bar on swipe-up and unhiding the navigation bar on swipe-down.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.hidesBarsOnSwipe = true
}
}
Setting hidesBarsOnSwipe to true will have effect only if we are using the UITableViewController or UICollectionViewController as main screens, hidesBarsOnSwipe will not work if we have added a UITableView to the UIViewController for displaying the list of data.
Solution
class TestTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.hidesBarsOnSwipe = true
}
}
Hope this answer might help...!
I filed a bug report with Apple and ended up using AMScrollingNavbar instead which works really well and is easy to setup.
As per previous comments - this seems like a bug as of ios 10.3
as you are using a uicollectionview - I draw your attention to some code I re-wrote from APDynamicHeaderTableViewController
https://github.com/aaronpang/APDynamicHeaderTableViewController/issues/4
It's using snapkit https://github.com/SnapKit/SnapKit
(Apologies to all the IB + NSLayout Constraint lovers.)
class APDynamicHeaderTableViewController : UIViewController {
var largeWideSize = CGSize(width: UIScreen.main.bounds.width , height: 285 )
let headerView = APDynamicHeaderView () // Change your header view here
let cellLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
var feedCV:UICollectionView!
fileprivate var headerViewHeight:CGFloat = 80 // this will be updated by scrolling
fileprivate var headerBeganCollapsed = false
fileprivate var collapsedHeaderViewHeight : CGFloat = UIApplication.shared.statusBarFrame.height
fileprivate var expandedHeaderViewHeight : CGFloat = 100
fileprivate var headerExpandDelay : CGFloat = 100
fileprivate var tableViewScrollOffsetBeginDraggingY : CGFloat = 0.0
init(collapsedHeaderViewHeight : CGFloat, expandedHeaderViewHeight : CGFloat, headerExpandDelay :CGFloat) {
self.collapsedHeaderViewHeight = collapsedHeaderViewHeight
self.expandedHeaderViewHeight = expandedHeaderViewHeight
self.headerExpandDelay = headerExpandDelay
super.init(nibName: nil, bundle: nil)
}
init () {
super.init(nibName: nil, bundle: nil)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
super.loadView()
self.view.backgroundColor = .green
// Cell Layout Sizes
cellLayout.scrollDirection = .vertical
cellLayout.sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
cellLayout.itemSize = CGSize(width: UIScreen.main.bounds.width, height: 185 + 80)
// Header view
self.view.addSubview(headerView)
headerView.snp.remakeConstraints { (make) -> Void in
make.top.left.equalToSuperview()
make.width.equalToSuperview()
make.height.equalTo(headerViewHeight)
}
// CollectionView
feedCV = UICollectionView(frame: .zero, collectionViewLayout: cellLayout)
self.view.addSubview(feedCV)
self.feedCV.snp.remakeConstraints { (make) -> Void in
make.top.equalTo(headerView.snp.bottom) // this is pegged to the header view which is going to grow in height
make.left.equalToSuperview()
make.width.equalToSuperview()
make.bottom.equalToSuperview()
}
feedCV.backgroundColor = .red
feedCV.showsVerticalScrollIndicator = true
feedCV.isScrollEnabled = true
feedCV.bounces = true
feedCV.delegate = self
feedCV.dataSource = self
// YOUR COLLECTIONVIEW CELL HERE!!!!!
feedCV.register(VideoCollectionViewCell.self, forCellWithReuseIdentifier: VideoCollectionViewCell.ID)
}
// Animate the header view to collapsed or expanded if it is dragged only partially
func animateHeaderViewHeight () -> Void {
Logger.verbose("animateHeaderViewHeight")
var headerViewHeightDestinationConstant : CGFloat = 0.0
if (headerViewHeight < ((expandedHeaderViewHeight - collapsedHeaderViewHeight) / 2.0 + collapsedHeaderViewHeight)) {
headerViewHeightDestinationConstant = collapsedHeaderViewHeight
} else {
headerViewHeightDestinationConstant = expandedHeaderViewHeight
}
if (headerViewHeight != expandedHeaderViewHeight && headerViewHeight != collapsedHeaderViewHeight) {
let animationDuration = 0.25
UIView.animate(withDuration: animationDuration, animations: { () -> Void in
self.headerViewHeight = headerViewHeightDestinationConstant
let progress = (self.headerViewHeight - self.collapsedHeaderViewHeight) / (self.expandedHeaderViewHeight - self.collapsedHeaderViewHeight)
self.headerView.expandToProgress(progress)
self.view.layoutIfNeeded()
})
}
}
}
extension APDynamicHeaderTableViewController : UICollectionViewDelegate {
}
extension APDynamicHeaderTableViewController : UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
// Clamp the beginning point to 0 and the max content offset to prevent unintentional resizing when dragging during rubber banding
tableViewScrollOffsetBeginDraggingY = min(max(scrollView.contentOffset.y, 0), scrollView.contentSize.height - scrollView.frame.size.height)
// Keep track of whether or not the header was collapsed to determine if we can add the delay of expansion
headerBeganCollapsed = (headerViewHeight == collapsedHeaderViewHeight)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Do nothing if the table view is not scrollable
if feedCV.contentSize.height < feedCV.bounds.height {
return
}
var contentOffsetY = feedCV.contentOffset.y - tableViewScrollOffsetBeginDraggingY
// Add a delay to expanding the header only if the user began scrolling below the allotted amount of space to actually expand the header with no delay (e.g. If it takes 30 pixels to scroll up the scrollview to expand the header then don't add the delay of the user started scrolling at 10 pixels)
if tableViewScrollOffsetBeginDraggingY > ((expandedHeaderViewHeight - collapsedHeaderViewHeight) + headerExpandDelay) && contentOffsetY < 0 && headerBeganCollapsed {
contentOffsetY = contentOffsetY + headerExpandDelay
}
// Calculate how much the header height will change so we can readjust the table view's content offset so it doesn't scroll while we change the height of the header
let changeInHeaderViewHeight = headerViewHeight - min(max(headerViewHeight - contentOffsetY, collapsedHeaderViewHeight), expandedHeaderViewHeight)
headerViewHeight = min(max(headerViewHeight - contentOffsetY, collapsedHeaderViewHeight), expandedHeaderViewHeight)
let progress = (headerViewHeight - collapsedHeaderViewHeight) / (expandedHeaderViewHeight - collapsedHeaderViewHeight)
// Logger.verbose("headerViewHeight:",headerViewHeight)
headerView.expandToProgress(progress)
headerView.snp.updateConstraints { (make) -> Void in
make.height.equalTo(headerViewHeight)
}
// When the header view height is changing, freeze the content in the table view
if headerViewHeight != collapsedHeaderViewHeight && headerViewHeight != expandedHeaderViewHeight {
feedCV.contentOffset = CGPoint(x: 0, y: feedCV.contentOffset.y - changeInHeaderViewHeight)
}
}
// Animate the header view when the user ends dragging or flicks the scroll view
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
animateHeaderViewHeight()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
animateHeaderViewHeight()
}
}
extension APDynamicHeaderTableViewController : UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: VideoCollectionViewCell.ID, for: indexPath) as! VideoCollectionViewCell
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return largeWideSize
}
}
To make hidesBarsOnSwipe working properly, your view controller's view must contain only UITableView instance and nothing else.

Resize cells when bounds UICollectionView change

I'm using a horizontally, paging UICollectionView to display a variable number of collection view cells. The size of each collection view cell needs to be equal to that of the collection view and whenever the size of the collection view changes, the size of the collection view cells need to update accordingly. The latter is causing issues. The size of the collection view cells is not updated when the size of the collection view changes.
Invalidating the layout doesn't seem to do the trick. Subclassing UICollectionViewFlowLayout and overriding shouldInvalidateLayoutForBoundsChange: doesn't work either.
For your information, I'm using an instance of UICollectionViewFlowLayout as the collection view's layout object.
I think solution below is much cleaner. You only need to override one of UICollectionViewLayout's method like:
- (void)invalidateLayoutWithContext:(UICollectionViewFlowLayoutInvalidationContext *)context
{
context.invalidateFlowLayoutAttributes = YES;
context.invalidateFlowLayoutDelegateMetrics = YES;
[super invalidateLayoutWithContext:context];
}
and
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
if(!CGSizeEqualToSize(self.collectionView.bounds.size, newBounds.size))
{
return YES;
}
return NO;
}
as well.
I have similar behavior in my app: UICollectionView with cells that should have the same width as collection view at all time. Just returning true from shouldInvalidateLayoutForBoundsChange: didn't work for me either, but I managed to make it work in this way:
class AdaptableFlowLayout: UICollectionViewFlowLayout {
var previousWidth: CGFloat?
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
let newWidth = newBounds.width
let shouldIvalidate = newWidth != self.previousWidth
if shouldIvalidate {
collectionView?.collectionViewLayout.invalidateLayout()
}
self.previousWidth = newWidth
return false
}
}
In documentation it is stated that when shouldInvalidateLayoutForBoundsChange returns true then invalidateLayoutWithContext: will be called. I don't know why invalidateLayout works and invalidateLayoutWithContext: doesn't.
Swift 4 Xcode 9 implementation for height changes:
final class AdaptableHeightFlowLayout: UICollectionViewFlowLayout {
var previousHeight: CGFloat?
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
let newHeight = newBounds.height
let shouldIvalidate = newHeight != self.previousHeight
if shouldIvalidate {
collectionView?.collectionViewLayout.invalidateLayout()
}
self.previousHeight = newHeight
return false
}
}

Customize right click highlight on view-based NSTableView

I have a view-based NSTableView with a custom NSTableCellView and a custom NSTableRowView. I customized both of those classes because I want to change the appearance of each row. By implementing the [NSTableRowView draw...] methods I can change the background, the selection, the separator and the drag destination highlight.
My question is: how can I change the highlight that appears when the row is right clicked and a menu appears?
For example, this is the norm:
And I want to change the square highlight to a round one, like this:
I'd imagine this would be done in NSTableRowView by calling a method like drawMenuHighlightInRect: or something, but I can't find it. Also, how can the NSTableRowView class be doing this if I customized, in my subclass, all of the drawing methods, and I don't call the superclass? Is this drawn by the table itself?
EDIT:
After some more experimenting I found out that the round highlight can be achieved by setting the tableview as a source list. Nonetheless, I want to know how to customize it if possible.
I know I'm a bit late to offer any help to the OP, but hopefully this can spare some other folks a little bit of time. I subclassed NSTableRowView to achieve the right-click contextual menu highlight (why Apple doesn't have a public drawing method to override this is beyond me). Here it is in all its glory:
BSDSourceListRowView.h
#import <Cocoa/Cocoa.h>
#interface BSDSourceListRowView : NSTableRowView
// This needs to be set when a context menu is shown.
#property (nonatomic, assign, getter = isShowingMenu) BOOL showingMenu;
#end
BSDSourceListRowView.m
#import "BSDSourceListRowView.h"
#implementation BSDSourceListRowView
- (void)drawBackgroundInRect:(NSRect)dirtyRect
{
[super drawBackgroundInRect:dirtyRect];
// Context menu highlight:
if ( self.isShowingMenu ) {
[self drawContextMenuHighlight];
}
}
- (void)drawContextMenuHighlight
{
BOOL selected = self.isSelected;
CGFloat insetY = ( selected ) ? 2.f : 1.f;
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.bounds, 2.f, insetY) xRadius:6.f yRadius:6.f];
NSColor *fillColor, *strokeColor;
if ( selected ) {
fillColor = [NSColor clearColor];
strokeColor = [NSColor whiteColor];
} else {
fillColor = [NSColor colorWithCalibratedRed:95.f/255.f green:159.f/255.f blue:1.f alpha:0.12f];
strokeColor = [NSColor alternateSelectedControlColor];
}
[fillColor setFill];
[strokeColor setStroke];
[path setLineWidth:2.f];
[path fill];
[path stroke];
}
- (void)drawSelectionInRect:(NSRect)dirtyRect
{
[super drawSelectionInRect:dirtyRect];
if ( self.isShowingMenu ) {
[self drawContextMenuHighlight];
}
}
- (void)setShowingMenu:(BOOL)showingMenu
{
if ( showingMenu == _showingMenu )
return;
_showingMenu = showingMenu;
[self setNeedsDisplay:YES];
}
#end
Feel free to use any of it, change any of it, or do whatever you want with any of it. Have fun!
Updated for Swift 3.x:
SourceListRowView.swift
import Cocoa
open class SourceListRowView : NSTableRowView {
open var isShowingMenu: Bool = false {
didSet {
if isShowingMenu != oldValue {
needsDisplay = true
}
}
}
override open func drawBackground(in dirtyRect: NSRect) {
super.drawBackground(in: dirtyRect)
if isShowingMenu {
drawContextMenuHighlight()
}
}
override open func drawSelection(in dirtyRect: NSRect) {
super.drawSelection(in: dirtyRect)
if isShowingMenu {
drawContextMenuHighlight()
}
}
private func drawContextMenuHighlight() {
let insetY: CGFloat = isSelected ? 2 : 1
let path = NSBezierPath(roundedRect: bounds.insetBy(dx: 2, dy: insetY), xRadius: 6, yRadius: 6)
let fillColor, strokeColor: NSColor
if isSelected {
fillColor = .clear
strokeColor = .white
} else {
fillColor = NSColor(calibratedRed: 95/255, green: 159/255, blue: 1, alpha: 0.12)
strokeColor = .alternateSelectedControlColor
}
fillColor.setFill()
strokeColor.setStroke()
path.lineWidth = 2
path.fill()
path.stroke()
}
}
Note: I haven't actually run this, but I'm pretty sure this should do the trick in Swift.
Stop Default Drawing
Several answers describe how to draw a custom contextual-click highlight. However, AppKit will continue to draw the default one. There is an easy trick to stop that and I didn't want it to get lost in a comment: subclass NSTableView and override -menuForEvent:
// NSTableView subclass
override func menu(for event: NSEvent) -> NSMenu?
{
// DO NOT call super's implementation.
return self.menu
}
Here, I assume that you've assigned a menu to the tableView in IB or have set the tableView's menu property programatically. NSTableView's default implementation of -menuForEvent: is what draws the contextual menu highlight.
Solve Bad Apple Engineering
Now that we're not calling super's implementation of menuForEvent:, the clickedRow property of our tableView will always be -1 when we right-click, which means our menuItems won't target the correct row of our tableView.
But fear not, we can do Apple Engineering's job for them. On our custom NSTableView subclass, we override the clickedRow property:
class MyTableView: NSTableView
{
private var _clickedRow: Int = -1
override var clickedRow: Int {
get { return _clickedRow }
set { _clickedRow = newValue }
}
}
Now we update the -menuForEvent: method:
// NSTableView subclass
override func menu(for event: NSEvent) -> NSMenu?
{
let location: CGPoint = convert(event.locationInWindow, from: nil)
clickedRow = row(at: location)
return self.menu
}
Great. We solved that problem. Onwards to the next thing:
Tell Your RowView To Do Custom Drawing
As others have suggested, add a custom Bool property to your NSTableRowView subclass. Then, in your drawing code, inspect that value to decide whether to draw your custom contextual highlight. However, the correct place to set that value is in the same NSTableView method:
// NSTableView subclass
override func menu(for event: NSEvent) -> NSMenu?
{
let location: CGPoint = convert(event.locationInWindow, from: nil)
clickedRow = row(at: location)
if clickedRow > 0,
let rowView: MyCustomRowView = rowView(atRow: tableRow, makeIfNecessary: false) as? MyCustomRowView
{
rowView.isContextualMenuTarget = true
}
return self.menu
}
Above, I've created MyCustomRowView (a subclass of NSTableRowView) and have added a custom property: isContextualMenuTarget. That custom property looks like this:
// NSTableRowView subclass
var isContextualMenuTarget: Bool = false {
didSet {
needsDisplay = true
}
}
In my drawing method, I inspect the value of that property and, if it's true, draw my custom highlight.
Clean Up When The Menu Closes
You have a controller that implements the datasource and delegate methods for your tableView. That controller is also likely the delegate for the tableView's menu. (You can assign that in IB or programatically.)
Whatever object is your menu's delegate, implement the menuDidClose: method. Here, I'm working in Objective-C because my controller is still ObjC:
// NSMenuDelegate object
- (void) menuDidClose:(NSMenu *)menu
{
// We use a custom flag on our rowViews to draw our own contextual menu highlight, so we need to reset that.
[_outlineView enumerateAvailableRowViewsUsingBlock:^(__kindof MyCustomRowView * _Nonnull rowView, NSInteger row) {
rowView.isContextualMenuTarget = NO;
}];
}
Performance Note: My tableView will never have more than about 50 entries. If you have a table with THOUSANDS of visible rows, you would be better served to save the rowView that you set isContextualMenuTarget=true on, then access that rowView directly in -menuDidClose: so you don't have to enumerate all rowViews.
Single-Column: This example assumes a single column tableView that has the same NSMenu for each row. You could adapt the same technique for multi-column and/or varying NSMenus per row.
And that's how you beat AppKit in the face until it does what you want.
This is already a bit old, but I've wasted on it quite a bit of time, so posting my solution in case it could help anyone:
In my case, I wanted to remove the lines completely
Lines are not "Focus" rings, they are some stuff Apple is doing in undocument API
The ONLY way I found to remove them (Without using Undocumented API) is by opening NSMenu programmatically, without Interface Builder.
For that, I had to cache "right-click" event on TableViewRow, which has some issue since not always called, so I've dealt with that issue too.
A. Subclass NSTableView:
Overriding right click event, calculating the location of click to get a correct row, and transferring it to my custom NSTableRowView!
class TableView: NSTableView {
override func rightMouseDown(with event: NSEvent) {
let location = event.locationInWindow
let toMyOrigin = self.superview?.convert(location, from: nil)
let rowIndex = self.row(at: toMyOrigin!)
if (rowIndex < 0 || self.numberOfRows < rowIndex) {
return
}
if let isRowExists = self.rowView(atRow: rowIndex, makeIfNecessary: false) {
if let isMyTypeRow = isRowExists as? MyNSTableRowView {
isMyTypeRow.costumRightMouseDown(with: event)
}
}
}
}
B. Subclass MyNSTableRowView
Presenting NSMenu programmatically
class MyNSTableRowView: NSTableRowView {
//My custom selection colors, don't have to implement this if you are ok with the default system highlighted background color
override func drawSelection(in dirtyRect: NSRect) {
if self.selectionHighlightStyle != .none {
let selectionRect = NSInsetRect(self.bounds, 0, 0)
Colors.tabSelectedBackground.setStroke()
Colors.tabSelectedBackground.setFill()
let selectionPath = NSBezierPath.init(roundedRect: selectionRect, xRadius: 0, yRadius: 0)
selectionPath.fill()
selectionPath.stroke()
}
}
func costumRightMouseDown(with event: NSEvent) {
let menu = NSMenu.init(title: "Actions:")
menu.addItem(NSMenuItem.init(title: "Some", action: #selector(foo), keyEquivalent: "a"))
NSMenu.popUpContextMenu(menu, with: event, for: self)
}
#objc func foo() {
}
}
I agree with MCMatan that this is not something you can tweak by changing any draw calls. The box will remain.
I took his approach of bypassing the default menu launch, but left the context menu setup as default in my NSTableView. I think this is a simpler way.
I derive from NSTableView and add the following:
public private(set) var rightClickedRow: Int = -1
override func rightMouseDown(with event: NSEvent)
{
guard let menu = self.menu else { return }
let windowClickLocation = event.locationInWindow
let outlineClickLocation = convert(windowClickLocation, from: nil)
rightClickedRow = row(at: outlineClickLocation)
menu.popUp(positioning: nil, at: outlineClickLocation, in: self)
}
override func rightMouseUp(with event: NSEvent) {
rightClickedRow = -1
}
My rightClickedRow is analogous to clickedRow for the table view. I have an NSViewController that looks after my table, and it is set as the table's menu delegate. I can use rightClickedRow in the delegate calls, such as menuNeedsUpdate().
I'd take a look at the NSTableRowView documentation. It's the class that is responsible for drawing selection and drag feedback in a view-based NSTableView.

Hide buttons from titlebar in Cocoa

The Apples Human Interface Guidelines say:
macOS Human Interface Guidelines: Panels
How do I make the very first titlebar in that image (with only a close button). Disabling both Resize and Minimize in IB only make the resize/minimize buttons get disabled. But I want them to disappear. How can I do that?
I believe this should work:
[[window standardWindowButton:NSWindowCloseButton] setHidden:YES];
[[window standardWindowButton:NSWindowMiniaturizeButton] setHidden:YES];
[[window standardWindowButton:NSWindowZoomButton] setHidden:YES];
Swift code for accepted answer
window!.standardWindowButton(.miniaturizeButton)!.isHidden = true
window!.standardWindowButton(.zoomButton)!.isHidden = true
window!.standardWindowButton(.closeButton)!.isHidden = true
I also needed this but visible for mouse overs - Swift:
var trackingTag: NSTrackingRectTag?
override func mouseEntered(with theEvent: NSEvent) {
if trackingTag == theEvent.trackingNumber {
window!.standardWindowButton(.closeButton)!.alphaValue = 1.00
}
}
override func mouseExited(with theEvent: NSEvent) {
if trackingTag == theEvent.trackingNumber {
window!.standardWindowButton(.closeButton)!.alphaValue = 0.01
}
}
func updateTrackingAreas(_ establish : Bool) {
if let tag = trackingTag {
window!.standardWindowButton(.closeButton)!.removeTrackingRect(tag)
}
if establish, let closeButton = window!.standardWindowButton(.closeButton) {
trackingTag = closeButton.addTrackingRect(closeButton.bounds, owner: self, userData: nil, assumeInside: false)
}
}
override func windowDidLoad() {
window!.ignoresMouseEvents = false
updateTrackingAreas(true)
window!.standardWindowButton(.closeButton)!.alphaValue = 0.01
}
func windowShouldClose(_ sender: Any) -> Bool {
window!.ignoresMouseEvents = true
updateTrackingAreas(false)
return true
}
Visibility is needed but just slightly - 0.01 opacity, to have the tracking area effective.
another way is...
for (id subview in [self window].contentView.superview.subviews) {
if ([subview isKindOfClass:NSClassFromString(#"NSTitlebarContainerView")]) {
NSView *titlebarView = [subview subviews][0];
for (id button in titlebarView.subviews) {
if ([button isKindOfClass:[NSButton class]]) {
[button setHidden:YES];
}
}
}
}