I have three NSViews inside an NSTableCellView. Depending on the data for the row, I want to show a selected role with a border on the NSView like this:
The blue border NSView is a subclass that looks like this:
class RolePill: NSView{
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
layer?.cornerRadius = 9
layer?.borderWidth = 2
layer?.borderColor = NSColor.clear.cgColor
}
}
The role gets set initially when my table loads like this:
extension UsersVC: NSTableViewDelegate, NSTableViewDataSource{
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "UserCell"), owner: nil) as! UserCell
let user = users[row]
//Role
cell.setRole(role: user.role)
}
}
And my table cell, where the role gets set on load and on a button click, is set up like this:
class UserCell: NSTableCellView{
#IBOutlet weak var wrapOwner: RolePill!
#IBOutlet weak var wrapAdmin: RolePill!
#IBOutlet weak var wrapUser: RolePill!
#IBAction func clickOwner(_ sender: NSButton) {
setRole(role: "owner")
}
#IBAction func clickAdmin(_ sender: NSButton) {
setRole(role: "admin")
}
#IBAction func clickUser(_ sender: NSButton) {
setRole(role: "user")
}
func setRole(role: String){
let selectedColor = getAccentColor()
let offColor = Color(named: "BravoDark")!
let offTextColor = Color(named: "BFC0C2")
switch role{
case "owner":
wrapOwner.layer?.borderColor = selectedColor.cgColor
wrapAdmin.layer?.borderColor = offColor.cgColor
wrapUser.layer?.borderColor = offColor.cgColor
labelOwner.textColor = Color.white
labelAdmin.textColor = offTextColor
labelUser.textColor = offTextColor
case "admin":
wrapOwner.layer?.borderColor = offColor.cgColor
wrapAdmin.layer?.borderColor = selectedColor.cgColor
wrapUser.layer?.borderColor = offColor.cgColor
labelOwner.textColor = offTextColor
labelAdmin.textColor = Color.white
labelUser.textColor = offTextColor
default:
wrapOwner.layer?.borderColor = offColor.cgColor
wrapAdmin.layer?.borderColor = offColor.cgColor
wrapUser.layer?.borderColor = selectedColor.cgColor
labelOwner.textColor = offTextColor
labelAdmin.textColor = offTextColor
labelUser.textColor = Color.white
}
}
}
When the table loads initially, and anytime it refreshes (tableView.reloadData()) I lose my border and it looks like this:
As you can see, the textColor is set correctly to white. But for some reason, the border isn't set until I actually click on one of the IBAction buttons and manually trigger a change.
I suspect this is some kind of layer drawing bug where the RolePill class is redrawing every time my NSTableView reloads, but I don't know how to get it to accept the initial role state sent in tableView viewForRow.
Any idea what I'm doing wrong here? Thanks!
I was able to get this to work by setting all the NSView properties inside setRole() like this:
view.wantsLayer = true
view.layer?.cornerRadius = 11
view.layer?.borderWidth = 2
view.layer?.borderColor = getAccentColor().cgColor
label.textColor = Color.white
It seemed that the draw() method on my RolePill subclass was getting called frequently, so I couldn't set a border color in there since it doesn't know what the user's data state is.
How do you add padding to the left of the text in a text field cell using swift? Previous answers are only for UITextField or in Objective C. To be clear, this is for an NSTextField.
Here is an example of someone who has made a custom NSTextFieldCell in Objective C.
Ported to Swift that looks like this:
import Cocoa
class PaddedTextFieldCell: NSTextFieldCell {
#IBInspectable var leftPadding: CGFloat = 10.0
override func drawingRect(forBounds rect: NSRect) -> NSRect {
let rectInset = NSMakeRect(rect.origin.x + leftPadding, rect.origin.y, rect.size.width - leftPadding, rect.size.height)
return super.drawingRect(forBounds: rectInset)
}
}
I've added the padding as a #IBInspectable property. That way you can set it as you like in Interface Builder.
Use With Interface Builder
To use your new PaddedTextFieldCell you drag a regular Text Field to your xib file
and then change the class of the inner TextFieldCell to be PaddedTextFieldCell
Success!
Use From Code
To use the PaddedTextFieldCell from code, you could do something like this (thank you to #Sentry.co for assistance):
class ViewController: NSViewController {
#IBOutlet weak var textField: NSTextField! {
didSet {
let paddedTextField = PaddedTextFieldCell()
paddedTextField.leftPadding = 40
textField.cell = paddedTextField
textField.isBordered = true
textField.isEditable = true
}
}
....
}
Hope that helps you.
i´m developing an app that has a lot of view controllers and did set the back ground image with code, but i have to copy and paste that block of code in every view controller, how can i type it once and call it to reuse the code in every view controller..Thanks A lot.
var imageView: UIImageView!
let image = UIImage(named: "backGroundImage.png")!
override func loadView() {
super.loadView()
self.imageView = UIImageView(frame: CGRectZero)
self.imageView.contentMode = .ScaleAspectFill
self.imageView.image = image
self.view.addSubview(imageView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.imageView.frame = self.view.bounds
}
What you can do is make a parent UIViewController class and have all of your view controllers inherit from that class. Somethig like this:
class MyParentViewController: UIViewController {
var imageView: UIImageView!
let image = UIImage()
override func viewDidLoad() {
super.viewDidLoad()
self.imageView = UIImageView(frame: CGRectZero)
self.imageView.contentMode = .ScaleAspectFill
self.imageView.image = image
self.view.addSubview(imageView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.imageView.frame = self.view.bounds
}
}
And then your child view controllers that enherit from this class would look something like this:
class MyViewController: MyParentViewController {
overide func viewDidLoad() {
image = UIImage(named:"My image name")
super.viewDidLoad()
}
}
Now, here is really the way you should do it
However, having multiple view controllers that are all the same probably isnt a good idea. I would just keep only the parent view controller (rename it obviously) and just change the value of "image" in the segue that leads to the view to whatever image you want. This would eliminate the need to have a ton of view controllers, and will make it easier to add more images in the future if you want.
Here's how I would do it:
protocol ImageViewBackground {
var backgroundImageView: UIImageView? {get set}
mutating func setBackgroundImage(image: UIImage)
}
extension ImageViewBackground where Self: UIViewController {
mutating func setBackgroundImage(image: UIImage) {
let backgroundImageView = self.backgroundImageView ?? setupImageView()
backgroundImageView.image = image
}
mutating func setupImageView() -> UIImageView {
let imageView = UIImageView(frame: self.view.frame)
imageView.contentMode = .ScaleAspectFill
view.addSubview(imageView)
backgroundImageView = backgroundImageView
return imageView
}
}
Then just make any UIViewController adhere to the protocol like so:
class ViewController: UIViewController, ImageViewBackground {
var backgroundImageView: UIImageView?
}
Then you can call setBackgroundImage from anywhere in your VC without having to make a generic subclass just for the background image.
here is my BorderView and The task I'm trying to do is to hookup the custom object (Border) to the main storyboard.
import UIKit
protocol BorderViewDataSource: class{
func colorForBorderView(sender:BorderView) -> String? }
class BorderView: UIView {
#IBOutlet weak var topLeftImage: UIImageView!
#IBOutlet weak var topRightImage: UIImageView!
#IBOutlet weak var bottomLeftImage: UIImageView!
#IBOutlet weak var bottomRightImage: UIImageView!
var view: UIView!
weak var dataSource:BorderViewDataSource?
override init(frame: CGRect) {
super.init(frame: frame)
xtraSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xtraSetup()
}
func xtraSetup() {
view = loadViewFromNib()
// use bounds not frame or it'll be offset
view.frame = bounds
// Make the view stretch with containing view
view.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
// Adding custom subview on top of our view (over any custom drawing > see note below)
addSubview(view)
let borderColor = dataSource?.colorForBorderView(self) ?? "red"
applyColor(borderColor)
}
// this is an actual load from nib module
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: "BorderView", bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
func applyColor(borderColor:String){
var imageName = "border-topleft-\(borderColor)"
topLeftImage.image = UIImage(named: imageName)
imageName = "border-topright-\(borderColor)"
print("Border color is \(borderColor), and Last image name is \(imageName)")
}
}
So when i open the app it will show the image of different color depending on what the value of the color is set in the ViewContorller.swift and here is my viewcontroller. swift code
class ViewController: UIViewController, BorderViewDataSource {
var borderType = 2
#IBOutlet weak var borderView: BorderView!{
didSet{
borderView.dataSource = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func colorForBorderView(sender: BorderView) -> String? {
var borderColor = "blue"
switch borderType {
case 1: borderColor = "blue"
case 2: borderColor = "purple"
case 3: borderColor = "red"
default: break
}
print("Border Type is \(borderType), and border color is \(borderColor)")
return borderColor
}
}
but colorForBorderView method is not calling when i running the app
It seems when the view is initialized, the dataSource property of that view is nil. And you only call that method during initialization of your view. You can apply the color after viewDidLoad() in your view controller
From the documentation:
When you assign a default value to a stored property, or set its
initial value within an initializer, the value of that property is set
directly, without calling any property observers.
That's the reason why the datasource is not set. Put the line borderView.dataSource = self into viewDidLoad()
I read the document Synchronizing Scroll Views, and did exactly as the document, but there is an isssue.
I want to synchronize a NSTableView and a NSTextView. first let NSTableView monitor NSTextView, and everything is ok when I scroll the TextView, but when I try to scroll TableView, I found that the TableView will jump to another place(maybe backward several rows) at first, then continue to scroll from that place.
This issue still exists even after I let TextView monitor TableView.
anyone know what's the problem? can't I synchronize a TableView and a TextView?
Edited:
OK, now I found that the TableView will go back to the place since last scrolling. for example, TableView's top row is 10th row, then I scroll TextView, now TableView's top row is 20th row, and if I scroll TableView again, the TableView will go back to 10th row first, then start to scroll.
I just ran into this exact problem while troubleshooting a very similar situation (on Lion). I noticed that this only occurs when the scrollers are hidden -- but I verified that they still exist in the nib, and are still instantiated correctly.
I even made sure to call -[NSScrollView reflectScrolledClipView:], but it didn't make a difference. It really seems like this is a bug in NSScrollView.
Anyway, I was able to work around the issue by creating a custom scroller class. All I had to do was override the following class methods:
+ (BOOL)isCompatibleWithOverlayScrollers
{
// Let this scroller sit on top of the content view, rather than next to it.
return YES;
}
- (void)setHidden:(BOOL)flag
{
// Ugly hack: make sure we are always hidden.
[super setHidden:YES];
}
Then, I allowed the scrollers to be "visible" in Interface Builder. Since they hide themselves, however, they do no appear onscreen and they can't be clicked by the user. It's surprising that the IB setting and the hidden property are not equivalent, but it seems clear from the behavior that they are not.
This isn't the best solution, but it's the simplest workaround I've come up with (so far).
I had a quite similar problem.
I have 3 scrollviews to synchronize.
One that is a header that only scrolls horizontally.
One that is a side bar that only scrolls vertically.
One that is a content area below the header and to the right of the side bar.
The header and side bar should move with the content area.
The content area should move with the header or the side bar if either is scrolled.
Horizontal scrolling was never a problem.
Vertical scrolling was always causing the two views to scroll opposite directions.
The odd resolution I came to was to create a clipView subclass (which I already did, as you pretty much always need to if you want anything nice that doesn't come out of the box.)
In the clipView subclass, I add a property BOOL isInverted and in the override of isFlipped I return self.isInverted.
The weird thing is that these BOOL values for flippedness are set and match in all 3 views from the beginning.
It seems that scrolling machinery is indeed buggy.
My workaround that I stumbled upon was to sandwich the scroll synching code between calls to set both the side bar and content view unflipped and then update any vertical scrolling, then set both flipped again.
Must be some aging code in the scrolling machinery trying to support inverted scrolling...
These are the methods called by the NSNotificationCenter addObserver methods to observe the NSViewBoundsDidChangeNotification for the clipViews.
- (void)synchWithVerticalControlClipView:(NSNotification *)aNotification
{
NSPoint mouseInWindow = self.view.window.currentEvent.locationInWindow;
NSPoint converted = [self.verticalControl.enclosingScrollView convertPoint:mouseInWindow fromView:nil];
if (!NSPointInRect(converted, self.verticalControl.enclosingScrollView.bounds)) {
return;
}
[self.contentGridClipView setIsInverted:NO];
[self.verticalControlClipView setIsInverted:NO];
// ONLY update the contentGrid view.
NSLog(#"%#", NSStringFromSelector(_cmd));
NSPoint changedBoundsOrigin = self.verticalControlClipView.documentVisibleRect.origin;
NSPoint currentOffset = self.contentGridClipView.bounds.origin;
NSPoint newOffset = currentOffset;
newOffset.y = changedBoundsOrigin.y;
NSLog(#"\n changedBoundsOrigin=%#\n currentOffset=%#\n newOffset=%#", NSStringFromPoint(changedBoundsOrigin), NSStringFromPoint(currentOffset), NSStringFromPoint(newOffset));
[self.contentGridClipView scrollToPoint:newOffset];
[self.contentGridClipView.enclosingScrollView reflectScrolledClipView:self.contentGridClipView];
[self.contentGridClipView setIsInverted:YES];
[self.verticalControlClipView setIsInverted:YES];
}
- (void)synchWithContentGridClipView:(NSNotification *)aNotification
{
NSPoint mouseInWindow = self.view.window.currentEvent.locationInWindow;
NSPoint converted = [self.contentGridView.enclosingScrollView convertPoint:mouseInWindow fromView:nil];
if (!NSPointInRect(converted, self.contentGridView.enclosingScrollView.bounds)) {
return;
}
[self.contentGridClipView setIsInverted:NO];
[self.verticalControlClipView setIsInverted:NO];
// Update BOTH the control views.
NSLog(#"%#", NSStringFromSelector(_cmd));
NSPoint changedBoundsOrigin = self.contentGridClipView.documentVisibleRect.origin;
NSPoint currentHOffset = self.horizontalControlClipView.documentVisibleRect.origin;
NSPoint currentVOffset = self.verticalControlClipView.documentVisibleRect.origin;
NSPoint newHOffset, newVOffset;
newHOffset = currentHOffset;
newVOffset = currentVOffset;
newHOffset.x = changedBoundsOrigin.x;
newVOffset.y = changedBoundsOrigin.y;
[self.horizontalControlClipView scrollToPoint:newHOffset];
[self.verticalControlClipView scrollToPoint:newVOffset];
[self.horizontalControlClipView.enclosingScrollView reflectScrolledClipView:self.horizontalControlClipView];
[self.verticalControlClipView.enclosingScrollView reflectScrolledClipView:self.verticalControlClipView];
[self.contentGridClipView setIsInverted:YES];
[self.verticalControlClipView setIsInverted:YES];
}
This works 99% of the time, with only occasional jitter.
Horizontal scroll synch has no problems.
Swift 4 version which uses document view in auto-layout environment.
Based on Apple article Synchronizing Scroll Views with the difference that NSView.boundsDidChangeNotification temporary ignored on clip view when synchronising to other scroll view.
To hide vertical scroller reusable type InvisibleScroller is used.
File SynchronedScrollViewController.swift – View controllers with two scroll views.
class SynchronedScrollViewController: ViewController {
private lazy var leftView = TestView().autolayoutView()
private lazy var rightView = TestView().autolayoutView()
private lazy var leftScrollView = ScrollView(horizontallyScrolledDocumentView: leftView).autolayoutView()
private lazy var rightScrollView = ScrollView(horizontallyScrolledDocumentView: rightView).autolayoutView()
override func setupUI() {
view.addSubviews(leftScrollView, rightScrollView)
leftView.backgroundColor = .red
rightView.backgroundColor = .blue
contentView.backgroundColor = .green
leftScrollView.verticalScroller = InvisibleScroller()
leftView.setIntrinsicContentSize(CGSize(intrinsicHeight: 720)) // Some fake height
rightView.setIntrinsicContentSize(CGSize(intrinsicHeight: 720)) // Some fake height
}
override func setupHandlers() {
(leftScrollView.contentView as? ClipView)?.onBoundsDidChange = { [weak self] in
print("\(Date().timeIntervalSinceReferenceDate) : Left scroll view changed")
self?.syncScrollViews(origin: $0)
}
(rightScrollView.contentView as? ClipView)?.onBoundsDidChange = { [weak self] in
print("\(Date().timeIntervalSinceReferenceDate) : Right scroll view changed.")
self?.syncScrollViews(origin: $0)
}
}
override func setupLayout() {
LayoutConstraint.pin(to: .vertically, leftScrollView, rightScrollView).activate()
LayoutConstraint.withFormat("|[*(==40)]-[*]|", leftScrollView, rightScrollView).activate()
}
private func syncScrollViews(origin: NSClipView) {
// See also:
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/NSScrollViewGuide/Articles/SynchroScroll.html
let changedBoundsOrigin = origin.documentVisibleRect.origin
let targetScrollView = leftScrollView.contentView == origin ? rightScrollView : leftScrollView
let curOffset = targetScrollView.contentView.bounds.origin
var newOffset = curOffset
newOffset.y = changedBoundsOrigin.y
if curOffset != changedBoundsOrigin {
(targetScrollView.contentView as? ClipView)?.scroll(newOffset, shouldNotifyBoundsChange: false)
targetScrollView.reflectScrolledClipView(targetScrollView.contentView)
}
}
}
File: TestView.swift – Test view. Draws line every 20 points.
class TestView: View {
override init() {
super.init()
setIsFlipped(true)
}
override func setupLayout() {
needsDisplay = true
}
required init?(coder decoder: NSCoder) {
fatalError()
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
guard let context = NSGraphicsContext.current else {
return
}
context.saveGraphicsState()
let cgContext = context.cgContext
cgContext.setStrokeColor(NSColor.white.cgColor)
for x in stride(from: CGFloat(20), through: bounds.height, by: 20) {
cgContext.addLines(between: [CGPoint(x: 0, y: x), CGPoint(x: bounds.width, y: x)])
NSString(string: "\(Int(x))").draw(at: CGPoint(x: 0, y: x), withAttributes: nil)
}
cgContext.strokePath()
context.restoreGraphicsState()
}
}
File: NSScrollView.swift - Reusable extension.
extension NSScrollView {
public convenience init(documentView view: NSView) {
let frame = CGRect(dimension: 10) // Some dummy non zero value
self.init(frame: frame)
let clipView = ClipView(frame: frame)
clipView.documentView = view
clipView.autoresizingMask = [.height, .width]
contentView = clipView
view.frame = frame
view.translatesAutoresizingMaskIntoConstraints = true
view.autoresizingMask = [.width, .height]
}
public convenience init(horizontallyScrolledDocumentView view: NSView) {
self.init(documentView: view)
contentView.setIsFlipped(true)
view.translatesAutoresizingMaskIntoConstraints = false
LayoutConstraint.pin(in: contentView, to: .horizontally, view).activate()
view.topAnchor.constraint(equalTo: contentView.topAnchor).activate()
hasVerticalScroller = true // Without this scroll might not work properly. Seems Apple bug.
}
}
File: InvisibleScroller.swift - Reusable invisible scroller.
// Disabling scroll view indicators.
// See: https://stackoverflow.com/questions/9364953/hide-scrollers-while-leaving-scrolling-itself-enabled-in-nsscrollview
public class InvisibleScroller: Scroller {
public override class var isCompatibleWithOverlayScrollers: Bool {
return true
}
public override class func scrollerWidth(for controlSize: NSControl.ControlSize, scrollerStyle: NSScroller.Style) -> CGFloat {
return CGFloat.leastNormalMagnitude // Dimension of scroller is equal to `FLT_MIN`
}
public override func setupUI() {
// Below assignments not really needed, but why not.
scrollerStyle = .overlay
alphaValue = 0
}
}
File: ClipView.swift - Customized subclass of NSClipView.
open class ClipView: NSClipView {
public var onBoundsDidChange: ((NSClipView) -> Void)? {
didSet {
setupBoundsChangeObserver()
}
}
private var boundsChangeObserver: NotificationObserver?
private var mIsFlipped: Bool?
open override var isFlipped: Bool {
return mIsFlipped ?? super.isFlipped
}
// MARK: -
public func setIsFlipped(_ value: Bool?) {
mIsFlipped = value
}
open func scroll(_ point: NSPoint, shouldNotifyBoundsChange: Bool) {
if shouldNotifyBoundsChange {
scroll(to: point)
} else {
boundsChangeObserver?.isActive = false
scroll(to: point)
boundsChangeObserver?.isActive = true
}
}
// MARK: - Private
private func setupBoundsChangeObserver() {
postsBoundsChangedNotifications = onBoundsDidChange != nil
boundsChangeObserver = nil
if postsBoundsChangedNotifications {
boundsChangeObserver = NotificationObserver(name: NSView.boundsDidChangeNotification, object: self) { [weak self] _ in
guard let this = self else { return }
self?.onBoundsDidChange?(this)
}
}
}
}
File: NotificationObserver.swift – Reusable Notification observer.
public class NotificationObserver: NSObject {
public typealias Handler = ((Foundation.Notification) -> Void)
private var notificationObserver: NSObjectProtocol!
private let notificationObject: Any?
public var handler: Handler?
public var isActive: Bool = true
public private(set) var notificationName: NSNotification.Name
public init(name: NSNotification.Name, object: Any? = nil, queue: OperationQueue = .main, handler: Handler? = nil) {
notificationName = name
notificationObject = object
self.handler = handler
super.init()
notificationObserver = NotificationCenter.default.addObserver(forName: name, object: object, queue: queue) { [weak self] in
guard let this = self else { return }
if this.isActive {
self?.handler?($0)
}
}
}
deinit {
NotificationCenter.default.removeObserver(notificationObserver, name: notificationName, object: notificationObject)
}
}
Result: