iOS8: Adding Gradient To UIButton Using IB - ios8

I'm using Auto-Layout. I have three UIButton's that take up the entire screen. Each UIButton has a different background color and I would like to add Gradient to each.
Following are the steps that I took:
Subclassed UIButton called CustomButton
Assigned each of these buttons CustomButton in Identity Inspector.
In CustomButton class, I declared two #IBInspectable properties.
I pass in these colors through the IB.
Below is how I configuered the gradient in CustomButton:
import UIKit
#IBDesignable class CustomButton: UIButton {
#IBInspectable var topColor: UIColor!
#IBInspectable var bottomColor: UIColor!
override func drawRect(rect: CGRect) {
let gradientColors: [CGColor] = [topColor.CGColor, bottomColor.CGColor]
let gradientLocations: [Float] = [0.0, 1.0]
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = gradientColors
gradientLayer.locations = gradientLocations
self.layer.addSublayer(gradientLayer)
}
}
However, the gradient is still not showing. What am I doing wrong?
Update
This isn't working either:
import UIKit
#IBDesignable class CustomButton: UIButton {
#IBInspectable var topColor: UIColor!
#IBInspectable var bottomColor: UIColor!
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
let gradientColors: [CGColor] = [topColor.CGColor, bottomColor.CGColor]
let gradientLocations: [Float] = [0.0, 1.0]
let gradientLayer: CAGradientLayer = CAGradientLayer()
gradientLayer.colors = gradientColors
gradientLayer.locations = gradientLocations
gradientLayer.frame = self.bounds
self.layer.addSublayer(gradientLayer)
}
}

First off, don’t create layers in -drawRect—it’s meant for drawing into the current Core Graphics context, and can get called multiple times, which will result in creating a new gradient layer each time. Instead, override -initWithCoder: and create the layer there.
The reason your layer isn’t showing up at all is because its frame isn’t set and is defaulting to CGRectZero, i.e. a 0✕0 rectangle. When you create the layer, you should be setting its frame to the view’s bounds. If the view’s going to change size, you should also override -layoutSubviews and update the layer’s frame there.

Related

Why doesn't Metal render my simple clear window code?

I have been following this tutorial. I downloaded the source and tried "translating" it to Swift. This is my "translated" code:
import Cocoa
import AppKit
import MetalKit
import simd
class MetalViewController: NSViewController {
#IBOutlet var inview: MTKView!
override func viewDidLoad() {
super.viewDidLoad()
let _view: MTKView = self.inview
_view.device = MTLCreateSystemDefaultDevice()
let _renderer: Renderer=initView(view: _view)
_view.delegate=_renderer as? MTKViewDelegate
_view.preferredFramesPerSecond=60
}
}
class Renderer: NSObject {
init(device: MTLDevice){
self._device=device
self._commandQueue=_device.makeCommandQueue()!
super.init()
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
let color = Color(red: 1.0,green: 0.0,blue: 0.0,alpha: 0.0)
view.clearColor = MTLClearColorMake(color.red, color.green, color.blue, color.alpha)
let commandbuffer = _commandQueue.makeCommandBuffer()
let renderpassdescriptor: MTLRenderPassDescriptor = view.currentRenderPassDescriptor!
let renderencoder: MTLRenderCommandEncoder = (commandbuffer?.makeRenderCommandEncoder(descriptor: renderpassdescriptor))!
renderencoder.endEncoding()
commandbuffer!.present(view.currentDrawable!)
commandbuffer!.commit()
}
var _device: MTLDevice
var _commandQueue: MTLCommandQueue
}
struct Color{
var red, green, blue, alpha: Double
}
func initView(view: MTKView) -> Renderer{
var renderer: Renderer
renderer=Renderer(device: view.device!)
return renderer
}
So I put the AAPLRenderer and AAPLViewControllers into one file, and made it so that there are no header files. I linked the view with #IBOutlet to the view controller because the view was a NSView and I cannot cast it to MTKView without getting a compile time error. The AppDelegate is the original one and I do not have a main file.
I end up with a window that does not show red, but rather shows nothing. I do not understand why this is happening. Please help me, thank you.
I see two issues.
1) MTKView's delegate property is a weak var, which means that if you don't hold onto an instance of your renderer, it'll be immediately deinited and never receive any delegate callbacks. Keep a reference to your renderer as a property on your view controller.
class MetalViewController: NSViewController {
#IBOutlet var inview: MTKView!
var renderer: Renderer!
override func viewDidLoad() {
// ...
let view: MTKView = self.inview
// ...
renderer = initView(view: view)
view.delegate = renderer
// ...
}
}
2) Because the Renderer class doesn't explicitly declare conformance to the MTKViewDelegate protocol, the conditional cast when assigning it as the view's delegate fails. Make Renderer explicitly conform to the protocol, and remove the conditional cast as shown above.
class Renderer: NSObject, MTKViewDelegate
Well, it could be anything. But, the first thing I would check is that your alpha setting for that red color should have alpha = 1.0 and not alpha = 0.0.

How to efficiently draw a Core Image with a Filter into an NSView?

I am applying a perspective Core Image filter to transform and draw a CIImage into a custom NSView and it seems slower than I expected (e.g, I drag a slider that alters the perspective transformation and the drawing lags behind the slider value). Here is my custom drawRect method where self.mySourceImage is a CIImage:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
if (self.perspectiveFilter == nil)
self.perspectiveFilter = [CIFilter filterWithName:#"CIPerspectiveTransform"];
[self.perspectiveFilter setValue:self.mySourceImage
forKey:#"inputImage"];
[self.perspectiveFilter setValue: [CIVector vectorWithX:0 Y:0]
forKey:#"inputBottomLeft"];
// ... set other vector parameters based on slider value
CIImage *outputImage = [self.perspectiveFilter outputImage];
[outputImage drawInRect:dstrect
fromRect:srcRect
operation:NSCompositingOperationSourceOver
fraction:0.8];
}
Here is an example output:
My experience with image filters tells me that this should be much faster. Is there some "best practice" that I am missing to speed this up?
Note that I only create the filter once (stored as a property).
I did make sure the view has a CALayer for a backing store. Should I be adding the filter to a CALayer somehow?
Note that I never create a CIContext -- I assume there is an implicit context used by NSView? Should I create a CIContext and render to an image and draw the image?
Here's how I use a GLKView in UIKit:
I prefer subclassing GLKView to allow for a few things:
initializing from code
overriding draw(rect:) for the UIImageView equivalence of contentMode (aspect fit in particular)
when using scaleAspectFit, creating a "clear color" for the background color to match the surrounding superviews
That said, here's what I have:
import GLKit
class ImageView: GLKView {
var renderContext: CIContext
var rgb:(Int?,Int?,Int?)!
var myClearColor:UIColor!
var clearColor: UIColor! {
didSet {
myClearColor = clearColor
}
}
var image: CIImage! {
didSet {
setNeedsDisplay()
}
}
var uiImage:UIImage? {
get {
let final = renderContext.createCGImage(self.image, from: self.image.extent)
return UIImage(cgImage: final!)
}
}
init() {
let eaglContext = EAGLContext(api: .openGLES2)
renderContext = CIContext(eaglContext: eaglContext!)
super.init(frame: CGRect.zero)
context = eaglContext!
self.translatesAutoresizingMaskIntoConstraints = false
}
override init(frame: CGRect, context: EAGLContext) {
renderContext = CIContext(eaglContext: context)
super.init(frame: frame, context: context)
enableSetNeedsDisplay = true
self.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
let eaglContext = EAGLContext(api: .openGLES2)
renderContext = CIContext(eaglContext: eaglContext!)
super.init(coder: aDecoder)
context = eaglContext!
self.translatesAutoresizingMaskIntoConstraints = false
}
override func draw(_ rect: CGRect) {
if let image = image {
let imageSize = image.extent.size
var drawFrame = CGRect(x: 0, y: 0, width: CGFloat(drawableWidth), height: CGFloat(drawableHeight))
let imageAR = imageSize.width / imageSize.height
let viewAR = drawFrame.width / drawFrame.height
if imageAR > viewAR {
drawFrame.origin.y += (drawFrame.height - drawFrame.width / imageAR) / 2.0
drawFrame.size.height = drawFrame.width / imageAR
} else {
drawFrame.origin.x += (drawFrame.width - drawFrame.height * imageAR) / 2.0
drawFrame.size.width = drawFrame.height * imageAR
}
rgb = myClearColor.rgb()
glClearColor(Float(rgb.0!)/256.0, Float(rgb.1!)/256.0, Float(rgb.2!)/256.0, 0.0);
glClear(0x00004000)
// set the blend mode to "source over" so that CI will use that
glEnable(0x0BE2);
glBlendFunc(1, 0x0303);
renderContext.draw(image, in: drawFrame, from: image.extent)
}
}
}
A few notes:
The vast majority of this was taken from something written a few years back (in Swift 2 I think) from objc.io with the associated GitHub project. In particular, check out their GLKView subclass that has code for scaleAspectFill and other content modes.
Note the usage of a single CIContext called renderContext. I use it to create a UIImage when needed (in iOS you "share" a UIImage).
I use a didSet with the image property to automatically call setNeedsDisplay when the image changes. (I also call this explicitly when an iOS device changes orientation.) I do not know the macOS equivalent of this call.
I hope this gives you a good start for using OpenGL in macOS. If it's anything like UIKit, trying to put a CIImage in an NSView doesn't involve the GPU, which is a bad thing.

How can I animate any UIView inside UITableViewCell?

let label = UILabel(frame: CGRectMake(20.0, 20.0, 15.0, 15.0))
label.backgroundColor = UIColor.flamingoColor()
UIView.animateWithDuration(3.0, animations: {
cell.addSubview(label)
label.backgroundColor = UIColor.yellowColor()
})
This code I put inside tableView:cellForRowAtIndexPath: -> result is a subview that has a yellow background, but I haven't seen any animation. The same is for method tableView:didEndDisplayingCell:
Which method should I use or what kind of animation should I put there to see any animations inside UITableViewCell. I know that I cannot animate existing labels, because these are with constraints.
Firstly, you should move cell.addSubview(label) to outside of the animation block and add the label to the contentView of the cell.
Back to your problem. You can't animate the background colour of a UILabel. You could use a UIView, of the same size, behind the UILabel for the same effect though.
To get the UIView to change colour once it's appeared I used a UITableViewCell subclass:
class AnimatedCell : UITableViewCell {
let animatedView = UIView()
/* Setup by adding animatedView to the contentView and setting the
animated view's initial frame and colour.
*/
func animate() {
UIView.animateWithDuration(3.0) {
animatedView.backgroundColor = UIColor.redColor()
}
}
Then in your UITableViewController subclass, when the view controller is presented the UITableView will be populated so you need to animate those cells with:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated: Bool)
for cell in tableView.visibleCells() as! [UITableViewCell] {
if let animatedCell = cell as? AnimatedCell {
animatedCell.animate()
}
}
}
Then, for cells that appear when you scroll:
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if let animatedCell = cell as? AnimatedCell {
a.animateLabel()
}
}
Also, you can animate with AutoLayout, you need to change the constant on the NSLayoutConstraint you want to animate. This gives a good example: iOS: How does one animate to new autolayout constraint (height)

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.

NSImageView image aspect fill?

So I am used to UIImageView, and being able to set different ways of how its image is displayed in it. Like for example AspectFill mode etc...
I would like to accomplish the same thing using NSImageView on a mac app. Does NSImageView work similarly to UIImageView in that regard or how would I go about showing an image in an NSImageView and picking different ways of displaying that image?
You may find it much easier to subclass NSView and provide a CALayer that does the aspect fill for you. Here is what the init might look like for this NSView subclass.
- (id)initWithFrame:(NSRect)frame andImage:(NSImage*)image
{
self = [super initWithFrame:frame];
if (self) {
self.layer = [[CALayer alloc] init];
self.layer.contentsGravity = kCAGravityResizeAspectFill;
self.layer.contents = image;
self.wantsLayer = YES;
}
return self;
}
Note that the order of setting the layer, then settings wantsLayer is very important (if you set wantsLayer first, you'll get a default backing layer instead).
You could have a setImage method that simply updates the contents of the layer.
Here is what I'm using, written with Swift. This approach works well with storyboards - just use a normal NSImageView, then replace the name NSImageView in the Class box, with MyAspectFillImageNSImageView ...
open class MyAspectFillImageNSImageView : NSImageView {
open override var image: NSImage? {
set {
self.layer = CALayer()
self.layer?.contentsGravity = kCAGravityResizeAspectFill
self.layer?.contents = newValue
self.wantsLayer = true
super.image = newValue
}
get {
return super.image
}
}
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
}
//the image setter isn't called when loading from a storyboard
//manually set the image if it is already set
required public init?(coder: NSCoder) {
super.init(coder: coder)
if let theImage = image {
self.image = theImage
}
}
}
I had the same problem. I wanted to have the image to be scaled to fill but keeping the aspect ratio of the original image. Strangely, this is not as simple as it seems, and does not come out of the box with NSImageView. I wanted the NSImageView scale nicely while it resize with superview(s). I made a drop-in NSImageView subclass you can find on github: KPCScaleToFillNSImageView
You can use this: image will be force to fill the view size
( Aspect Fill )
imageView.imageScaling = .scaleAxesIndependently
( Aspect Fit )
imageView.imageScaling = .scaleProportionallyUpOrDown
( Center Top )
imageView.imageScaling = .scaleProportionallyDown
It works for me.
I was having an hard time trying to figure out how you can make an Aspect Fill Clip to Bounds :
Picture credit: https://osxentwicklerforum.de/index.php/Thread/28812-NSImageView-Scaling-Seitenverh%C3%A4ltnis/
Finally I made my own Subclass of NSImageView, hope this can help someone :
import Cocoa
#IBDesignable
class NSImageView_ScaleAspectFill: NSImageView {
#IBInspectable
var scaleAspectFill : Bool = false
override func awakeFromNib() {
// Scaling : .scaleNone mandatory
if scaleAspectFill { self.imageScaling = .scaleNone }
}
override func draw(_ dirtyRect: NSRect) {
if scaleAspectFill, let _ = self.image {
// Compute new Size
let imageViewRatio = self.image!.size.height / self.image!.size.width
let nestedImageRatio = self.bounds.size.height / self.bounds.size.width
var newWidth = self.image!.size.width
var newHeight = self.image!.size.height
if imageViewRatio > nestedImageRatio {
newWidth = self.bounds.size.width
newHeight = self.bounds.size.width * imageViewRatio
} else {
newWidth = self.bounds.size.height / imageViewRatio
newHeight = self.bounds.size.height
}
self.image!.size.width = newWidth
self.image!.size.height = newHeight
}
// Draw AFTER resizing
super.draw(dirtyRect)
}
}
Plus this is #IBDesignable so you can set it on in the StoryBoard
WARNINGS
I'm new to MacOS Swift development, I come from iOS development that's why I was surprised I couldn't find a clipToBound property, maybe it exists and I wasn't able to find it !
Regarding the code, I suspect this is consuming a lot, and also this has the side effect to modify the original image ratio over the time. This side effect seemed negligible to me.
Once again if their is a setting that allow a NSImageView to clip to bounds, please remove this answer :]
Image scalling can be updated with below function of NSImageView.
[imageView setImageScaling:NSScaleProportionally];
Here are more options to change image display property.
enum {
NSScaleProportionally = 0, // Deprecated. Use NSImageScaleProportionallyDown
NSScaleToFit, // Deprecated. Use NSImageScaleAxesIndependently
NSScaleNone // Deprecated. Use NSImageScaleNone
};
Here is another approach which uses SwiftUI under the hood
The major advantage here is that if your image has dark & light modes, then they are respected when the system appearance changes
(I couldn't get that to work with the other approaches)
This relies on an image existing in your assets with imageName
import Foundation
import AppKit
import SwiftUI
open class AspectFillImageView : NSView {
#IBInspectable
open var imageName: String?
{
didSet {
if imageName != oldValue {
insertSwiftUIImage(imageName)
}
}
}
open override func prepareForInterfaceBuilder() {
self.needsLayout = true
}
func insertSwiftUIImage(_ name:String?){
self.removeSubviews()
guard let name = name else {
return
}
let iv = Image(name).resizable().scaledToFill()
let hostView = NSHostingView(rootView:iv)
self.addSubview(hostView)
//I'm using PureLayout to pin the subview. You will have to rewrite this in your own way...
hostView.autoPinEdgesToSuperviewEdges()
}
func commonInit() {
insertSwiftUIImage(imageName)
}
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
commonInit()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
}
Answers already given here are very good, but most of them involve subclassing NSView or NSImageView.
You could also achieve the result just by using a CALayer. But in that case you wouldn't have auto layout capabilities.
The simplest solution is to have a NSView, without subclassing it, and setting manually it's layer property. It could also be a NSImageView and achieve the same result.
Example
Using Swift
let view = NSView()
view.layer = .init() // CALayer()
view.layer?.contentsGravity = .resizeAspectFill
view.layer?.contents = image // image is a NSImage, could also be a CGImage
view.wantsLayer = true

Resources