Programmatically Setting AutoresizingMask to NSView Object - swift

I wonder why it always fails whenever I try to set the autoresizingMask to an NSView object programmatically? In the following lines of code, I have a tiny, blue NSVIew object (topLeftView) set at the top-left corner of its parent container.
class ViewController: NSViewController {
// MARK: - IBOutlet
#IBOutlet weak var panView: PanView!
override func viewDidLoad() {
super.viewDidLoad()
makeImageContainer(point: CGPoint.zero)
}
func makeViewContainer(point: CGPoint) {
let myView = NSView(frame: CGRect(origin: CGPoint.zero, size: CGSize(width: 150.0, height: 150.0))
myView.wantsLayer = true
if let myLayer = myView.layer {
myLayer.backgroundColor = NSColor.orange.cgColor
}
panView.addSubview(myView) // panView is an IBOutlet-connected `NSView` object.
/* tiny, blue `NSView` object */
let topLeftView = NSView(frame: CGRect(origin: CGPoint(x: 0.0, y: 150.0 - 6.0), size: CGSize(width: 6.0, height: 6.0)))
topLeftView.wantsLayer = true
topLeftView.autoresizingMask = [.minXMargin, .maxYMargin]
if let layer = topLeftView.layer {
layer.backgroundColor = NSColor.blue.cgColor
}
myView.addSubview(topLeftView)
}
}
So it has minXMargin and maxYMargin autoresizingMasks.
Now, I want to click on a push button to resize this orange NSView container object to see if the tiny, blue NSView object will stick at the top-left corner.
#IBAction func testClicked(_ sender: NSButton) {
for i in 0..<panView.subviews.count {
let subView = panView.subviews[i]
if i == 4 {
subView.setFrameSize(CGSize(width: 200.0, height: 200.0))
}
}
}
And the tiny guy has moved by 50 points to the right and 50 points to the bottom. What am I doing wrong? Thanks.

Related

How do I draw a rectangle with equal line widths into an NSStatusItemButton?

The following code draws a rectangle with NSBezierPath into an NSImage, which is then set as the image for an NSStatusItemButton.
import Cocoa
class ViewController: NSViewController {
let statusItem: NSStatusItem = NSStatusBar.system.statusItem(
withLength: NSStatusItem.squareLength)
override func viewDidLoad() {
super.viewDidLoad()
let imageSize = NSSize.init(width: 18.0, height: 18.0)
let statusItemImage = NSImage(
size: imageSize,
flipped: false,
drawingHandler: { (dstRect: NSRect) -> Bool in
NSColor.black.setStroke()
let path = NSBezierPath()
path.appendRect(NSRect(
x: NSMinX(dstRect),
y: NSMinY(dstRect),
width: 10,
height: 10))
path.stroke()
return true
})
statusItem.button?.image = statusItemImage
}
override var representedObject: Any? {
didSet {
}
}
}
In the menu bar, it looks like this:
The left and bottom edge of the rectangle have a different width than the right and top edge.
How do I get a rectangle with equal line widths?
You have to set a lineWidth for your path:
/// The new rectangle
let new = CGRect(origin: dstRect.origin,
size: .init(width: 10, height: 10))
let path = NSBezierPath(rect: new)
// The line width size
path.lineWidth = 0.1
path.stroke()

Drag a CGRect using UIPanGestureRecognizer

I am subclassing UIView, and am trying to "drag" a CGRect back and forth across the screen. Basically I want to move(redraw) the rectangle every time I drag my finger. So far, I have this code:
var rectangle: CGRect {
get {
return CGRect(x: 200,
y: 200,
width: frame.width / 6,
height: 15)
}
set {}
}
override func draw(_ rect: CGRect) {
let gesture = UIPanGestureRecognizer(target: self, action: #selector(dragRectangle(recognizer:)))
addGestureRecognizer(gesture)
drawRectangle(rect)
}
func drawRectangle(_ rect: CGRect) {
let path = UIBezierPath(rect: rectangle)
UIColor.black.set()
path.fill()
}
#objc func dragRectangle(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self)
rectangle = CGRect(x: rectangle.midX + translation.x, y: rectangle.midY + translation.y, width: rectangle.width, height: rectangle.height)
setNeedsDisplay()
recognizer.setTranslation(CGPoint.zero, in: self)
}
This is my first time using UIPanGestureRecognizer, so I'm not sure of all the details that go into this. I have set breakpoints in drawRectangle and confirmed that this is being called. However, the rectangle on the screen does not move at all, no matter how many times I try to drag it. What's wrong?
This is how you can do it easily. just copy paste and run this.
//
// RootController.swift
// SampleApp
//
// Created by Chanaka Caldera on 24/6/19.
// Copyright © 2019 homeapps. All rights reserved.
//
import UIKit
class RootController: UIViewController {
private var draggableView: UIView!
private var pangesture: UIPanGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
setdragview()
}
}
extension RootController {
fileprivate func setdragview() {
draggableView = UIView()
draggableView.translatesAutoresizingMaskIntoConstraints = false
draggableView.backgroundColor = .lightGray
view.addSubview(draggableView)
let draggableviewConstraints = [draggableView.leftAnchor.constraint(equalTo: view.leftAnchor,constant: 10),
draggableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 64),
draggableView.widthAnchor.constraint(equalToConstant: 100),
draggableView.heightAnchor.constraint(equalToConstant: 40)]
NSLayoutConstraint.activate(draggableviewConstraints)
pangesture = UIPanGestureRecognizer()
draggableView.isUserInteractionEnabled = true
draggableView.addGestureRecognizer(pangesture)
pangesture.addTarget(self, action: #selector(draggableFunction(_:)))
}
}
extension RootController {
#objc fileprivate func draggableFunction(_ sender: UIPanGestureRecognizer) {
view.bringSubviewToFront(draggableView)
let translation = sender.translation(in: self.view)
draggableView.center = CGPoint(x: draggableView.center.x + translation.x, y: draggableView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
print("drag works : \(translation.x)")
}
}
here is the demo,
Hope this will help. cheers !
Try like this (check comments through code):
#IBDesignable
class Rectangle: UIView {
#IBInspectable var color: UIColor = .clear {
didSet { backgroundColor = color }
}
// draw your view using the background color
override func draw(_ rect: CGRect) {
backgroundColor?.set()
UIBezierPath(rect: rect).fill()
}
// add the gesture recognizer to your view
override func didMoveToSuperview() {
addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(pan)))
}
// your gesture selector
#objc func pan(_ gesture: UIPanGestureRecognizer) {
// update your view frame origin
frame.origin += gesture.translation(in: self)
// reset the gesture translation
gesture.setTranslation(.zero, in: self)
}
}
extension CGPoint {
static func +=(lhs: inout CGPoint, rhs: CGPoint) {
lhs.x += rhs.x
lhs.y += rhs.y
}
}
To draw rectangles on your view when panning you can do as follow:
import UIKit
class ViewController: UIViewController {
var rectangles: [Rectangle] = []
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(pan)))
}
#objc func pan(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
let rectangle = Rectangle(frame: .init(origin: gesture.location(in: view), size: .init(width: 0, height: 0)))
rectangle.fillColor = .red
rectangle.strokeColor = .white
rectangle.lineWidth = 3
view.addSubview(rectangle)
rectangles.append(rectangle)
case .changed:
let distance = gesture.translation(in: view)
let index = rectangles.index(before: rectangles.endIndex)
let frame = rectangles[index].frame
rectangles[index].frame = .init(origin: frame.origin, size: .init(width: frame.width + distance.x, height: frame.height + distance.y))
rectangles[index].setNeedsDisplay()
gesture.setTranslation(.zero, in: view)
case .ended:
break
default:
break
}
}
}
Sample Project
I figured out most of the problem as to why the rectangle wasn't moving. It turns out that I misunderstood how variable getters and setters work in Swift. Nevertheless, I changed this code:
var rectangle: CGRect {
get {
return CGRect(x: 200,
y: 200,
width: frame.width / 6,
height: 15)
}
set {}
}
to be a lazy variable:
lazy var rectangle: CGRect = {
return CGRect(x: 200,
y: 200,
width: self.frame.width / 6,
height: 15)
}()
The reason I needed get and set in the first place was because I use frame in my variable calculations, and that was not available until the view itself was fully instantiated. I also tweaked this code a bit:
rectangle = CGRect(x: rectangle.midX + translation.x, y: rectangle.midY + translation.y, width: rectangle.width, height: rectangle.height)
and used minX instead of midX:
rectangle = CGRect(x: rectangle.minX + translation.x, y: rectangle.minY + translation.y, width: rectangle.width, height: rectangle.height)
This is because CGRect is initialized with the x and y parameters being the minX and minY.
This is good progress(at least the rectangle moves). However, I am not able to figure out why the rectangle only switches places after I have released my mouse, resulting in choppy movement.

UIProgressView setProgress issue

I've encountered an issue using a UIProgressView where low values (1% - about 10%) look off. You can see with the example above that 97% looks accurate while 2% does not.
Here's the code for setting colors:
self.progressView.trackTintColor = UIColor.green.withAlphaComponent(0.3)
self.progressView.tintColor = UIColor.green.withAlphaComponent(1.0)
But, if I comment out the trackTintColor or the tintColor, then the 2% looks correct. Why when using these together does it cause this issue? Just an Xcode bug? Has anyone resolved this before?
I've experienced the same issue in my project. For me it's fixed by using progressTintColor instead of tintColor.
progressView.progressTintColor = UIColor.green.withAlphaComponent(1.0)
progressView.trackTintColor = UIColor.green.withAlphaComponent(0.3)
you need to create color image
SWIFT 3 Example:
class ViewController: UIViewController {
#IBOutlet weak var progressView: UIProgressView!
#IBAction func lessButton(_ sender: UIButton) {
let percentage = 20
let invertedValue = Float(100 - percentage) / 100
progressView.setProgress(invertedValue, animated: true)
}
#IBAction func moreButton(_ sender: UIButton) {
let percentage = 80
let invertedValue = Float(100 - percentage) / 100
progressView.setProgress(invertedValue, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
//create gradient view the size of the progress view
let gradientView = GradientView(frame: progressView.bounds)
//convert gradient view to image , flip horizontally and assign as the track image
progressView.trackImage = UIImage(view: gradientView).withHorizontallyFlippedOrientation()
//invert the progress view
progressView.transform = CGAffineTransform(scaleX: -1.0, y: -1.0)
progressView.progressTintColor = UIColor.black
progressView.progress = 1
}
}
extension UIImage{
convenience init(view: UIView) {
UIGraphicsBeginImageContext(view.frame.size)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: (image?.cgImage)!)
}
}
#IBDesignable
class GradientView: UIView {
private var gradientLayer = CAGradientLayer()
private var vertical: Bool = false
override func draw(_ rect: CGRect) {
super.draw(rect)
// Drawing code
//fill view with gradient layer
gradientLayer.frame = self.bounds
//style and insert layer if not already inserted
if gradientLayer.superlayer == nil {
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = vertical ? CGPoint(x: 0, y: 1) : CGPoint(x: 1, y: 0)
gradientLayer.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
gradientLayer.locations = [0.0, 1.0]
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
}

Animate meteor traveling from top to bottom in Swift 3

I want an image of a red circle to move from the top of the screen to the bottom, as though it were a meteor traveling across the screen. It does move, but it moves in a millisecond with no animation. How do I make it move smoothly like a meteor with a tail?
class ViewController: UIViewController {
let meteorImage = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.meteorImage.backgroundColor = .clear
self.meteorImage.frame = CGRect(x: 50, y: 0, width: 50, height: 50)
self.meteorImage.isHidden = false
self.meteorImage.image = #imageLiteral(resourceName: "success")
view.addSubview(self.meteorImage)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func buttonPressed(_ sender: UIButton) {
let totalHeight = Int(view.frame.height)
var currentHeight = 0
while currentHeight < totalHeight {
self.meteorImage.backgroundColor = .clear
self.meteorImage.frame = CGRect(x: 50, y: currentHeight, width: 50, height: 50)
self.meteorImage.isHidden = false
self.meteorImage.image = #imageLiteral(resourceName: "success")
view.addSubview(self.meteorImage)
currentHeight += 1
}
}
}
Animations have to be done with an animate call.
This is a simple way to do an animation.
UIView.animate(withDuration: 0.1, animations: {
// Apply Changes to frame.
})
Consider also: instead of doing the while loop simply set the final location of the meteor's frame and set the duration to the amount of time you want it to take.
let newFrame= CGRect(x: 50,
y: currentHeight,
width: 50,
height: 50)
UIView.animate(withDuration: 3.0, animations: {
// Apply Changes to frame.
self.meteorImage.frame = newFrame
})
If you want to make your animation more complicated you can look into keyframe animations on a UIView.

How to set a border only for one of the edges of UIView

In swift, is there a way to only set a border for top side of a UIView ?
There are many ways, but drawing the border yourself might offer a little more control. I would recommend subclassing a UIView and use a CAShapeLayer to accomplish this.
Something to the effect of (using Swift 3):
import UIKit
class TopBorderedView: UIView {
//decalare a private topBorder
fileprivate weak var topBorder: CAShapeLayer?
//declare a border thickness to allow outside access to setting it
var topThickness: CGFloat = 1.0 {
didSet {
drawTopBorder()
}
}
//declare public color to allow outside access
var topColor: UIColor = UIColor.lightGray {
didSet {
drawTopBorder()
}
}
//implment the draw method
fileprivate func drawTopBorder() {
let start = CGPoint(x: 0, y: 0)
let end = CGPoint(x: bounds.width, y: 0)
removeIfNeeded(topBorder)
topBorder = addBorder(from: start, to: end, color: topColor, thickness: topThickness)
}
//implement a private border drawing method that could be used for border on other sides if desired, etc..
fileprivate func addBorder(from: CGPoint, to: CGPoint, color: UIColor, thickness: CGFloat) -> CAShapeLayer {
let border = CAShapeLayer()
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
border.path = path.cgPath
border.strokeColor = color.cgColor
border.lineWidth = thickness
border.fillColor = nil
layer.addSublayer(border)
return border
}
//used to remove the border and make room for a redraw to be autolayout friendly
fileprivate func removeIfNeeded(_ border: CAShapeLayer?) {
if let bdr = border {
bdr.removeFromSuperlayer()
}
}
//override layoutSubviews() (probably debatable) and call the drawTopBorder method to draw and redraw if needed
override func layoutSubviews() {
super.layoutSubviews()
drawTopBorder()
}
}
For maximum reusability in the context of storyboards - I would also take a look at using #IBDesignable and #IBInspectable for common UI patterns like this. For a decent intro, checkout NSHipster: IBDesignable and IBInspectable