Swift Performance - performance

Is the Swift compiler smart enough to optimize away multiple repeated method calls that return the same object? Or, should I first assign the return value to a constant and use that instead?
For Example:
The Table View Programming Guide for iOS says that when adding subviews to a UITableViewCell's contentView, avoid making them transparent. "Transparent subviews affect scrolling performance because of the increased compositing cost."
So, I often do the following:
class EmployeeCell: UITableViewCell {
var nameLabel: UILabel
var titleLabel: UILabel
// ...
init(style: UITableViewCellStyle, reuseIdentifier: String) {
nameLabel = UILabel(frame: CGRectZero)
nameLabel.backgroundColor = UIColor.whiteColor()
titleLabel = UILabel(frame: CGRectZero)
titleLabel.backgroundColor = UIColor.whiteColor()
// ...
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
}
Would it be better, however, to first (inside init) do let whiteColor = UIColor.whiteColor() and then replace each call to UIColor.whiteColor() with whiteColor?
Or, perhaps in this example, I should define a function (inside init) that creates a UILabel with a frame of CGRectZero and a backgroundColor of UIColor.whiteColor() and just call that to initialize each label property. If I do that, will Swift know to optimize (perhaps inline) that function?

use swiftc -help There is a lot of options, use -emit-xxxx to see the resulting job in details

Related

Animating constraints with layer-backed NSView

I'm attempting to implement an animation that shows/hides a view in a horizontal arrangement. I'd like this to happen with slide, and with no opacity changes. I'm using auto-layout everywhere.
Critically, the total width of the containing view changes with the window. So, constant-based animations are not possible (or so I believe, but happy to be proved wrong).
|- viewA -|- viewB -|
My first attempt was to use NSStackView, and animate the isHidden property of an arranged subview. Despite seeming like it might do the trick, I was not able to pull off anything close to what I was after.
My second attempt was to apply two constraints, one to force viewB to be zero width, and a second to ensure the widths are equal. On animation I change the priorities of these constraints from defaultHigh <-> defaultLow.
This results in the correct layout in both cases, but the animation is not working out.
With wantsLayer = true on the containing view, no animation occurs whatsoever. The views just jump to their final states. Without wantsLayer, the views do animate. However, when collapsing, viewA does a nice slide, but viewB instantly disappears. As an experiment, I changed the zero width to a fixed 10.0, and with that, the animation works right in both directions. However, I want the view totally hidden.
So, a few questions:
Is it possible to animate layouts like this with layer-backed views?
Are there other techniques possible for achieving the same effect?
Any ideas on how to achieve these nicely with NSStackView?
class LayoutAnimationViewController: NSViewController {
let containerView: NSView
let view1: ColorView
let view2: ColorView
let widthEqualContraint: NSLayoutConstraint
let widthZeroConstraint: NSLayoutConstraint
init() {
self.containerView = NSView()
self.view1 = ColorView(color: NSColor.red)
self.view2 = ColorView(color: NSColor.blue)
self.widthEqualContraint = view2.widthAnchor.constraint(equalTo: view1.widthAnchor)
widthEqualContraint.priority = .defaultLow
self.widthZeroConstraint = view2.widthAnchor.constraint(equalToConstant: 0.0)
widthZeroConstraint.priority = .defaultHigh
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
self.view = containerView
// view.wantsLayer = true
view.addSubview(view1)
view.addSubview(view2)
view.subviewsUseAutoLayout = true
NSLayoutConstraint.activate([
view1.topAnchor.constraint(equalTo: view.topAnchor),
view1.bottomAnchor.constraint(equalTo: view.bottomAnchor),
view1.leadingAnchor.constraint(equalTo: view.leadingAnchor),
// view1.trailingAnchor.constraint(equalTo: view2.leadingAnchor),
view2.topAnchor.constraint(equalTo: view.topAnchor),
view2.bottomAnchor.constraint(equalTo: view.bottomAnchor),
view2.leadingAnchor.constraint(equalTo: view1.trailingAnchor),
view2.trailingAnchor.constraint(equalTo: view.trailingAnchor),
widthEqualContraint,
widthZeroConstraint,
])
}
func runAnimation() {
view.layoutSubtreeIfNeeded()
self.widthEqualContraint.toggleDefaultPriority()
self.widthZeroConstraint.toggleDefaultPriority()
// self.leadingConstraint.toggleDefaultPriority()
NSAnimationContext.runAnimationGroup({ (context) in
context.allowsImplicitAnimation = true
context.duration = 3.0
self.view.layoutSubtreeIfNeeded()
}) {
Swift.print("animation complete")
}
}
}
extension LayoutAnimationViewController {
#IBAction func runTest1(_ sender: Any?) {
self.runAnimation()
}
}
Also, some potentially relevant, but so far unhelpful, related questions:
Animating Auto Layout changes concurrently with NSPopover contentSize change
Animating Auto Layout constraints with NSView.layoutSubtreeIfNeeded() not working on macOS High Sierra
Hide view item of NSStackView with animation

Setting Background Color of NSTableCellView in Swift

After searching through SO and online, I'm struggling to figure out a concept that I thought would be relatively simple. Essentially, I have a table in an OS X Swift app, with several columns, and it is currently populating data. I am trying to discern how I can set the background color of each "row" (ideally with alternating colors, but I'll start with just one color). My MasterViewController file is like so;
import Cocoa
class MasterViewController: NSViewController {
var minions = [Minion]()
func setupSampleMinion() {
minions = Minion.fetchMinionData()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
}
// MARK: - NSTableViewDataSource extension MasterViewController: NSTableViewDataSource {
func numberOfRowsInTableView(aTableView: NSTableView) -> Int {
return self.minions.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
// 1
var cellView: NSTableCellView = tableView.makeViewWithIdentifier(tableColumn!.identifier, owner: self) as! NSTableCellView
let minion = self.minions[row]
// 2
if tableColumn!.identifier == "MyColumn" {
// 3
cellView.imageView!.image = NSImage(named: "minion.name!")
cellView.textField!.stringValue = minion.name!
return cellView
}
return cellView
}
}
func tableView(tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
let myCustomView = MyRowView()
return myCustomView
}
class MyRowView: NSTableRowView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
self.backgroundColor = NSColor(red: 0.76, green: 0.82, blue: 0.92, alpha: 1)
NSRectFill(dirtyRect)
}
}
// MARK: - NSTableViewDelegate extension MasterViewController: NSTableViewDelegate {
}
While I THINK I have some of the coding right, this does not seem to set the background color if the row in any way. Any thoughts or overall guidance would be most appreciated. Thank you!
If you just want the rows to use the standard alternating colors for rows, there's a simple checkbox in the Attributes inspector for the table view in IB to enable that.
To use a non-standard background color, you want to set the row view's backgroundColor, but not inside of drawRect(). If you change properties of a view that affect how it draws inside of drawRect(), that will probably mark the view as needing display, which will provoke another call to drawRect(), etc.
It should work to just set it in the delegate's tableView(_:didAddRowView:forRow:) method. That's documented in the description of the backgroundColor property.
With regard to your attempt at overriding drawRect(): setting the row view's backgroundColor will presumably affect how the superclass draws. So, setting it after calling through to super is unlikely to help. It definitely won't affect the subsequent NSRectFill() call. That function relies on the fill color set for the current graphics context, which is implicit. You would change that by calling someColor.set().
Buy, anyway, there should be no need to override drawRect() given that you can set the backgroundColor. If you want to achieve some background drawing beyond what's possible by just setting a color, you should override drawBackgroundInRect() and not drawRect(), anyway.
Finally, your implementation of tableView(tableView:rowViewForRow:) should call the table view's makeViewWithIdentifier(_:owner:) method first, before creating a new view. And it should set the identifier on any new view it does create. That allows the table view to maintain a reuse queue of views, to avoid constantly destroying and recreating views.

Custom TableViewCell with Subview whose height changes

I'm using a TableView and have a custom TableViewCell that I've added a subview to.
The problem is that I need the subview's height to change sometimes and therefore, the table's contentView would have to be updated as well as the row's height.
The subview of the custom TableViewCell is represented by the yellow background.
These images show what's currently happening in my simulator.
On Load
After the event that causes the subview's height to increase
What's the best approach to take with something like this?
Should I use constraints? And if so, what kind of constraints should I use? Would I have to then reload the tableview too every time the subview's size changes?
Here is the code I'm currently using for my custom TableViewCell:
import UIKit
class CustomCell: UITableViewCell {
var newView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.newView = UIView(frame: self.frame)
self.newView.backgroundColor = .yellowColor()
self.addSubview(newView)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
self.newView.frame.size.width = self.frame.size.width // because self.frame.width is different than it was in the init method
}
func somethingHappenedThatMySubviewHasToIncreaseInHeight() {
self.newView.frame.size.height = self.frame.size.height + 40
}
}
The best approach is to use Auto Layout and self-sizing cells. Setup constraints in storyboard for your custom cell.
You will not need to reload the tableView. Each cell will automatically adjust its height, based on how much vertical space its subview takes.
For more information, see the detailed walkthrough by smileyborg in his answer to Using Auto Layout in UITableView for dynamic cell layouts & variable row heights.

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

Using Autolayout with expanding NSTextViews

My app consists of an NSScrollView whose document view contains a number of vertically stacked NSTextViews — each of which resizes in the vertical direction as text is added.
Currently, this is all managed in code. The NSTextViews resize automatically, but I observe their resizing with an NSViewFrameDidChangeNotification, recalc all their origins so that they don't overlap, and resize their superview (the scroll view's document view) so that they all fit and can be scrolled to.
This seems as though it would be the perfect candidate for autolayout! I set NSLayoutConstraints between the first text view and its container, the last text view and its container, and each text view between each other. Then, if any text view grows, it automatically "pushes down" the origins of the text views below it to satisfy contraints, ultimately growing the size of the document view, and everyone's happy!
Except, it seems there's no way to make an NSTextView automatically grow as text is added in a constraints-based layout? Using the exact same NSTextView that automatically expanded as text was entered before, if I don't specify a constraint for its height, it defautls to 0 and isn't shown. If I do specify a constraint, even an inequality such as >=20, it stays stuck at that size and doesn't grow as text is added.
I suspect this has to do with NSTextView's implementation of -intrinsicContentSize, which by default returns (NSViewNoInstrinsicMetric, NSViewNoInstrinsicMetric).
So my questions: if I subclasses NSTextView to return a more meaningful intrinsicContentSize based on the layout of my text, would my autolayout then work as expected?
Any pointers on implementing intrinsicContentSize for a vertically resizing NSTextView?
I am working on a very similar setup — a vertical stack of views containing text views that expand to fit their text contents and use autolayout.
So far I have had to subclass NSTextView, which is does not feel clean, but works superbly in practice:
- (NSSize) intrinsicContentSize {
NSTextContainer* textContainer = [self textContainer];
NSLayoutManager* layoutManager = [self layoutManager];
[layoutManager ensureLayoutForTextContainer: textContainer];
return [layoutManager usedRectForTextContainer: textContainer].size;
}
- (void) didChangeText {
[super didChangeText];
[self invalidateIntrinsicContentSize];
}
The initial size of the text view when added with addSubview is, curiously, not the intrinsic size; I have not yet figured out how to issue the first invalidation (hooking viewDidMoveToSuperview does not help), but I'm sure I will figure it out eventually.
I had a similar problem with an NSTextField, and it turned out that it was due to the view wanting to hug its text content tightly along the vertical orientation. So if you set the content hugging priority to something lower than the priorities of your other constraints, it may work. E.g.:
[textView setContentHuggingPriority:NSLayoutPriorityFittingSizeCompression-1.0 forOrientation:NSLayoutConstraintOrientationVertical];
And in Swift, this would be:
setContentHuggingPriority(NSLayoutConstraint.Priority.fittingSizeCompression, for:NSLayoutConstraint.Orientation.vertical)
Here is how to make an expanding NSTextView using Auto Layout, in Swift 3
I used Anchors for Auto Layout
Use textDidChange from NSTextDelegate. NSTextViewDelegate conforms to NSTextDelegate
The idea is that textView has edges constraints, which means whenever its intrinsicContentSize changes, it will expand its parent, which is scrollView
import Cocoa
import Anchors
class TextView: NSTextView {
override var intrinsicContentSize: NSSize {
guard let manager = textContainer?.layoutManager else {
return .zero
}
manager.ensureLayout(for: textContainer!)
return manager.usedRect(for: textContainer!).size
}
}
class ViewController: NSViewController, NSTextViewDelegate {
#IBOutlet var textView: NSTextView!
#IBOutlet weak var scrollView: NSScrollView!
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
activate(
scrollView.anchor.top.constant(100),
scrollView.anchor.paddingHorizontally(30)
)
activate(
textView.anchor.edges
)
}
// MARK: - NSTextDelegate
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
print(textView.intrinsicContentSize)
textView.invalidateIntrinsicContentSize()
}
}
Class ready for copying and pasting. Swift 4.2, macOS 10.14
class HuggingTextView: NSTextView, NSTextViewDelegate {
//MARK: - Initialization
override init(frame: NSRect) {
super.init(frame: frame)
delegate = self
}
override init(frame frameRect: NSRect, textContainer container: NSTextContainer?) {
super.init(frame: frameRect, textContainer: container)
delegate = self
}
required init?(coder: NSCoder) {
super.init(coder: coder)
delegate = self
}
//MARK: - Overriden
override var intrinsicContentSize: NSSize {
guard let container = textContainer, let manager = container.layoutManager else {
return super.intrinsicContentSize
}
manager.ensureLayout(for: container)
return manager.usedRect(for: container).size
}
//MARK: - NSTextViewDelegate
func textDidChange(_ notification: Notification) {
invalidateIntrinsicContentSize()
}
}

Resources