Load multiple instances of a NSView from Nib - cocoa

I was wondering if there was a method to create a view in a xib file and then connect its outlets to a class, so you can create multiple instances of that class and place them in the window.
I have some problems, so can you help me fix my code?
Here's what I did:
Firstly I created 2 files: CustomView.xib and CustomView.swift.
Then I designed the interface adding an NSImageView to the custom view. I set the file's owner to the class name and added an outlet from the NSImageView to the class.
Then I created the following function to load the interface from the nib:
func loadView() -> NSView {
var top = NSArray()
Bundle.main.loadNibNamed("CustomView", owner: self, topLevelObjects: &top)
let view = top[0] as! NSView
return view
}
And set up the class to load the interface:
override init(frame: CGRect) {
super.init(frame: frame)
let view = loadView()
view.frame = bounds
addSubview(view)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
So I went on creating two variables of that class and placing it on the window:
var x = CustomView(frame: CGRect(x: 0, y: 0, width: 600, height: 105))
var y = CustomView(frame: CGRect(x: 0, y: 105, width: 600, height: 105))
And for some reasons this code gives me a strange error. It worked the first time, with one variable, but if I place more than one, it says it couldn't cast an NSWindow type in a NSView.
Could not cast value of type 'NSApplication' (0x7fffb5cf3ef0) to 'NSView' (0x7fffb5d04bb0).
I think that this error is given because sometimes the first top level object is the view, and sometimes it's the window. So I'm getting confused.
Obviously the error is thrown on this line:
let view = top[0] as! NSView
So what's the problem here?
(Please do not answer with cocoa touch code)

Use the filter function to get the NSView instance
func loadView() -> NSView {
var topLevelObjects = NSArray()
Bundle.main.loadNibNamed("CustomView", owner: self, topLevelObjects: &topLevelObjects)
let views = (topLevelObjects as Array).filter { $0 is NSView }
return views[0] as! NSView
}

Related

Additional view in NSCollectionViewItem pauses dragEvent in CollectionViewController

I am trying to implement drop delegates on a NSCollectionViewController and having issues using a custom NSCollectionViewItem with an additional View Layer I've added onto the CollectionView Item. FWIW, The additional view is used draw a dashed border to indicate a drop area.
The drag event works fine on this collectionItem, and all other collectionItems without this view when it is hidden, but as soon as the drag event occurs on top of this view, the drag event pauses.
The drag event resumes as soon as the mouse is dragged outside of the view, but nothing happens if I release the drag while the mouse is over the view.
I would love to know what is happening here and how to prevent the custom view from "stealing" the mouse event from the CollectionViewContoller.
Delegate Method on DropViewController
func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer<NSIndexPath>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionView.DropOperation>) -> NSDragOperation {
print("1")
if proposedDropIndexPath.pointee.item <= self.destinationDirectoryArray.count {
if proposedDropOperation.pointee == NSCollectionView.DropOperation.on {
return .move
}
} else if proposedDropIndexPath.pointee.item == self.destinationDirectoryArray.count {
//There's some stuff here validating the URL removed for brevity. It works okay when the focus is outside the view, but happy to add back in if helpful
if proposedDropOperation.pointee == NSCollectionView.DropOperation.on {
return .move
}
}
return[]
}
Configuring Collection View
func configureCollectionView() {
let flowLayout = NSCollectionViewFlowLayout()
flowLayout.minimumInteritemSpacing = 8.0
flowLayout.minimumLineSpacing = 8.0
destinationCollectionView.delegate = self
destinationCollectionView.dataSource = self
destinationCollectionView.register(NSNib(nibNamed: "DestinationCollectionItem", bundle: nil), forItemWithIdentifier: directoryItemIdentifier)
destinationCollectionView.collectionViewLayout = flowLayout
destinationCollectionView.registerForDraggedTypes([.fileURL])
destinationCollectionView.setDraggingSourceOperationMask(NSDragOperation.move, forLocal: true)
}
Collection View Item Setup
class DestinationCollectionItem: NSCollectionViewItem {
#IBOutlet weak var backgroundLayer: NSView!
override func viewDidLoad() {
super.viewDidLoad()
self.highlightState = .none
view.wantsLayer = true
view.layer?.cornerRadius = 8.0
backgroundLayer.isHidden = true
}
}
Custom Border View - Applied custom class in Xib and linked to File's Owner
class BorderedView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let path : NSBezierPath = NSBezierPath(roundedRect: self.bounds, xRadius: 10.0, yRadius: 10.0)
path.addClip()
let dashHeight: CGFloat = 2
let dashLength: CGFloat = 7
let dashColor: NSColor = .lightGray
// setup the context
let currentContext = NSGraphicsContext.current!.cgContext
currentContext.setLineWidth(dashHeight)
currentContext.setLineDash(phase: 0, lengths: [dashLength])
currentContext.setStrokeColor(dashColor.cgColor)
// draw the dashed path
let cgPath : CGPath = CGPath(roundedRect: NSRectToCGRect(self.bounds), cornerWidth: 10.0, cornerHeight: 10.0, transform: nil)
currentContext.addPath(cgPath)
currentContext.strokePath()
}
}
Well - I solved this one pretty quick.
While I previously tried adding unregisterDraggedTypes() to the backgroundLayer, the issue turned out to also be occurring on the image layer. I applied it to both the Image and backgroundLayer and it works now.
Collection View Item Setup
class DestinationCollectionItem: NSCollectionViewItem {
#IBOutlet weak var backgroundLayer: NSView!
override func viewDidLoad() {
super.viewDidLoad()
self.highlightState = .none
view.wantsLayer = true
view.layer?.cornerRadius = 8.0
backgroundLayer.isHidden = true
backgroundLayer.unregisterDraggedTypes()
self.imageView?.unregisterDraggedTypes()
self.textField?.unregisterDraggedTypes()
}
}

Making Buttons Circular in swift4

I have few buttons which are added from Storyboard(not by code) . I want to make it circular in shape . How do I do it? I don't want a circular button added from code but from storyboard.Thanks
You can make use of IBDesginable to make UIButton circular even in storyboard.
Add this class RoundButton in your project.
#IBDesignable class RoundButton : UIButton{
override init(frame: CGRect) {
super.init(frame: frame)
sharedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
sharedInit()
}
override func prepareForInterfaceBuilder() {
sharedInit()
}
func sharedInit() {
refreshCorners(value: cornerRadius)
}
func refreshCorners(value: CGFloat) {
layer.cornerRadius = value
}
#IBInspectable var cornerRadius: CGFloat = 15 {
didSet {
refreshCorners(value: cornerRadius)
}
}
}
Follow below steps to make it designable in Storyboard.
Add UIButton in viewcontroller and set some background color.
Set Width and Height same. Here both are 100.
Now you can see one more property named Corner Radius we declared in RoundButton class. Set it to 50 as half of Width/Height. You can set according to your need.
Now you can see rounded corner button within storyboard. You do not need to run code to see changes. You can make reuse of class in any other project to make button circular.
More about IBDesignable.
Very simple and easy.
yourButton.layer.cornerRadius = yourButton.frame.width/2
this will surely going to work.
Eat sleep code repeat😇
let button = UIButton(type: .custom)
button.frame = CGRect(x: 160, y: 100, width: 50, height: 50)
button.layer.cornerRadius = 0.5 * button.bounds.size.width
button.clipsToBounds = true
button.setImage(UIImage(named:"thumbsUp.png"), for: .normal)
button.addTarget(self, action: #selector(thumbsUpButtonPressed), for: .touchUpInside)

Xcode 8 Playground doesn't display NSView

Xcode doesn't display NSView in the playground. But it's displaying UIView without any problem. Is it a bug?
Code:
let view = NSView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.layer?.backgroundColor = NSColor.white.cgColor
PlaygroundPage.current.liveView = view
Also the Xcode Playground is very slow. Is there any way to speed up the playground?
UIView and NSView both are different in their workings. The code you posted is enough for UIView but not for NSView.
According to Apple Documentation for NSView:
An NSView object provides the infrastructure for drawing, printing,
and handling events in an app. You typically don’t use NSView objects
directly. Instead, you use objects whose classes descend from NSView
or you subclass NSView yourself and override its methods to implement
the behavior you need.
and
draw(_:) draws the NSView object. (All subclasses must implement this
method, but it’s rarely invoked explicitly.)
So, you must inherit NSView and implement draw(_:)
Here is the code:
import Cocoa
import PlaygroundSupport
class view: NSView
{
override init(frame: NSRect)
{
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect)
{
NSColor.blue.setFill()
NSRectFill(self.bounds)
}
}
var v = view(frame: NSRect(x: 0, y: 0, width: 200, height: 200))
PlaygroundPage.current.liveView = v
Output:
It is better to use iOS than macOS in Playground because it is easy and also you can find a ton of tutorials or answers.

Cocoa - Present NSViewController programmatically

Generally, We can able to display next view controller from first view controller by having different kind of NSStoryboardSeque like Present, Show, Sheet etc., But, How we can achieve the same programmatically?.
Comparing with UIViewController, presenting a view controller modally by presentViewController:animated:. Is there any same kind of approach for NSViewController?
Thanks in advance.
The two different presentation types I use are:
func presentViewControllerAsModalWindow(_ viewController: NSViewController)
func presentViewControllerAsSheet(_ viewController: NSViewController)
After doing some more research another way to do using:
func presentViewController(_ viewController: NSViewController, animator: NSViewControllerPresentationAnimator)
And eating a custom presentation animator. Here you have the freedom to do what you like :)
In case someone is looking for the solution in 2022,
extension NSViewController {
func presentInNewWindow(viewController: NSViewController) {
let window = NSWindow(contentViewController: viewController)
var rect = window.contentRect(forFrameRect: window.frame)
// Set your frame width here
rect.size = .init(width: 1000, height: 600)
let frame = window.frameRect(forContentRect: rect)
window.setFrame(frame, display: true, animate: true)
window.makeKeyAndOrderFront(self)
let windowVC = NSWindowController(window: window)
windowVC.showWindow(self)
}
}
1.Create a NSViewController instance with StoryBoard Identifier
let theTESTVCor = self.storyboard?.instantiateController(withIdentifier: "TESTVCor") as! NSViewController
2.Present In Via the current NSViewController
theNSViewController.presentViewControllerAsModalWindow(theTESTVCor)
⚠️ DO NOT FORGET to set the Identifier of the NSViewController in Storyboard
If you have a view controller (presenting) than it's as simple as following function are provided:
open func presentAsSheet(_ viewController: NSViewController)
open func presentAsSheet(_ viewController: NSViewController)
open func present(_ viewController: NSViewController, asPopoverRelativeTo positioningRect: NSRect, of positioningView: NSView, preferredEdge: NSRectEdge, behavior: NSPopover.Behavior)
If you need to present a view controller in a new window (NOT MODAL) you need to create own NSWindow, NSWindowController
let gridView = NSGridView(views: [
[NSTextField(labelWithString: "label1"),NSTextField(labelWithString: "label2")],
[NSTextField(labelWithString: "label3"),NSTextField(labelWithString: "label4")]
])
let viewController = NSViewController()
viewController.view = gridView
let window = NSWindow(contentViewController: viewController)
window.center()
let windowController = NSWindowController(window: window)
windowController.showWindow(nil)
EXPLANATION:
Storyboards are using seques to perform some magic. The show seque is simply calling action "perform:" on object NSStoryboardShowSegueTemplate ([NSApp sendAction:to:from]). This seque will create NSWindowController and NSWindow (private method windowWithContentViewController:) for you and on top it will layoutSubviews/resize and center the window. Magic bonus is self retaining the window so you don't care about memory management.
Example of programatic calling (using Storyboards to instantiate windowController with viewController)
import Cocoa
import Contacts
class ShorteningHistoryWindowController : NSWindowController, Storyboarded {
static var defaultStoryboardName = "ShorteningHistory"
}
struct ShorteningHistory {
static let shared = ShorteningHistory()
private var windowController : NSWindowController
private init() {
windowController = ShorteningHistoryWindowController.instantiate()
}
public func showHistory() {
windowController.showWindow(self)
}
}
extension Storyboarded where Self: NSWindowController {
static var defaultStoryboardName: NSStoryboard.Name { return String(describing: self) }
static var defaultIdentifer: NSStoryboard.SceneIdentifier {
let fullName = NSStringFromClass(self)
let className = fullName.components(separatedBy: ".")[1]
return className
}
static func instantiate() -> Self {
let storyboard = NSStoryboard(name: defaultStoryboardName, bundle: Bundle.main)
guard let vc = storyboard.instantiateController(withIdentifier: defaultIdentifer) as? Self else {
fatalError("Could not instantiate initial storyboard with name: \(defaultIdentifer)")
}
return vc
}
}
PS: Don't forget to set Storyboard Identifiers in Storyboard

Swift NSStackView : Learn By Doing. What am I doing wrong?

How do I get my NSStackView to lay out my view one after the other ? My blue box is drawing on top of my red box.
import Cocoa
class TestView : NSView{
override init() {
super.init(frame: NSRect(x: 0,y: 0,width: 100,height: 100))
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
NSColor.redColor().setFill()
NSBezierPath.fillRect(self.bounds)
}
}
class TestView2 : NSView{
override init() {
super.init(frame: NSRect(x: 0,y: 0,width: 100,height: 100))
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
NSColor.blueColor().setFill()
NSBezierPath.fillRect(self.bounds)
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
#IBOutlet weak var searchFacetItems: SearchFacetSelectorView!
#IBOutlet weak var searchFacetHeader: SearchFacetSelectorHeader!
var content : NSStackView!
let testView = TestView()
let testView2 = TestView2()
func applicationDidFinishLaunching(aNotification: NSNotification?) {
// Insert code here to initialize your application
content = NSStackView(frame: window.contentView.bounds)
content.orientation = NSUserInterfaceLayoutOrientation.Vertical
content.alignment = NSLayoutAttribute.CenterX
content.addView(testView, inGravity: NSStackViewGravity.Center)
content.addView(testView2, inGravity: NSStackViewGravity.Center)
window.contentView = content!
}
func applicationWillTerminate(aNotification: NSNotification?) {
// Insert code here to tear down your application
}
}
learn by reading
It's not appkit, but maybe i'll get some clues: HOW TO USE UIVIEWS WITH AUTO LAYOUT PROGRAMMATICALLY
Auto Layout Guide
You should be able to do this by adjusting the NSRect coordinates. Currently you've got both NSRects using the same exact coordinates:
NSRect(x: 0, y: 0, width: 100, height: 100)
If you want the NSView horizontally (on the right) located next of the other:
NSRect(0, 0, 100, 100) // TestView1
NSRect(100, 0, 100, 100) // TestView2
vertically below:
NSRect(0, 0, 100, 100) // TestView1
NSRect(0, -100, 100, 100) // TestView2
The views don't have any constraints that describe their preferred sizes, such as intrinsicContentSizes or explicit constraints. And NSStackView only adds constraints that positions its stacked views relative to each other, not ones to size individual views.
Without them, their size in the stacking axis becomes ambiguous and will typically give all of the sizing to a single view. In your example, I'd guess that the blue box is not drawing on top of the red box, but just that the red box has a 0 height (and is stacked after the blue view).
Adding NSButtons to a stack view don't have this problem, as they do have a defined intrinsicContentSize.
Depending on what your views are -- do they have intrinsic sizes, or will they be defined by constraints to internal subviews -- you'll either want to override intrinsicContentSize or add those constraints that end up defining their heights.
Edit: Oh, and make sure translatesAutoresizingMaskIntoConstraints is set to false on the views you're adding to the stack view (it doesn't look like you are from the sample code). If not, you'll quickly run into trouble where the autoresizing constraints are trying to position the view and conflicting with constraints that the stack view adds.

Resources