Hiding Dividers in NSSplitView - swift

Since NSSplitView doesn't allow for hiding its dividers (the delegate method only allows for hiding dividers that are on the split views edge), I chose to subclass NSSplitView and override its draw methods to prevent specific dividers from drawing.
However, as soon as I override either draw(rect:) or drawDivider(in:) the NSSplitView no longer animates it's dividers if I collapse an item like so
activityItem.animator().isCollapsed = collapsed
It even happens if I call super directly without adding my own drawing code
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
The code above is enough to completely break animations.
Basically all I am trying to achieve is hiding a split view item alongside its divider, but that is apparently too much to ask of a NSSplitView without reimplementing it completely.
I'm on my last straw here. Any other method to accomplish hiding items + divider?

Ok, I went a whole different way and found a way to make it work. So if you are trying to completely customize dividers this is how you do it.
Subclass NSSplitView and return 0 from dividerThickness
So your new split view won't display dividers at all now, but you can add them manually where you want them
Add NSBox or your custom divider views where you want your dividers to show up in your split view subviews, preferrably at the top of the subview.
Override the split view delegate method splitView(:additionalEffectiveRectOfDividerAt:) and manually return rects that match your custom NSBox dividers
You might need to convert(:from:) between NSView coordinates to get your effective rects, but it works! The delegate might look something like this
override func splitView(_ splitView: NSSplitView, additionalEffectiveRectOfDividerAt dividerIndex: Int) -> NSRect {
let item = splitViewItems[dividerIndex]
let itemView = item.viewController.view
let frame = view.convert(itemView.bounds, from: itemView)
let dividerFrame = CGRect(x: 0,
y: view.bounds.height - frame.minY,
width: frame.width,
height: 1)
return dividerFrame
}
There you have it. Custom dividers that also work with animations!

Related

How to make a CAShapeLayer have a blur effect?

I have this CAShapeLayer that I want to have a blur effect. How would I be able to do that?
EDIT
I tried it this way but the blur view doesn't show. Anyone know why? Thanks!
func createLayer(in rect: CGRect) -> CAShapeLayer{
let effectView = UIVisualEffectView(effect:UIBlurEffect(style: .regular))
effectView.frame = rect
let view = UIView(frame: rect)
view.addSubview(effectView)
let mask = CAShapeLayer()
mask.frame = rect
mask.cornerRadius = 10
effectView.layer.mask = mask
maskLayer.append(mask)
layer.insertSublayer(mask, at: 1)
return mask
}
The short answer: You don't. You can add a visual effects view (UIVisualEffectView) of type blur (a UIBlurEffect) on top of the shape layer's view, or you could write code that takes the contents of the shape layer, applies a Core Image filter to it, and copies the output to another layer.
Using a UIVisualEffectView is a lot easier than working with Core Image filters, but a visual effect view operates on a view, not a layer. You'll need to make the shaper layer be part of the layer's layer hierarchy in order to use it.
Edit:
Your code has errors and doesn't really make sense. Your method createLayer (which I guess is a view controller instance method?) creates and returns a shape layer.
That method creates a throw-away UIView that is never added to the view hierarchy, nor passed back to the caller. That view will get deallocated as soon as your method returns.
Next you create a visual effects view and make that a subview of the throw-away view. Since the only place that view is attached is to the throw-away view, it will also get deallocated as soon as your method returns.
Next you create a shape layer and set it up as the mask of some other layer maskLayer, which you don't explain. Nor do you install a path into the shape layer.
If you have a view called shapeView, of class ShapeView, and you want to attach a visual effects view to it, you could use code like this:
class ViewController: UIViewController {
#IBOutlet weak var shapeView: ShapeView!
var blurView: UIVisualEffectView?
override func viewDidLoad() {
super.viewDidLoad()
blurView = UIVisualEffectView(effect:UIBlurEffect(style: .regular))
blurView?.frame = shapeView.frame
//Add the blur view on top of the shape view
view.addSubview(blurView!)
}
override func viewDidLayoutSubviews() {
//Update the blurView's frame if needed
blurView?.frame = shapeView.frame
}
}

Custom TabBarController with active circle

After reading a few articles about custom UITabBarControllers, I am left more confused than before I even started doing the research in the first place.
My goal is to create a custom TabBar with 3 important properties:
No text, just icons
The active icon is marked by a circle filled with a color behind it, and therefore needs a different icon color
Here's what I am trying to achieve:
I've been able to remove the text and center the icon by following another StackOverflow answer (Remove tab bar item text, show only image), although the solution seems like a hack to me.
How would I go about creating a circle behind the item and change the active item's color?
Also, would someone mind explaining the difference between the XCode inspector sections "Tab Bar Item" and "Bar Item", which appear directly under each other?
The first step is simple: leaving the title property of the UITabbarItem empty should hide the label.
Your second step can actually be broken down into two steps: changing the color of the icon and adding a circle behind it.
The first step here is simple again: you can set a different icon to use for the currently selected ViewController (I use Storyboards, this process is pretty straightforward). What you'd do is add a white version of the icon to be shown when that menu option is selected.
The final step is displaying the circle. To do this, we'll need the following information:
Which item is currently selected?
What is the position of the icon on the screen?
The first of these two is pretty easy to find out, but the second poses a problem: the icons in a UITabBar aren't spaced around the screen equally, so we can't just divide the width of the tabbar by the amount of items in it, and then take half of that to find the center of the icons. Instead, we will subclass UITabBarController.
Note: the tabBar property of a UITabBarController does have a .selectionIndicatorImage property. You can assign an image to this and it will be shown behind your icon. However, you can't easily control the placement of this image, and that is why we still resort to subclassing UITabBarController.
class CircledTabBarController: UITabBarController {
var circle: UIView?
override func viewDidLoad() {
super.viewDidLoad()
let numberOfItems = CGFloat(tabBar.items!.count)
let tabBarItemSize = CGSize(width: (tabBar.frame.width / numberOfItems) - 20, height: tabBar.frame.height)
circle = UIView(frame: CGRect(x: 0, y: 0, width: tabBarItemSize.height, height: tabBarItemSize.height))
circle?.backgroundColor = .darkGray
circle?.layer.cornerRadius = circle!.frame.width/2
circle?.alpha = 0
tabBar.addSubview(circle!)
tabBar.sendSubview(toBack: circle!)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let index = -(tabBar.items?.index(of: tabBar.selectedItem!)?.distance(to: 0))!
let frame = frameForTabAtIndex(index: index)
circle?.center.x = frame.origin.x + frame.width/2
circle?.alpha = 1
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
let index = -(tabBar.items?.index(of: item)?.distance(to: 0))!
let frame = frameForTabAtIndex(index: index)
self.circle?.center.x = frame.origin.x + frame.width/2
}
func frameForTabAtIndex(index: Int) -> CGRect {
var frames = tabBar.subviews.compactMap { (view:UIView) -> CGRect? in
if let view = view as? UIControl {
for item in view.subviews {
if let image = item as? UIImageView {
return image.superview!.convert(image.frame, to: tabBar)
}
}
return view.frame
}
return nil
}
frames.sort { $0.origin.x < $1.origin.x }
if frames.count > index {
return frames[index]
}
return frames.last ?? CGRect.zero
}
}
Now use this subclass of UITabBarController instead of the base class.
So why this approach over simply changing the icon to a circled one? Because you can do many different things with this. I wrote an article about animating the UITabBarController in a similar manner, and if you like, you can easily use above implementation to add animation to yours too.
The easiest and actually cleanest way to do it is to design your icons and import them as images to the .xcassets folder. Then you can just set the different icons for the different states for each of the viewControllers with:
ViewController.tabBarItem = UITabBarItem(title: "", image: yourImage.withRenderingMode(.alwaysOriginal), selectedImage: yourImage)
your selected image will be the one with the circle and the image will be without. It is way easier than manipulating the images in xcode and it is also less expensive since the compiler only has to render the images and doesn't have to manipulate them.
About the other question UIBarItem is
An abstract superclass for items that can be added to a bar that appears at the bottom of the screen.
UITabBarItem is a subclass of UIBarItem to provide extra funtionality.

NSScrollView not scrolling

I have a form in a Mac app that needs to scroll. I have a scrollView embedded in a ViewController. I have the scrollView assigned with an identifier that links it to its own NSScrollView file. The constraints are set to the top, right, and left of the view controller, it also has the hight constraint set to the full height of the ViewController.
Here is my code:
import Cocoa
class ScrollView: NSScrollView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
NSRect documentView.NSMakeSize(0, 0, 1058.width, 1232.height)
}
override func scrollWheel(with event: NSEvent) {
switch event.phase {
case NSEvent.Phase.began:
Swift.print("Began")
// case NSEvent.Phase.changed:
// Swift.print("Changed")
case NSEvent.Phase.ended:
Swift.print("Ended")
default:
break
}
switch event.momentumPhase {
case NSEvent.Phase.began:
Swift.print("Momentum Began")
// case NSEvent.Phase.changed:
// Swift.print("Momentum Changed")
case NSEvent.Phase.ended:
Swift.print("Momentum Ended")
default:
break
}
super.scrollWheel(with: event)
}
I cant seem to get my app to scroll at all. I think I am not setting the frame correctly. What is the best way to do set the frame correctly? Am I coding the NSScrollView correctly?
I think you are making your life very hard because you are doing things that are not exactly recommended by Apple. First of all, you should not subclass NSScrollView. Rather you should read first Introduction to Scroll View Programming Guide for Cocoa by Apple to understand how you should create the correct hierarchy of views for an NSScrollView to work correctly.
A second recommendation is for you to check this nice article about how you should set up an NSScrollView in a playground, so that you can play with the code you want to implement.
Third, using Autolayout and NSScrollView has caused a lot of grief to a lot of people. You need to set up the AutoLayout just right, so that everything is going to work as expected. I recommend that you check this answer by Ken Thomases, which clearly explains how you need to set up auto layout constraints for an NSScrollView to work properly.
I just got over the "hump" with a NSScrollView inside a NSWindow. In order for scrolling to occur the view inside the NSScrollview needs to be larger than the content window. That's hard to set with dynamic constraints. Statically setting the inner view to a larger width/height than the window "works" but the static sizes usually are not what you want.
Here is my interface builder view hierarchy and constraints, not including the programmatically added boxes
In my app the user is adding "boxes" (custom draggable views) inside the mainView, which is inside a scrollview in a NSwindow.
Here's the functionality I wanted:
If I expanded the NSWindow, I wanted the mainView inside the scrollview to expand to fill the whole window. No scrolling needed in this case if all the boxes are visible.
If I shrank the NSWindow, I wanted the mainView inside the scrollview to shrink just enough to include all my mainView subviews ("boxes"), but not any further (i added a minBorder of 20). This results in scrolling if a box's position is further right/up than the nswindow's width/height.
I found the trick is to calculate the size of the mainView I want based on the max corner of each draggable boxview, or the height/width of the content frame of the nswindow, whichever is larger.
Below is my code, including some debugging prints.
Be careful of which subviews you use to calculate the max size. If you include a subview that's dynamically attached to the right/top of the window, then your window will never shrink. If you add +20 border to that, you might infinite loop. Not a problem in my case.
extension MapWindowController: NSWindowDelegate {
func windowDidEndLiveResize(_ notification: Notification) {
if let frame = window?.frame, let content = window?.contentRect(forFrameRect: frame) {
print("window did resize \(frame)")
var maxX: CGFloat = content.width
var maxY: CGFloat = content.height
for view in mainView?.subviews ?? [] {
let frameMaxX = view.frame.maxX + minBorder
let frameMaxY = view.frame.maxY + minBorder
if frameMaxX > maxX {
maxX = frameMaxX
}
if frameMaxY > maxY {
maxY = frameMaxY
}
}
print("view maxX \(maxX) maxY \(maxY)")
print("window width \(content.width) height \(content.height)")
mainView?.setFrameSize(NSSize(width: maxX, height: maxY))
}
}
}

viewDidLayoutSubviews is called when label changes (xcode) (swift)

I have a label, toggle button and an animation in my code. When I press the toggle button -> animation starts , label changes. Below is my code sample.
override func viewDidLayoutSubviews() {
println("viewDidLayoutSubviews is called")
// Initial state of my animation.
}
#IBAction func Toggled(sender: AnyObject) {
CallTextChange()
// Two different Animations
}
func CallTextChange() { // Change text here}
Every time I change the text in label viewDidLayoutSubviews is called.
Is there a way to stop calling it every time I change the label?
I found the answer for my problem.
When we create UIImage by drag and dropping from the object provided by Xcode, the image is temporary placed where it was statically placed. So when animating in middle when viewDidLayoutSubviews is called the image is placed in the static place.
So the UIImage has to be created programmatically.
CreateImage.image = UIImage(named: "Image.png")
CreateImage = UIImageView(frame: CGRect(x: 0, y: 0, width: CreateImage.image!.size.width/6, height: CreateImage.image!.size.height/6))
self.view.addSubview(CreateImage)
Try to pass one boolean flag values in function
I think when you change text of and UILabel it will change its intrinsic content size that in turn will notify the system to relayout the view hierarchy. So it seems inevitable that viewDidLayoutSubviews will be called.
Either you put a boolean flag to make your initial animation code execute once only or move your code to other place like viewDidLoad() method.

NSScrollView with unclipped content view?

Is there a way I can set my scrollview not to clip its contents? (Which is a NSTextView)
I have a subclass of NSScrollView and want its content not to be clipped to its bounds.
I have tried overriding:
- (BOOL) wantsDefaultClipping{
return NO;
}
in MyScrollView and in MytextView without any effect.
In the iOS I would simply would do: myuitextView.clipsToBounds=NO; how can I do this in Cocoa?
EDIT
This is an example of what I want to achieve but in the mac
The scrollview is white, the scroller will never go outside its bounds but the text does since I did myuitextView.clipsToBounds=NO
See picture here
EDIT2
I wouldn't mind clip my view like #Josh suggested. But the real behaviour I would like to have can be explained with this picture:
Do you see the word *****EDIT***** that has being cut in the very first line?
I want the text not to be cut this way, rather I want it to completely appear and I will put a semitransparent image so it looks like it fades off when it's outside the frame.
Q: Why don't I simply put a semitransparent NSImageView on it so it looks like what I want?
A: Because 1.Scroller will be faded as well. Even if I correctly place the semitransparent NSImageView so the scroller looks fine, the cursor/caret will be able to go underneath the semitransparent NSImageView again it does not look good.
I would like to be able to control the area is clipped by NSClipView. I think that would solve my problem. Is there any alternative I have? maybe I can control the caret position or scrolling position through NSTextView so caret will never go near the top/bottom frame limits? or any work-around?
Any advice is appreciated.
Now that it's 2016 and we're using vibrant titlebars with full size content views, I'll add my thoughts to how someone might accomplish this. Hopefully, this will help anyone who came here looking for help on this, as it helped me.
This answers the question in regards to scrolling under the titlebar, but you could easily modify this technique to scroll under other things using the insets and caret position.
To get a scroll view (with or without an NSTextView inside of it) to scroll behind a titlebar, you can use:
// For transparent title.
window.titlebarAppearsTransparent = true
window.styleMask = window.styleMask | NSFullSizeContentViewWindowMask
window.appearance = NSAppearance(named: NSAppearanceNameVibrantLight)
This effectively overlays the titlebar of the NSWindow onto the window's contentView.
To constrain something to the top of the window without knowing the height of the titlebar:
// Make a constraint for SOMEVIEW to the top layout guide of the window:
let topEdgeConstraint = NSLayoutConstraint(
item: SOMEVIEW, attribute: NSLayoutAttribute.Top,
relatedBy: NSLayoutRelation.Equal,
toItem: window.contentLayoutGuide,
attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 0.0)
// Turn the constraint on automatically:
topEdgeConstraint.active = true
This allows you to constrain the top of an element to the bottom of the titlebar (and or toolbar + any accessory views it may have). This was shown at WWDC in 2015: https://developer.apple.com/videos/play/wwdc2014/220/
To get the scrollview to scroll under the titlebar but show its scrollbars inside the unobscured part of the window, pin it to the top of the content view in IB or via code, which will cause it to be under the titlebar. Then, tell it to automatically update it's insets:
scrollView.automaticallyAdjustsContentInsets = true
Finally, you can subclass your window and handle the cursor/caret position. There is a presumed bug (or developer error on my part) that doesn't make the scrollview always scroll to the cursor/caret when it goes above or below the content insets of the scrollview.
To fix this, you must manually find the caret position and scroll to see it when the selection changes. Forgive my awful code, but it seems to get the job done. This code belongs in an NSWindow subclass, so self is referring to the window.
// MARK: NSTextViewDelegate
func textViewDidChangeSelection(notification: NSNotification) {
scrollIfCaretIsObscured()
textView.needsDisplay = true // Prevents a selection rendering glitch from sticking around
}
// MARK: My Scrolling Functions
func scrollIfCaretIsObscured() {
let rect = caretRectInWindow()
let y: CGFloat = caretYPositionInWindow() - rect.height
// Todo: Make this consider the text view's ruler height, if present:
let tbHeight: CGFloat
if textView.rulerVisible {
// Ruler is shown:
tbHeight = (try! titlebarHeight()) + textViewRulerHeight
} else {
// Ruler is hidden
tbHeight = try! titlebarHeight()
}
if y <= tbHeight {
scrollToCursor()
}
}
func caretYPositionInWindow() -> CGFloat {
let caretRectInWin: NSRect = caretRectInWindow()
let caretYPosInWin: CGFloat = self.contentView!.frame.height - caretRectInWin.origin.y
return caretYPosInWin
}
func caretRectInWindow() -> CGRect {
// My own version of something based off of an old, outdated
// answer on stack overflow.
// Credit: http://stackoverflow.com/questions/6948914/nspopover-below-caret-in-nstextview
let caretRect: NSRect = textView.firstRectForCharacterRange(textView.selectedRange(), actualRange: nil)
let caretRectInWin: NSRect = self.convertRectFromScreen(caretRect)
return caretRectInWin
}
/// Scrolls to the current caret position inside the text view.
/// - Parameter textView: The specified text view to work with.
func scrollToCursor() {
let caretRectInScreenCoords = textView.firstRectForCharacterRange(textView.selectedRange(), actualRange: nil)
let caretRectInWindowCoords = self.convertRectFromScreen(caretRectInScreenCoords)
let caretRectInTextView = textView.convertRect(caretRectInWindowCoords, fromView: nil)
textView.scrollRectToVisible(caretRectInTextView)
}
enum WindowErrors: ErrorType {
case CannotFindTitlebarHeight
}
/// Calculates the combined height of the titlebar and toolbar.
/// Don't try this at home.
func titlebarHeight() throws -> CGFloat {
// Try the official way first:
if self.titlebarAccessoryViewControllers.count > 0 {
let textViewInspectorBar = self.titlebarAccessoryViewControllers[0].view
if let titlebarAccessoryClipView = textViewInspectorBar.superview {
if let view = titlebarAccessoryClipView.superview {
if let titleBarView = view.superview {
let titleBarHeight: CGFloat = titleBarView.frame.height
return titleBarHeight
}
}
}
}
throw WindowErrors.CannotFindTitlebarHeight
}
Hope this helps!
I would simply try to observe the document view's frame and match the scroll view's frame when the document resizes.
This is a little hairy. AFAIK, NSViews can't draw outside their own frame. At any rate I've never seen it done, and I was somewhat surprised when I realized that UIView allows it by default. But what you probably want to do here is not manipulate clipping rectangles (doing any such thing inside NSScrollView will probably not do what you want or expect), but instead try to cover up the vertically-truncated text lines with either layers or views that are the same color as the background. Perhaps you could subclass NSClipView and override viewBoundsChanged: and/or viewFrameChanged: in order to notice when the text view is being shifted, and adjust your "shades" accordingly.
You might consider using a translucent layer to achieve this appearance, without actually drawing outside your view. I'm not certain of the rules on iOS, but on the Mac, a view drawing outside its bounds can cause interference with surrounding drawing.
However, you can set the clipping region to be whatever you like inside your scroll view subclass's drawRect: using -[NSBezierPath setClip:]:
- (void)drawRect:(NSRect)dirtyRect {
[NSGraphicsContext saveGraphicsState];
[[NSBezierPath bezierPathWithRect:[[self documentView] frame]] setClip];
//...
[NSGraphicsContext restoreGraphicsState];
}
It might be possible (since you asked) to use this code in an NSClipView subclass, but there's not much info about that, and I think you may have a hard time making it interact properly with its scroll view. If it were me, I'd try subclassing NSScrollView first.