CALayer 'loses' values set from outside class during implicit animation - animation

I have a CLLayer with an implicit animation on a custom property in Swift.
From inside the class I have access to the interpolated values of my property during the animation. But when I use other properties that I set from outside the class, they don't have a value during the animation. When the animation has ended there original value is available again.
So in the following example people has five random values which are printed to the console in drawInContext, but when the animation is running people is empty
This is my custom CALayer
class BackgroundLayer: CALayer {
#NSManaged var range: Double
var people: []
override class func needsDisplayForKey(key: String) -> Bool {
if key == "range" {
return true
}
return super.needsDisplayForKey(key)
}
override func drawInContext(ctx: CGContext!) {
let center = CGPoint(x: Double(bounds.width)/2, y: Double(bounds.height)-bottomOffset)
let xDist = Double(bounds.width)
let yDist = Double(bounds.height)-bottomOffset-minRadius
let maxDistance = sqrt(pow(xDist,2) + pow(yDist,2))
let radius = CGFloat(minRadius + range * maxDistance)
CGContextSetFillColorWithColor(ctx, UIColor(red: 208/255, green: 2/255, blue: 27/255, alpha: 1).CGColor)
CGContextFillEllipseInRect(ctx, CGRectMake(center.x-radius, center.y-radius, radius*2, radius*2))
println("are there people? \(people)")
}
override func actionForKey(event: String!) -> CAAction! {
if event == "range" {
var currentValue = self.presentationLayer().valueForKey(event) as Double
if currentValue == 0 {
currentValue = self.range
}
var rangeAnim = CABasicAnimation(keyPath: event)
rangeAnim?.fromValue = currentValue
rangeAnim?.duration = 0.5
return rangeAnim
}
return super.actionForKey(event)
}
}
And this is how I use it in my UIViewController that sets the five random values
class ViewController: UIViewController {
private let backgroundLayer = BackgroundLayer()
override func viewDidLoad() {
super.viewDidLoad()
for index in 0..<5 {
println()
let point = (x: Double(arc4random_uniform(UInt32(view.frame.width))), y: Double(arc4random_uniform(UInt32(view.frame.height))))
backgroundLayer.people.append(point)
}
println(backgroundLayer.people)
}
override func viewWillLayoutSubviews() {
backgroundLayer.contentsScale = UIScreen.mainScreen().scale
backgroundLayer.frame = self.view.frame
self.view.layer.addSublayer(backgroundLayer)
backgroundLayer.setNeedsDisplay()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
backgroundLayer.range = 1
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
backgroundLayer.range = 0
}
}

Related

Would someone please share a current SpriteKit texture atlas sample to pull apart

My background is Arduino and I have found Swift a bit opaque. There are a lot of outdated tutorials out there too. I am working on just getting an animated sprite on screen. I have a small animated .png sequence that I will use, saving it into a ball.atlas folder and copying it to Assets.xcassets.
I have found what I believe was posted by Knight0fDragon. Running it shows "worked" in the console, but I don't understand how to assign it to a player so to speak and thus show up on the simulator. If someone would be willing to share the additional code needed to do so, I could pull it apart.
Here is the code:
let textureAtlas = SKTextureAtlas(named: "example")
var textureArray = [SKTexture]()
var frames:[SKTexture] = []
for index in 1 ... 59 {
let textureName = "example_\(index)"
let texture = textureAtlas.textureNamed(textureName)
frames.append(texture)
textureArray = frames
print("worked")
I can't share a texture atlas with you but I help explain a little see below
let textureAtlas = SKTextureAtlas(named: "example")
var textureArray = [SKTexture]()
var frames:[SKTexture] = []
for index in 1 ... 59 {
let textureName = "example_\(index)"
let texture = textureAtlas.textureNamed(textureName)
frames.append(texture)
textureArray = frames
print("worked")
The line that gives "let textureName = "example_(index)" is where KoD gives you the naming convention for all the sprites in the Atlas. The atlas itself for this bit of code is called "example". All the sprites saved in the Atlas are assigned the following name "example_X" (X is an integer). example_1, example_2, example_3 etc... etc until you get to example_59. If you don't want to make 59 textures then you need to edit the for loop giving the new value.
hmmmmm OK have a look here
import SpriteKit
class GameScene: SKScene{
//MARK: PROPERTIES
var lastUpdateTime: TimeInterval = 0
var dt: TimeInterval = 0
var myAnimation = SKAction()
let myAnimaTimeInterval: TimeInterval = 0.1
let mySprite = SKSpriteNode(texture: SKTextureAtlas(named: "example").textureNamed("example_1"))
//MARK: INITIALISATION
override func sceneDidLoad() {
}
override func didMove(to view: SKView) {
addChild(mySprite)
mySprite.position = CGPoint.zero
setAnimation()
runAnimation(node: mySprite)
}
//MARK: GAME LOOP
override func update(_ currentTime: TimeInterval) {
updateGameLoopTimer(currentTime: currentTime)
}
override func didEvaluateActions() {
}
override func didFinishUpdate() {
}
//MARK: TOUCHES
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
}
//MARK: HELPER FUNCTIONS
private func setAnimation() {
let textureAtlas = SKTextureAtlas(named: "example")
var textureArray: [SKTexture] = []
for index in 2...59 {
let textureName = "example_\(index)"
let texture = textureAtlas.textureNamed(textureName)
textureArray.append(texture)
}
for index in (1...58).reversed() {
let textureName = "example_\(index)"
let texture = textureAtlas.textureNamed(textureName)
textureArray.append(texture)
}
let animation = SKAction.animate(with: textureArray, timePerFrame: myAnimaTimeInterval)
myAnimation = SKAction.repeatForever(animation)
}
private func runAnimation(node: SKSpriteNode) {
node.run(myAnimation, withKey: "myAnimation")
}
private func stopAnimation(node: SKSpriteNode) {
node.removeAction(forKey: "myAnimation")
}
public func updateGameLoopTimer(currentTime: TimeInterval) {
if lastUpdateTime > 0 {
dt = currentTime - lastUpdateTime
} else {
dt = 0
}
lastUpdateTime = currentTime
}
//MARK: CLEAN-UP
deinit {
}
}
I've taken a few liberties with the first sprite for the character which made me change the loop a little.
You can attach the animation to any sprite using the runAnimation method and stop the animation with the stopAnimation method. I've just added the animation to the didMoveToView call.
Try replacing your gameScene file with the above. OK now with the game loop timer ... maybe there are better ways to time the game.... but for you and me it is OK.
Try it again

Not able to Rotate my 3D objects

I had created one project using ARKit and SceneKit framework. In which I am working with file extension .dae, the files are locally available in my project as shown in below screenshot.
Here I had applied many gestures on this virtual object such as Tap Gesture(When I tap on camera screen, it places the virtual object there), same way Pinch Gesture and Pan Gesture. All of these gestures are working perfectly fine. Now I wanted to apply rotation gesture, for which I got stuck how to do that, also I am not getting any such available sources to achieve this.
Below is my working code so far,
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
#IBOutlet var sceneView: ARSCNView!
private var movedObject: SCNNode?
private var hud :MBProgressHUD!
override func viewDidLoad() {
super.viewDidLoad()
self.sceneView.autoenablesDefaultLighting = true
sceneView.delegate = self
sceneView.showsStatistics = true
let scene = SCNScene()
sceneView.scene = scene
registerGestureRecognizers()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
// Run the view's session
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
}
private func registerGestureRecognizers() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapped(recognizer:)))
tapGestureRecognizer.numberOfTapsRequired = 1
self.sceneView.addGestureRecognizer(tapGestureRecognizer)
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(pinched(recognizer:)))
self.sceneView.addGestureRecognizer(pinchGestureRecognizer)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(moveObject(recognizer:)))
panGestureRecognizer.maximumNumberOfTouches = 1
panGestureRecognizer.minimumNumberOfTouches = 1
self.sceneView.addGestureRecognizer(panGestureRecognizer)
let rotationGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(rotateObject(recognizer:)))
self.sceneView.addGestureRecognizer(rotationGestureRecognizer)
}
#objc func pinched(recognizer :UIPinchGestureRecognizer) {
if recognizer.state == .changed {
guard let sceneView = recognizer.view as? ARSCNView else {
return
}
let touch = recognizer.location(in: sceneView)
let hitTestResults = self.sceneView.hitTest(touch, options: nil)
if let hitTest = hitTestResults.first {
let chairNode = hitTest.node
let pinchScaleX = Float(recognizer.scale) * chairNode.scale.x
let pinchScaleY = Float(recognizer.scale) * chairNode.scale.y
let pinchScaleZ = Float(recognizer.scale) * chairNode.scale.z
chairNode.scale = SCNVector3(pinchScaleX,pinchScaleY,pinchScaleZ)
recognizer.scale = 1
}
}
}
#objc func moveObject(recognizer: UIPanGestureRecognizer) {
print("Move object")
if recognizer.state == .began {
print("Pan state began")
let tapPoint: CGPoint? = recognizer.location(in: sceneView)
let result = sceneView.hitTest(tapPoint ?? CGPoint.zero, options: nil)
if result.count == 0 {
return
}
let hitResult: SCNHitTestResult? = result.first
if (hitResult?.node.name == "free_car_1") {
movedObject = hitResult?.node
} else if (hitResult?.node.parent?.name == "free_car_1") {
movedObject = hitResult?.node.parent
}
if (movedObject != nil) {
print("Holding an Object")
}
}
if recognizer.state == .changed {
print("Pan State Changed")
if (movedObject != nil) {
let tapPoint: CGPoint? = recognizer.location(in: sceneView)
let hitResults = sceneView.hitTest(tapPoint ?? CGPoint.zero, types: .featurePoint)
let result: ARHitTestResult? = hitResults.last
let matrix: SCNMatrix4 = SCNMatrix4((result?.worldTransform)!)
//SCNMatrix4FromMat4((result?.worldTransform)!)
let vector: SCNVector3 = SCNVector3Make(matrix.m41, matrix.m42, matrix.m43)
movedObject?.position = vector
print("Moving object position")
}
}
if recognizer.state == .ended {
print("Done moving object homeie")
movedObject = nil
}
}
#objc func tapped(recognizer :UITapGestureRecognizer) {
guard let sceneView = recognizer.view as? ARSCNView else {
return
}
let touch = recognizer.location(in: sceneView)
let hitTestResults = sceneView.hitTest(touch)
guard let hitTest = hitTestResults.first?.node else {
let hitTestResultsWithExistingPlane = sceneView.hitTest(touch, types: .existingPlane)
let chairScene = SCNScene(named: "ShelbyWD.dae")!
guard let chairNode = chairScene.rootNode.childNode(withName: "ShelbyWD", recursively: true) else {
return
}
if let hitTestAvailable = hitTestResultsWithExistingPlane.first {
chairNode.position = SCNVector3(hitTestAvailable.worldTransform.columns.3.x,hitTestAvailable.worldTransform.columns.3.y,hitTestAvailable.worldTransform.columns.3.z)
self.sceneView.scene.rootNode.addChildNode(chairNode)
return
}
return
}
hitTest.removeFromParentNode()
}
#objc func rotateObject(recognizer :UIRotationGestureRecognizer)
{
}
}
Can anyone help me out to apply rotation gesture on my object?
Thank you!
In order to rotate an SCNNode, the 1st thing you need to do, is create a variable to store the rotationAngle around the YAxis or any other that you wish to perform the rotation on e.g:
var currentAngleY: Float = 0.0
Then have some way to have detected to node you wish to rotate, which in my example I am calling currentNode e.g.
var currentNode: SCNNode!
In my example I will just rotate around the YAxis.
You can use a UIPanGestureRecognizer like so:
/// Rotates An Object On It's YAxis
///
/// - Parameter gesture: UIPanGestureRecognizer
#objc func rotateObject(_ gesture: UIPanGestureRecognizer) {
guard let nodeToRotate = currentNode else { return }
let translation = gesture.translation(in: gesture.view!)
var newAngleY = (Float)(translation.x)*(Float)(Double.pi)/180.0
newAngleY += currentAngleY
nodeToRotate.eulerAngles.y = newAngleY
if(gesture.state == .ended) { currentAngleY = newAngleY }
print(nodeToRotate.eulerAngles)
}
Or if you wish to use a UIRotationGesture you can do something like this:
/// Rotates An SCNNode Around It's YAxis
///
/// - Parameter gesture: UIRotationGestureRecognizer
#objc func rotateNode(_ gesture: UIRotationGestureRecognizer){
//1. Get The Current Rotation From The Gesture
let rotation = Float(gesture.rotation)
//2. If The Gesture State Has Changed Set The Nodes EulerAngles.y
if gesture.state == .changed{
isRotating = true
currentNode.eulerAngles.y = currentAngleY + rotation
}
//3. If The Gesture Has Ended Store The Last Angle Of The Cube
if(gesture.state == .ended) {
currentAngleY = currentNode.eulerAngles.y
isRotating = false
}
}
Hope it helps...

Set identifier of table cell view programmatically

I have table view like this (in a mac cocoa application):
In the leftmost panel you can see that I have set the identifier of the Table Cell View to "1". That's fine if you just have 2 columns, once the number goes up, this approach will become cumbersome. Can I do this programmatically?
Here is an example:
import Cocoa
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
private var dataModel = DataModel()
private var answer = 0
private var keyData: (Int, [Int]) = (0, []) {
didSet {
tbl.reloadData()
}
}
#IBOutlet weak var questionIndex: NSTextField!
#IBOutlet weak var tbl: NSTableView!
#IBAction func replay(_ sender: Any) {
dataModel = DataModel()
questionIndex.stringValue = "0:"
answer = 0
updateModel()
}
#IBAction func forward(_ sender: NSButton) {
if sender.tag == 1 {
answer += keyData.0
}
updateModel()
}
func updateModel() {
let group = dataModel.nextGroup()
if let g = group {
self.keyData = g
let s = questionIndex.stringValue
questionIndex.stringValue = String(Int(String(s.characters.dropLast()))! + 1) + ":"
return
}
let alert = NSAlert()
alert.messageText = "You did have \(answer) on your mind, didn't you?"
alert.runModal()
}
override func viewDidLoad() {
super.viewDidLoad()
for (n, col) in tbl.tableColumns.enumerated() {
col.identifier = String(n)
}
updateModel()
}
func numberOfRows(in tableView: NSTableView) -> Int {
return keyData.1.count / 8 + 1
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let colId = tableColumn!.identifier
let colIndex = Int(colId)!
let index = (row * 8) + colIndex
let cell = tbl.make(withIdentifier: colId, owner: self) as! NSTableCellView
if 0 <= index && index < keyData.1.count {
cell.textField!.integerValue = keyData.1[index]
} else {
cell.textField!.stringValue = ""
}
return cell
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
I have assigned the cell identifiers by hand, and made them identical the corresponding column index, so as to creating a mapping between the cell id and the 2D array (which is the underlying data model) column index. The app is running fine, I just don't like assigning these IDs by click-and-point.
The full project can be found here: https://github.com/kindlychung/MysteriousNum
Create custom cell and add init to it using following lines.
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
and register this cell class as.
self.tableView.register(CustomCell.self, forCellReuseIdentifier: "customCell")
also dequeueReusableCell using same cell like:
tableView.dequeueReusableCell(withIdentifier: "customCell",for: indexPath) as! CustomCell

Reordering Realm.io data in tableView with Swift

I have implemented a basic example of an ios app using Realm.io
I'd like to be able to reorder table rows in my iOS app and save the order back to Realm.
Realm model contains a property called position for this purpose.
P.S: Sorry for so much code.
import UIKit
import Realm
class Cell: UITableViewCell {
var position: Int!
init(style: UITableViewCellStyle, reuseIdentifier: String!) {
super.init(style: .Subtitle, reuseIdentifier: reuseIdentifier)
}
}
class Language: RLMObject {
var title = ""
var position = Int()
}
class ManagerLanguagesController: UITableViewController, UITableViewDelegate, UITableViewDataSource {
var array = RLMArray()
var notificationToken: RLMNotificationToken?
var editButton = UIBarButtonItem()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
notificationToken = RLMRealm.defaultRealm().addNotificationBlock { note, realm in
self.reloadData()
}
reloadData()
}
override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
return Int(array.count)
}
func setupUI() {
tableView.registerClass(Cell.self, forCellReuseIdentifier: "cell")
self.title = "Languages"
var addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "add")
editButton = UIBarButtonItem(title: "Edit", style: .Plain, target: self, action: "edit")
var buttons = [addButton, editButton]
self.navigationItem.rightBarButtonItems = buttons
}
func add() {
var addLanguageView:UIViewController = self.storyboard.instantiateViewControllerWithIdentifier("newLanguage") as UIViewController
self.navigationController.presentViewController(addLanguageView, animated: true, completion: nil)
}
func edit () {
if tableView.editing {
/* FROM THIS POINT I'M PROBABLY DOING SOMETHING WRONG.. IT IS NOT WORKING */
var positionArray = NSMutableArray()
let realm = RLMRealm.defaultRealm()
var i = 0
for var row = 0; row < tableView.numberOfRowsInSection(0); row++ {
var cellPath = NSIndexPath(forRow: row, inSection: 0)
var cell:Cell = tableView.cellForRowAtIndexPath(cellPath) as Cell
positionArray.addObject(cell.position)
}
realm.beginWriteTransaction()
for row: RLMObject in array {
row["position"] = positionArray[i]
i++
}
realm.commitWriteTransaction()
/* -- NOT WORKING END -- */
tableView.setEditing(false, animated: true)
editButton.style = UIBarButtonItemStyle.Plain
editButton.title = "Edit"
} else{
tableView.setEditing(true, animated: true)
editButton.title = "Done"
editButton.style = UIBarButtonItemStyle.Done
}
}
override func tableView(tableView: UITableView?, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell? {
let cell = tableView!.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as Cell
let object = array[UInt(indexPath!.row)] as Language
cell.textLabel.text = object.title
cell.position = object.position // I have implemented this to be able to retain initial positions for each row and maybe use this when reordering..
return cell
}
override func tableView(tableView: UITableView!, canMoveRowAtIndexPath indexPath: NSIndexPath!) -> Bool {
return true
}
override func tableView(tableView: UITableView!, moveRowAtIndexPath sourceIndexPath: NSIndexPath!, toIndexPath destinationIndexPath: NSIndexPath!) {
// println("Old index: \(sourceIndexPath.indexAtPosition(sourceIndexPath.length - 1)+1)")
// println("New index: \(destinationIndexPath.indexAtPosition(sourceIndexPath.length - 1)+1)")
// Maybe something needs to be implemented here instead...
}
func reloadData() {
array = Language.allObjects().arraySortedByProperty("position", ascending: true)
tableView.reloadData()
}
}
Thanks in advance
Instead of using a position property, you could instead keep an ordered array as a property on another object. This way you don't have to keep the position up to date and instead arrange your objects as needed:
class Language: RLMObject {
dynamic var title = ""
}
class LanguageList: RLMObject {
dynamic var languages = RLMArray(objectClassName: "Language")
}
class ManagerLanguagesController: UITableViewController, UITableViewDelegate, UITableViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
// create our list
var realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
realm.addObject(LanguageList())
realm.commitWriteTransaction()
...
}
// helper to get the RLMArray of languages in our list
func array() -> RLMArray {
return (LanguageList.allObjects().firstObject() as LanguageList).languages
}
override func tableView(tableView: UITableView!, moveRowAtIndexPath sourceIndexPath: NSIndexPath!, toIndexPath destinationIndexPath: NSIndexPath!) {
var languages = array()
var object = languages.objectAtIndex(UInt(sourceIndexPath.row)) as Language
var realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
languages.removeObjectAtIndex(UInt(sourceIndexPath.row))
languages.insertObject(object, atIndex: UInt(destinationIndexPath.row))
realm.commitWriteTransaction()
}
...
}
this work for me to move rows in tableview using realm with swift 2.2:
func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
let aux = TimesHome.mutableCopy() as! NSMutableArray
let itemToMove = aux[fromIndexPath.row]
let realm = try! Realm()
realm.beginWrite()
aux.removeObjectAtIndex(fromIndexPath.row)
aux.insertObject(itemToMove, atIndex: toIndexPath.row)
try! realm.commitWrite()
TimesHome = aux
let times = realm.objects(ParciaisTimes)
if times.count > 0 {
for tm in times {
for i in 1...aux.count {
if aux[i-1].valueForKey("time_id") as! Int == tm.time_id {
realm.beginWrite()
tm.ordem = i
try! realm.commitWrite()
}
}
}
}
}

How to draw your own NSTabView tabs?

I want to draw my own tabs for NSTabViewItems. My Tabs should look different and start in the top left corner and not centered.
How can I do this?
it is possible to set the NSTabView's style to Tabless and then control it with a NSSegmentedControl that subclasses NSSegmentedCell to override style and behavior. For an idea how to do this, check out this project that emulates Xcode 4 style tabs: https://github.com/aaroncrespo/WILLTabView/.
One of possible ways to draw tabs - is to use NSCollectionView. Here is Swift 4 example:
Class TabViewStackController contains TabViewController preconfigured with style .unspecified and custom TabBarView.
class TabViewStackController: ViewController {
private lazy var tabBarView = TabBarView().autolayoutView()
private lazy var containerView = View().autolayoutView()
private lazy var tabViewController = TabViewController()
private let tabs: [String] = (0 ..< 14).map { "TabItem # \($0)" }
override func setupUI() {
view.addSubviews(tabBarView, containerView)
embedChildViewController(tabViewController, container: containerView)
}
override func setupLayout() {
LayoutConstraint.withFormat("|-[*]-|", forEveryViewIn: containerView, tabBarView).activate()
LayoutConstraint.withFormat("V:|-[*]-[*]-|", tabBarView, containerView).activate()
}
override func setupHandlers() {
tabBarView.eventHandler = { [weak self] in
switch $0 {
case .select(let item):
self?.tabViewController.process(item: item)
}
}
}
override func setupDefaults() {
tabBarView.tabs = tabs
if let item = tabs.first {
tabBarView.select(item: item)
tabViewController.process(item: item)
}
}
}
Class TabBarView contains CollectionView which represents tabs.
class TabBarView: View {
public enum Event {
case select(String)
}
public var eventHandler: ((Event) -> Void)?
private let cellID = NSUserInterfaceItemIdentifier(rawValue: "cid.tabView")
public var tabs: [String] = [] {
didSet {
collectionView.reloadData()
}
}
private lazy var collectionView = TabBarCollectionView()
private let tabBarHeight: CGFloat = 28
private (set) lazy var scrollView = TabBarScrollView(collectionView: collectionView).autolayoutView()
override var intrinsicContentSize: NSSize {
let size = CGSize(width: NSView.noIntrinsicMetric, height: tabBarHeight)
return size
}
override func setupHandlers() {
collectionView.delegate = self
}
override func setupDataSource() {
collectionView.dataSource = self
collectionView.register(TabBarTabViewItem.self, forItemWithIdentifier: cellID)
}
override func setupUI() {
addSubviews(scrollView)
wantsLayer = true
let gridLayout = NSCollectionViewGridLayout()
gridLayout.maximumNumberOfRows = 1
gridLayout.minimumItemSize = CGSize(width: 115, height: tabBarHeight)
gridLayout.maximumItemSize = gridLayout.minimumItemSize
collectionView.collectionViewLayout = gridLayout
}
override func setupLayout() {
LayoutConstraint.withFormat("|[*]|", scrollView).activate()
LayoutConstraint.withFormat("V:|[*]|", scrollView).activate()
}
}
extension TabBarView {
func select(item: String) {
if let index = tabs.index(of: item) {
let ip = IndexPath(item: index, section: 0)
if collectionView.item(at: ip) != nil {
collectionView.selectItems(at: [ip], scrollPosition: [])
}
}
}
}
extension TabBarView: NSCollectionViewDataSource {
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return tabs.count
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let tabItem = tabs[indexPath.item]
let cell = collectionView.makeItem(withIdentifier: cellID, for: indexPath)
if let cell = cell as? TabBarTabViewItem {
cell.configure(title: tabItem)
}
return cell
}
}
extension TabBarView: NSCollectionViewDelegate {
func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
if let first = indexPaths.first {
let item = tabs[first.item]
eventHandler?(.select(item))
}
}
}
Class TabViewController preconfigured with style .unspecified
class TabViewController: GenericTabViewController<String> {
override func viewDidLoad() {
super.viewDidLoad()
transitionOptions = []
tabStyle = .unspecified
}
func process(item: String) {
if index(of: item) != nil {
select(itemIdentifier: item)
} else {
let vc = TabContentController(content: item)
let tabItem = GenericTabViewItem(identifier: item, viewController: vc)
addTabViewItem(tabItem)
select(itemIdentifier: item)
}
}
}
Rest of the classes.
class TabBarCollectionView: CollectionView {
override func setupUI() {
isSelectable = true
allowsMultipleSelection = false
allowsEmptySelection = false
backgroundView = View(backgroundColor: .magenta)
backgroundColors = [.clear]
}
}
class TabBarScrollView: ScrollView {
override func setupUI() {
borderType = .noBorder
backgroundColor = .clear
drawsBackground = false
horizontalScrollElasticity = .none
verticalScrollElasticity = .none
automaticallyAdjustsContentInsets = false
horizontalScroller = InvisibleScroller()
}
}
// Disabling scroll view indicators.
// See: https://stackoverflow.com/questions/9364953/hide-scrollers-while-leaving-scrolling-itself-enabled-in-nsscrollview
private class InvisibleScroller: Scroller {
override class var isCompatibleWithOverlayScrollers: Bool {
return true
}
override class func scrollerWidth(for controlSize: NSControl.ControlSize, scrollerStyle: NSScroller.Style) -> CGFloat {
return CGFloat.leastNormalMagnitude // Dimension of scroller is equal to `FLT_MIN`
}
override func setupUI() {
// Below assignments not really needed, but why not.
scrollerStyle = .overlay
alphaValue = 0
}
}
class TabBarTabViewItem: CollectionViewItem {
private lazy var titleLabel = Label().autolayoutView()
override var isSelected: Bool {
didSet {
if isSelected {
titleLabel.font = Font.semibold(size: 10)
contentView.backgroundColor = .red
} else {
titleLabel.font = Font.regular(size: 10.2)
contentView.backgroundColor = .blue
}
}
}
override func setupUI() {
view.addSubviews(titleLabel)
view.wantsLayer = true
titleLabel.maximumNumberOfLines = 1
}
override func setupDefaults() {
isSelected = false
}
func configure(title: String) {
titleLabel.text = title
titleLabel.textColor = .white
titleLabel.alignment = .center
}
override func setupLayout() {
LayoutConstraint.withFormat("|-[*]-|", titleLabel).activate()
LayoutConstraint.withFormat("V:|-(>=4)-[*]", titleLabel).activate()
LayoutConstraint.centerY(titleLabel).activate()
}
}
class TabContentController: ViewController {
let content: String
private lazy var titleLabel = Label().autolayoutView()
init(content: String) {
self.content = content
super.init()
}
required init?(coder: NSCoder) {
fatalError()
}
override func setupUI() {
contentView.addSubview(titleLabel)
titleLabel.text = content
contentView.backgroundColor = .green
}
override func setupLayout() {
LayoutConstraint.centerXY(titleLabel).activate()
}
}
Here is how it looks like:
NSTabView isn't the most customizable class in Cocoa, but it is possible to subclass it and do your own drawing. You won't use much functionality from the superclass besides maintaining a collection of tab view items, and you'll end up implementing a number of NSView and NSResponder methods to get the drawing and event handling working correctly.
It might be best to look at one of the free or open source tab bar controls first, I've used PSMTabBarControl in the past, and it was much easier than implementing my own tab view subclass (which is what it was replacing).
I've recently done this for something I was working on.
I ended using a tabless tab view and then drawing the tabs myself in another view. I wanted my tabs to be part of a status bar at the bottom of the window.
You obviously need to support mouse clicks which is fairly easy, but you should make sure your keyboard support works too, and that's a little more tricky: you'll need to run timers to switch the tab after no keyboard access after half a second (have a look at the way OS X does it). Accessibility is another thing you should think about but you might find it just works—I haven't checked it in my code yet.
I very much got stuck on this - and posted NSTabView with background color - as the PSMTabBarControl is now out of date also posted https://github.com/dirkx/CustomizableTabView/blob/master/CustomizableTabView/CustomizableTabView.m
It's very easy to use a separate NSSegmentedCell to control tab selection in an NSTabView. All you need is an instance variable that they can both bind to, either in the File's Owner, or any other controller class that appears in your nib file. Just put something like this in the class Interface declaraton:
#property NSInteger selectedTabIndex;
Then, in the IB Bindings Inspector, bind the Selected Index of both the NSTabView and the NSSegmentedCell to the same selectedTabIndex property.
That's all you need to do! You don't need to initialize the property unless you want the default selected tab index to be something other than zero. You can either keep the tabs, or make the NSTabView tabless, it will work either way. The controls will stay in sync regardless of which control changes the selection.

Resources