How to use NSScroller in a custom NSView - nsview

I have a custom NSView with two NSClipViews. I want to add a vertical scroller for each clipview. The snippet below shows a NSScroller, but it is not drawing any knob or buttons. Interaction is also not possible.
NSScroller is a small class, so I am not sure what I am missing here.
required init?(coder: NSCoder)
{
super.init(coder: coder)
let width = NSScroller.scrollerWidthForControlSize(NSControlSize.RegularControlSize, scrollerStyle: NSScrollerStyle.Overlay)
let scroller = NSScroller(frame: NSRect(x: 10, y: 10, width: width, height: 500))
self.addSubview(scroller)
scroller.doubleValue = 0.5
scroller.knobProportion = 0.1
scroller.needsLayout = true //no effect
scroller.needsDisplay = true //no effect
}

The missing line:
scroller.enabled = true

Related

NSWindow - scale all content when resizing

I cannot figure out how to accomplish what seems to be a very simple task. I have a macOS cocoa application which consists of one window with a custom view inside. The view is 600x500 points. Inside this view is a subwiew that spans the whole width of the main view, but only a small fraction of the view's heigth. Its vertical origin is at about y=200.
The subview draws a rect starting 20 points off the left and right border, about 30 points in heigth. It also uses its layer with a CAEmitter layer as sublayer. This emitter's position can be controlled by the user, so its position will be updated by a function called from outside the view.
What I want:
When the user resizes the window, it should only scale proportionally (I managed to to that), so that the window's content as a whole gets resized. No subviews should get rearranged in any way, they all should keep their size and positions relative to each other, just getting bigger/smaller.
I do not get this to work.
What I have tried so far:
With no additional properties set I can resize the window, but the
subiew does not scale at all, it sticks to its position in the
superview.
I set subView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor).isActive = true and the same for the trailingAnchor, additionally I set subView.autoresizingMask = [.width] Now the width of the rect drawn in the subview gets resized, but the rect's edges stay away 20 points from the border. I want this distance to be scaled as well.
The rect's heigth stays fixed. When I add -heigth to the subview's autoresizing mask the heigth gets distorted in a riculously disproportianal way (the distance to top and bottom margins are kept constant, so although the subview's heigth is only about 30 at start the rect gets blown up on resizing.
The emitter does not get repositioned. Its horizontal position gets calculated on user interaction and is relative to the view's borders, after resizing it sticks to its absloute position. I solved that by calling the calculating function from the draw() method, but that does not seem right to me
There are so many entry points in the cocoa framework regarding sizes, anchors, constraints etc., I simply do not know where to start, so any help would be appreciated.
UPDATE: I added code stripped down to the min:
class MyViewController: NSViewController {
public init () {
super.init(nibName: nil, bundle: nil)
self.view = MyView(frame: NSRect(x: 0, y: 0, width: 600, height: 500))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyView: NSView {
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
let subView = MySubView(frame: NSRect(x: 0, y: frame.height/3, width: frame.width, height: frame.height/10))
self.addSubview(subView)
subView.autoresizingMask = [.width, .height]
subView.translatesAutoresizingMaskIntoConstraints = true
subView.trailingAnchor.constraint(greaterThanOrEqualTo: self.trailingAnchor).isActive = true
subView.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor).isActive = true
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MySubView: NSView {
override func draw(_ dirtyRect: NSRect) {
let context = NSGraphicsContext.current!.cgContext
context.setStrokeColor(.black)
context.setFillColor(CGColor.init(red: 0.7, green: 0.7, blue: 0.7, alpha: 1))
context.setLineWidth(2)
let borderRect = CGRect(x: constant.sideMargin, y: 5, width: frame.width-2*constant.sideMargin, height: frame.height-10)
let roundedRect = CGPath.init(roundedRect: borderRect, cornerWidth: 5, cornerHeight: 5, transform: nil)
context.addPath(roundedRect)
context.fillPath()
context.strokePath()
context.addPath(roundedRect)
context.strokePath()
}
}
As explained in the View Programming Guide, NSView will scale and offset its contents if its bounds is different from its frame. Just set bounds to {600, 500} when frame changes. The subviews of the view don't need constraints because their coordinates don't change.
MyView setup code:
// frame changed notitication
self.postsFrameChangedNotifications = true
self.frameChangeObserver = NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification,
object: self, queue: OperationQueue.main) { (note) in
self.setBoundsSize(NSSize(width:600, height: 500))
}
let subView = MySubView(frame: NSRect(x: 0, y: bounds.height/3, width: bounds.width, height: bounds.height/10))
// no constraints
subView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(subView)

Weird blurred SKShapeNode

I am trying to start with the basics of SpriteKit and draw a rounded rectangle
import Foundation
import SpriteKit
class UINode: SKShapeNode {
override init() {
super.init()
self.path = UIBezierPath(roundedRect: CGRect(x: -5, y: -2, width: 10, height: 4), cornerRadius: 1).cgPath
self.position = CGPoint(x: 0, y: 0)
self.fillColor = UIColor.red
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
The respective Scene containing that SKShapeNode contains a SKCameraNode, which can be moved and scaled for panning/zooming. That works fine.
However the output of that Shape is very strange, as you can see in the attachment. This is the result of the above code.
I have no idea what's going on. If I reduce the cornerRadius to zero, I get a rectangular shape with the same strange white border and artifacts. But I except a simple rounded Rect without any blurring, second color or artifacts (thin lines)
What am I doing wrong?
Rendering result

Custom Class Not Drawing at Center - Swift

I have a UIView with a custom class of TimerView. I am trying to draw a dot in the exact center of the UIView but it is appearing at the bottom slightly off center:
Custom class is here:
import UIKit
class TimerView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
drawDot()
}
func drawDot() {
let midViewX = self.frame.midX
let midViewY = self.frame.midY
let dotPath = UIBezierPath(ovalIn: CGRect(x: midViewX, y: midViewY, width: 5, height: 5))
let dotLayer = CAShapeLayer()
dotLayer.path = dotPath.cgPath
dotLayer.strokeColor = UIColor.blue.cgColor
self.layer.addSublayer(dotLayer)
}
}
Here is the re-implementation of TimerView class. For UIView, init is not the best place to get the frame/bounds values because it might change once the autolayout applies the constraints in runtime. layoutSubviews is the best place to get the correct frame/bounds values for parent/child views and to setup the child space properties. Secondly you should be using parent view bounds to setup the child's frame.
class TimerView: UIView {
private var dotLayer: CAShapeLayer?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
dotLayer = CAShapeLayer()
dotLayer?.strokeColor = UIColor.blue.cgColor
self.layer.addSublayer(dotLayer!)
}
override func layoutSubviews() {
super.layoutSubviews()
drawDot()
}
func drawDot() {
let midViewX = self.bounds.midX
let midViewY = self.bounds.midY
let dotPath = UIBezierPath(ovalIn: CGRect(x: midViewX, y: midViewY, width: 5, height: 5))
dotLayer?.path = dotPath.cgPath
}
}

Swift 3 - Custom button (image and text) only draws correctly if there's no outlet?

EDIT: Although I accepted an answer that helped me get the buttons drawn correctly at runtime, I still have other issues. The root of which, I suspect, is the issue of why giving my custom button an outlet interferes with how it is drawn. I still need to know why this is. (see my answer below)
I have my own button class that extends UIButton (see below) and has several IBInspectable properties like border width/color, corner radius, and even start/end colors for a gradient background. I also use such properties for setting the image and titles' insets programmatically so I can account for various screen sizes.
Previously I had an issue where if I changed the "View as" device in the storyboard, let's say from iPhone SE to iPhone 7, and then refreshed the views and ran on a physical iPhone SE it would deploy with the insets of an iPhone 7 instead of calculating the insets with the physical device's own screen size. However, I nearly resolved this problem by overriding draw in my custom button class.
My problem now is that the overridden draw method is only called (or seems to be effective only when) the button has no outlet to the ViewController class it's placed in.
For example:
*The img is a placeholder; the insets suit the real imgs appropriately.
That bottom-right button has the gradient and corners drawn properly, where the other 3 do not. I have confirmed that adding an outlet to this button makes it behave like the rest, and removing an outlet from any of the other 3 causes it to be drawn correctly.
For reference, the outlets are just
#IBOutlet weak var button1: UIButton!
#IBOutlet weak var button2: UIButton!
#IBOutlet weak var button3: UIButton!
immediately under the class MyViewController: UIViewController { declaration.
Also, I didn't forget to set the Custom Class in the interface builder for each button.
Here is the button class:
import UIKit
#IBDesignable
class ActionButton: UIButton {
override init(frame: CGRect){
super.init(frame: frame)
setupView()
}
required init(coder aDecoder: NSCoder){
super.init(coder: aDecoder)!
setupView()
}
#IBInspectable var cornerRadius: CGFloat = 0{
didSet{
setupView()
}
}
#IBInspectable var startColor: UIColor = UIColor.white{
didSet{
setupView()
}
}
#IBInspectable var endColor: UIColor = UIColor.black{
didSet{
setupView()
}
}
#IBInspectable var borderWidth: CGFloat = 0{
didSet{
self.layer.borderWidth = borderWidth
}
}
#IBInspectable var borderColor: UIColor = UIColor.clear{
didSet{
self.layer.borderColor = borderColor.cgColor
}
}
private func setupView(){
let colors:Array = [startColor.cgColor, endColor.cgColor]
gradientLayer.colors = colors
gradientLayer.cornerRadius = cornerRadius
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
self.setNeedsDisplay()
}
var gradientLayer: CAGradientLayer{
return layer as! CAGradientLayer
}
override func draw(_ rect: CGRect) {
//Draw image
setupView()
let btnW = self.frame.size.width
let btnH = self.frame.size.height
//Compute and set image insets
let topBtnInset = btnH * (-5/81)
let bottomBtnInset = -4 * topBtnInset
let leftBtnInset = btnW / 4 //btnW * (35/141)
let rightBtnInset = leftBtnInset
self.imageEdgeInsets = UIEdgeInsets(top: topBtnInset, left: leftBtnInset, bottom: bottomBtnInset, right: rightBtnInset)
//Compute and set title insets
let topTitleInset = btnH * (47/81)
let bottomTitleInset = CGFloat(0)
let leftTitleInset = CGFloat(-256) //Image width
let rightTitleInset = CGFloat(0)
self.titleEdgeInsets = UIEdgeInsets(top: topTitleInset, left: leftTitleInset, bottom: bottomTitleInset, right: rightTitleInset)
//Draw text
//Default font size
self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 15)
if(btnH > 81){
self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 17)
}else if(btnH > 97){
self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20)
}
}
override class var layerClass: AnyClass{
return CAGradientLayer.self
}
}
Please ignore the wonky way I handled insets, font sizes, etc..
Does anyone know why specifying an outlet for the button makes it not draw the layer correctly?
A couple of thoughts:
The draw(_:) method is for stroking paths, drawing images, etc. It is for rendering the view at a given moment of time. You should not be updating layers, UIKit subviews, etc.
In your snippet, your draw(_:) is calling setupView, which then calls setNeedsDisplay (which theoretically could trigger draw(_:) to be called again). I'd suggest reserving draw(_:) for only those things that need to be manually drawn. (As it is now, you probably could completely eliminate this method as there's nothing here that is drawing anything.)
For any manual configuration of subviews (their frames, etc.), that should be moved to layoutSubviews, which is called whenever the OS determines that the subviews might need to have their frame values adjusted.
If you have any troubles with designable views, it's worth confirming that you have these in a separate target. It minimizes what needs to be recompiled when rendering in IB. It also prevents issues in the main target affecting the ability to render these designables in IB. (When Apple introduced designable views, they were very specific that one should put them in a separate target.)
I've also had intermittent problems with designable views in Xcode, but these problems are often resolved by quitting Xcode, emptying the derived data folder, and restarting.
I must confess that I do not know why adding an outlet to a designable view would affect how the view is rendered. I know that when I did the above, though, I was unable to reproduce your problem.
Anyway, I tweaked your ActionButton like so, and it appears to work for me:
#import UIKit
#IBDesignable
public class ActionButton: UIButton {
public override init(frame: CGRect = .zero) {
super.init(frame: frame)
setupView()
}
public required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
setupView()
}
#IBInspectable public var cornerRadius: CGFloat = 0 { didSet { setupView() } }
#IBInspectable public var startColor: UIColor = .white { didSet { setupView() } }
#IBInspectable public var endColor: UIColor = .black { didSet { setupView() } }
#IBInspectable public var borderWidth: CGFloat = 0 { didSet { layer.borderWidth = borderWidth } }
#IBInspectable public var borderColor: UIColor = .clear { didSet { layer.borderColor = borderColor.cgColor } }
private func setupView() {
let colors = [startColor.cgColor, endColor.cgColor]
gradientLayer.colors = colors
gradientLayer.cornerRadius = cornerRadius
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
}
private var gradientLayer: CAGradientLayer {
return layer as! CAGradientLayer
}
public override func layoutSubviews() {
super.layoutSubviews()
let btnW = frame.width
let btnH = frame.height
//Compute and set image insets
let topBtnInset = btnH * (-5/81)
let bottomBtnInset = -4 * topBtnInset
let leftBtnInset = btnW / 4 //btnW * (35/141)
let rightBtnInset = leftBtnInset
imageEdgeInsets = UIEdgeInsets(top: topBtnInset, left: leftBtnInset, bottom: bottomBtnInset, right: rightBtnInset)
//Compute and set title insets
let topTitleInset = btnH * (47/81)
let bottomTitleInset = CGFloat(0)
let leftTitleInset = CGFloat(-256) //THIS MUST BE EQUAL TO -IMAGE WIDTH
let rightTitleInset = CGFloat(0)
titleEdgeInsets = UIEdgeInsets(top: topTitleInset, left: leftTitleInset, bottom: bottomTitleInset, right: rightTitleInset)
//Adjust text font
if btnH > 81 {
titleLabel?.font = .boldSystemFont(ofSize: 17)
} else if btnH > 97 {
titleLabel?.font = .boldSystemFont(ofSize: 20)
} else {
//Default font size
titleLabel?.font = .boldSystemFont(ofSize: 15)
}
}
public override class var layerClass: AnyClass {
return CAGradientLayer.self
}
}
I've accepted Rob's answer because it is far more likely to be of use to anyone else, but I've found what specifically was my issue, on the off-chance someone has a similar problem.
Apparently in my code for the View Controller on which those buttons were placed I had a function I had forgot about that enables/disables those custom buttons, as well as set their background color (to a solid color). This is why those buttons looked wrong. The 4th button was never subject to mutation by this function. It was not the fact the other 3 had an outlet, but that when I removed the outlet I commented-out the call to that function.
Nevertheless, other problems were addressed in this question!
-I had a really sloppy implementation of a UIButton that overrode and called UI functions improperly, that caused problems like freezing on rotation. These were also addressed in Rob's (the accepted) answer.
-"Refresh All Views" in the Interface Builder was hanging on "Signing product", which was caused by calling self.titleLabel?.font. According to some answers on the Apple developer forums, doing this in layoutSubViews() is bad!

Swift creating a line inside a view that the user can resize

How can I create a simple line that I can control its originating point, but let the user determine where it ends?
I have a UIView on my storyboard and on top of this I want to insert a line. The line would be restricted to be within this particular view and would start vertically centered and from the left side.
I have buttons with a - and a + to allow the user to increase its length or decrease it.
I suppose the easiest method will be to a add another UIView (call it lineView) inside your main view, set it's width to whatever you want the width of your line to be and hook it up with an #IBOutlet. Then in your #IBAction methods for - and +, change the height of lineView
I found the answer here, How to draw a line between two points over an image in swift 3?
class LineView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.init(white: 0.0, alpha: 0.0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
context.setStrokeColor(UIColor.blue.cgColor)
context.setLineWidth(3)
context.beginPath()
context.move(to: CGPoint(x: 5.0, y: 5.0)) // This would be oldX, oldY
context.addLine(to: CGPoint(x: 50.0, y: 50.0)) // This would be newX, newY
context.strokePath()
}
}
}
let imageView = UIImageView(image: #imageLiteral(resourceName: "image.png")) // This would be your mapView, here I am just using a random image
let lineView = LineView(frame: imageView.frame)
imageView.addSubview(lineView)