How to give an NSOutlineView a tiled background image that scrolls? - swift

I have a standard NSOutlineView. I would like it to have a background image, which tiles vertically, and which scrolls together with the outline view cells.
I've somewhat achieved this using the following in my ViewController:
class ViewController: NSViewController {
#IBOutlet weak var outlineView: NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if let image = NSImage(named: "tile") {
let color = NSColor.init(patternImage: image)
outlineView.backgroundColor = color
}
}
}
That works, except when you scroll past the top or bottom of the view (with the stretch provided by the containing scroll view).
I've tried putting the background image on the scroll view, but then it is static and doesn't scroll with the outline view's content.
I've also tried subclassing various objects in the view hierarchy and overriding their draw(_ dirtyRect: NSRect) method and doing:
self.wantsLayer = true
self.layer?.backgroundColor = ...etc
but got no success from that either.
Can anyone provide any suggestions?

I ended up creating a new custom NSView:
class MyView: NSView {
override func draw(_ dirtyRect: NSRect) {
if let image = NSImage(named: "Tile") {
let color = NSColor.init(patternImage: image)
color.setFill()
dirtyRect.fill()
}
super.draw(dirtyRect)
}
}
Then in my ViewController class I added an instance of the custom view, and used autolayout constraints to pin the new view to my outlineView's clip view starting 2000points above it, and ending 2000 below. This means no matter how far you over-scroll into the stretch area, you still see the tiled background.
class MyViewController: NSViewController {
#IBOutlet weak var outlineView: NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
guard let clipView = self.outlineView.superview else { return }
let newView = MyView(frame: .zero) // Frame is set by autolayout below.
newView.translatesAutoresizingMaskIntoConstraints = false
clipView.addSubview(newView, positioned: .below, relativeTo: self.outlineView)
// Add autolayout constraints to pin the new view to the clipView.
// See https://apple.co/3c6EMcH
newView.leadingAnchor.constraint(equalTo: clipView.leadingAnchor).isActive = true
newView.widthAnchor.constraint(equalTo: clipView.widthAnchor).isActive = true
newView.topAnchor.constraint(equalTo: clipView.topAnchor, constant: -2000).isActive = true
newView.bottomAnchor.constraint(equalTo: clipView.bottomAnchor, constant: 2000).isActive = true
}
}
I've removed other code from the above so hopefully I've left everything needed to illustrate the solution.

Related

Strange cornerRadius behaviour on UIView from xib

I created a custom Numpad keyboard through xib and wanted to initialize it with a rounded corners.
Here is the code I use:
import UIKit
class NumpadView: UIView {
#IBOutlet weak var resetButton: NumpadButton!
#IBOutlet weak var decimalButton: NumpadButton!
var target: UITextInput?
var view: UIView?
init(target: UITextInput, view: UIView) {
super.init(frame: .zero)
self.target = target
self.view = view
initializeSubview()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initializeSubview()
}
func initializeSubview() {
let xibFileName = "NumpadView"
let view = Bundle.main.loadNibNamed(xibFileName, owner: self, options: nil)![0] as! UIView
self.layer.cornerRadius = 30
self.layer.masksToBounds = true
self.addSubview(view)
view.frame = self.bounds
self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
But then I receive the strange view look in area where cornerRadius is implemented:
How to remove that grey background which is visible near the rounded corners?
UPDATE:
According to View Debugger it seems like this grey layer between yellow square and Visual Effect View is a UICompatibilityInputViewController:
How I presenting the Numpad:
1.I created a NumpadView as a UIView subclass in a xib:
2.In my VC I just change a standard textField.inputView property on my custom NumpadView:
import UIKit
class NumpadViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
setupKeyboardHide()
textField.delegate = self
textField.inputView = NumpadView(target: textField, view: view)
}
}
Test project on Github
Another option to get your "rounded corners"...
Get rid of the Visual Effect View in your Numpad class, and set a layer mask on the superview at run-time.
In class NumpadView: UIView, UIInputViewAudioFeedback:
override func layoutSubviews() {
super.layoutSubviews()
guard let sv = superview else { return }
let maskLayer = CAShapeLayer()
let bez = UIBezierPath(roundedRect: bounds, cornerRadius: 16)
maskLayer.path = bez.cgPath
sv.layer.mask = maskLayer
}
Looks like this:
Sure, the NumpadView object has rounded corner. However, you are using it like this:
textField.inputView = NumpadView(target: textField, view: view)
So, that part which is not rounded is the textField's inputView. I'm not quite sure if you can modify its layer to have corner radius. But, if you really want to get that rounded corner effect, an easier approach is just to add the NumpadView directly to the parent view and anchored to the bottom. Then show it via the begin editing delegate of the textfield (and hide via end editing).

Draw 2 Images in NSView

I'm trying to create a custom NSView so I can apply that view to my NSStatusBarButton. I want to have 2 small images next to each other inside the view but I can't figure out how I create a custom view that does that. I've tried to simply add one picture to the view but it doesn't show up in the status bar button:
import Cocoa
class statusBarView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let myimage = NSImage(named:NSImage.Name("StatusBarButtonImage"))
myimage!.draw(in: dirtyRect)
}
}
[...]
if let button2 = statusItem2.button {
button2.addSubview(statusBarView())
}
Can someone help me out here?
There are two things I would change.
Set the frame for your statusBarView.
if let button2 = statusItem2.button {
let statusBarView = statusBarView()
statusBarView.frame = button2.frame
button2.addSubview(statusBarView)
}
Draw in the frame of the view not in the dirtyRect
myimage!.draw(in: self.frame)
Hope this helps

How to properly size content on ScrollView for all devices?

I am using a scrollview along with Nibs. The goal is to layout 3 nibs with image inside them and scroll through them. I am also using storyboards to layout the scrollview.
The issue is venting looks good besides on the iPhone Plus sizes. The nibs are not centered right and bleeds on the screen? how do I fix this? Please check my code below.
#IBOutlet private weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
imageArray = [imageOne, imageTwo, imageThree]
scrollView.contentSize = CGSize(width: self.view.bounds.width * CGFloat(imageArray.count), height: scrollView.frame.size.height)
loadOnboardingDescriptions()
}
// MARK: Load onboarding dscription features
func loadOnboardingDescriptions() {
for (index, image) in imageArray.enumerated() {
if let onboardingDescriptionView = Bundle.main.loadNibNamed(Constants.NibName.onboardingDescriptionView.rawValue, owner: self, options: nil)?.first as? OnboardingDescriptionView {
onboardingDescriptionView.onboardingImageView.image = UIImage(named: image["image"]!)
onboardingDescriptionView.frame.size.width = self.view.bounds.size.width
onboardingDescriptionView.frame.origin.x = CGFloat(index) * self.view.bounds.size.width
print(onboardingDescriptionView.frame.origin.x)
self.scrollView.addSubview(onboardingDescriptionView)
}
}
}
EDIT:
As suggested by Matt bellow, I should have called this function inside:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
loadOnboardingDescriptions()
}

Why my background gradient view is not loading in my customer UINavigationController class

I'm trying to set a gradient to the background of my subclassed NavigationController. When I add a colour to the same code it works well but I can't seem to let my gradient show up. I created a subclass of a UIView that returns a CAGradientLayer as its background view.
Here is my subclassed UIView : (Note the colours are weird so I am sure its loading the right Gradient.
#IBDesignable
class GenericBackgrounView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
///The roundness for the corner
#IBInspectable var cornerRadius: CGFloat = 0.0 {
didSet{
setupGradient()
}
}
func setupGradient() {
//let gradientColors = [bgDarkColor.cgColor, bgDarkColor.blended(withFraction: 0.5, of: bgLightColor).cgColor, bgLightColor.cgColor]
let gradientColors = [UIColor.brown.cgColor, UIColor.red.blended(withFraction: 0.5, of: UIColor.cyan).cgColor, UIColor.yellow.cgColor]
gradientLayer.colors = gradientColors
gradientLayer.locations = ESDefault.backgroundGradientColorLocations
setNeedsDisplay()
}
var gradientLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override func awakeFromNib() {
super.awakeFromNib()
setupGradient()
}
override func prepareForInterfaceBuilder() {
setupGradient()
}
}
And Here is my UINavigationController :
class GenericNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
let backView = GenericBackgrounView(frame: self.view.frame)
backView.bounds = self.view.bounds
self.view.backgroundColor = UIColor.clear
self.view.addSubview(backView)
self.view.sendSubview(toBack: backView)
// Do any additional setup after loading the view.
}
}
Also note that my GenericBackgroundView works fine when I use it for any views I add in the interface builder.
I have been at this to long. I think I will suggest to Apple to setup some kind of Theming API in both code and Interface Builder... and the ability to add gradients straight into Interface Builder...
Thanks for you help.
Instead of setting it up in awakeFromNib() , try calling it in viewDidLayoutSubviews(). Reason is that in viewDidLayoutSubviews() will have the correct frame of the view , while in awakeFromNib() you wouldn't know the right frame of the view.From Apple Documentation.
Alright, I've tinkered a bit and found some working code. I would still love to understand the reason why this works and not the way I had it before. I hate feeling it works by magic...
here is the working code : (Remember that my gradient is in form of CAGradientLayer and I have made some static variable that has defaults.
import UIKit
class GenericNavigationController: UINavigationController {
let backViewGradient = Default.testGradientCALayer
override func viewDidLoad() {
super.viewDidLoad()
setupBackground()
}
func setupBackground() {
backViewGradient.frame = self.view.frame
self.view.layer.insertSublayer(backViewGradient, at: 0)
}
override func prepareForInterfaceBuilder() {
setupBackground()
}
}
What I'm wondering is how come since all the UIControls that are subclassed from UIView don't all work the same. They should all have a view that is the background and we should all be able to either add a layer or a subview to them and be able to get my previous code to work or my latest code too which does not work with TableViewCells.
I will leave this question open because I would love to know the truth behind this. I don't think I can fully grasp Swift or Xcode if it behaves somewhat magically and inconsistent.

Is it possible to make a circular NSButton?

I am trying to create a custom shape NSButton. In particular I am trying to make a round button, using a custom image. I've found a tutorial on the creation of custom UIButton and tried to adapt it to NSButton. But there's a huge problem. clipsToBounds seems to be iOS only(
Here's my code:
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var mainButton: NSButton!
var size = 32
override func viewDidLoad() {
super.viewDidLoad()
configureButton()
// Do any additional setup after loading the view.
}
func configureButton()
{
mainButton.wantsLayer = true
mainButton.layerContentsRedrawPolicy = NSViewLayerContentsRedrawPolicy.OnSetNeedsDisplay
mainButton.layer?.cornerRadius = 0.5 * mainButton.bounds.size.width
mainButton.layer?.borderColor = NSColor(red:0.0/255.0, green:122.0/255.0, blue:255.0/255.0, alpha:1).CGColor as CGColorRef
mainButton.layer?.borderWidth = 2.0
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
}
What am I doing wrong? How can I make a circular NSButton? Can you suggest anything on replacing clipsToBounds?
Because here is what I was able to get so far:
NSButton is a subclass of NSView, so all methods in NSView, such as drawRect(_:), are also available in NSButton.
So create a new Button.swift which you draw your custom layout
import Cocoa
class Button: NSButton {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
// Drawing code here.
let path = NSBezierPath(ovalIn: dirtyRect)
NSColor.green.setFill()
path.fill()
}
}
Great Tutorial Here. It is iOS , but quite similar!
Don't play around with corner radius. A circle doesn't have corners. To make the button appear as a circle, mask the button's layer to a circle.
You are setting the radius based upon the width of the button. By the look of your screenshot, the button is not a square when you start, so rounding the corners cannot create a circle.