Automatic re-ordering of Core Data in Swift - xcode

I am trying to reorder a my Core Data list when moving cells in "Edit Mode". There seems to be helpful discussion in this area for Obj-c (see links below), but I can't find anything in Swift.
Has anyone come across any related documentation for Swift? Or would anyone be willing to translate the obj-c code to Swift? Thanks!
Obj-c links:
How can I maintain display order in UITableView using Core Data?
How to implement re-ordering of CoreData records?

Here is a nice way I do it. It also checks if the destination may not contain any data and you can also move accross sections.
isMoving will inhibit the delegate from firing off. You also need a sortorder attribute at your entity.
private func indexIsOutOfRange(indexPath:NSIndexPath) -> Bool {
if indexPath.section > self.fetchedResultsController.sections!.count - 1 {
return true
}
if indexPath.row > self.fetchedResultsController.sections![indexPath.section].objects!.count - 1 {
return true
}
return false
}
override func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath)
{
if indexIsOutOfRange(destinationIndexPath) {
return
}
isMovingItem = true
if sourceIndexPath.section == destinationIndexPath.section {
if var todos = self.fetchedResultsController.sections![sourceIndexPath.section].objects {
let todo = todos[sourceIndexPath.row] as! FoodEntry
todos.removeAtIndex(sourceIndexPath.row)
todos.insert(todo, atIndex: destinationIndexPath.row)
var idx = 1
for todo in todos as! [FoodEntry] {
todo.sortOrder = NSNumber(integer: idx++)
}
try!managedObjectContext.save()
}
} else {
if var allObjectInSourceSection = fetchedResultsController.sections![sourceIndexPath.section].objects {
let object = allObjectInSourceSection[sourceIndexPath.row] as! FoodEntry
allObjectInSourceSection.removeAtIndex(sourceIndexPath.row)
for (index,object) in (allObjectInSourceSection as! [FoodEntry]).enumerate() {
object.sortOrder = NSNumber(integer: index)
}
if var allObjectInDestinationSection = fetchedResultsController.sections![destinationIndexPath.section].objects {
allObjectInDestinationSection.insert(object, atIndex: destinationIndexPath.row)
for (index,object) in (allObjectInDestinationSection as! [FoodEntry]).enumerate() {
object.sortOrder = NSNumber(integer: index)
object.section = NSNumber(integer: destinationIndexPath.section)
}
}
}
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
tableView.reloadRowsAtIndexPaths(tableView.indexPathsForVisibleRows!, withRowAnimation: .Fade)
})
isMovingItem = false
try!managedObjectContext.save()
fetch()
}

Related

Is there any way to make a paged ScrollView in SwiftUI?

I've been looking through the docs with each beta but haven't seen a way to make a traditional paged ScrollView. I'm not familiar with AppKit so I am wondering if this doesn't exist in SwiftUI because it's primarily a UIKit construct. Anyway, does anyone have an example of this, or can anyone tell me it's definitely impossible so I can stop looking and roll my own?
You can now use a TabView and set the .tabViewStyle to PageTabViewStyle()
TabView {
View1()
View2()
View3()
}
.tabViewStyle(PageTabViewStyle())
As of Beta 3 there is no native SwiftUI API for paging. I've filed feedback and recommend you do the same. They changed the ScrollView API from Beta 2 to Beta 3 and I wouldn't be surprised to see a further update.
It is possible to wrap a UIScrollView in order to provide this functionality now. Unfortunately, you must wrap the UIScrollView in a UIViewController, which is further wrapped in UIViewControllerRepresentable in order to support SwiftUI content.
Gist here
class UIScrollViewViewController: UIViewController {
lazy var scrollView: UIScrollView = {
let v = UIScrollView()
v.isPagingEnabled = true
return v
}()
var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
func pinEdges(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
}
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UIScrollViewViewController {
let vc = UIScrollViewViewController()
vc.hostingController.rootView = AnyView(self.content())
return vc
}
func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
viewController.hostingController.rootView = AnyView(self.content())
}
}
And then to use it:
var body: some View {
GeometryReader { proxy in
UIScrollViewWrapper {
VStack {
ForEach(0..<1000) { _ in
Text("Hello world")
}
}
.frame(width: proxy.size.width) // This ensures the content uses the available width, otherwise it will be pinned to the left
}
}
}
Apple's official tutorial covers this as an example. I find it easy to follow and suitable for my case. I really recommend you check this out and try to understand how to interface with UIKit. Since SwiftUI is so young, not every feature in UIKit would be covered at this moment. Interfacing with UIKit should address most if not all needs.
https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
Not sure if this helps your question but for the time being while Apple is working on adding a Paging View in SwiftUI I've written a utility library that gives you a SwiftUI feel while using a UIPageViewController under the hood tucked away.
You can use it like this:
Pages {
Text("Page 1")
Text("Page 2")
Text("Page 3")
Text("Page 4")
}
Or if you have a list of models in your application you can use it like this:
struct Car {
var model: String
}
let cars = [Car(model: "Ford"), Car(model: "Ferrari")]
ModelPages(cars) { index, car in
Text("The \(index) car is a \(car.model)")
.padding(50)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
}
You can simply track state using .onAppear() to load your next page.
struct YourListView : View {
#ObservedObject var viewModel = YourViewModel()
let numPerPage = 50
var body: some View {
NavigationView {
List(viewModel.items) { item in
NavigationLink(destination: DetailView(item: item)) {
ItemRow(item: item)
.onAppear {
if self.shouldLoadNextPage(currentItem: item) {
self.viewModel.fetchItems(limitPerPage: self.numPerPage)
}
}
}
}
.navigationBarTitle(Text("Items"))
.onAppear {
guard self.viewModel.items.isEmpty else { return }
self.viewModel.fetchItems(limitPerPage: self.numPerPage)
}
}
}
private func shouldLoadNextPage(currentItem item: Item) -> Bool {
let currentIndex = self.viewModel.items.firstIndex(where: { $0.id == item.id } )
let lastIndex = self.viewModel.items.count - 1
let offset = 5 //Load next page when 5 from bottom, adjust to meet needs
return currentIndex == lastIndex - offset
}
}
class YourViewModel: ObservableObject {
#Published private(set) items = [Item]()
// add whatever tracking you need for your paged API like next/previous and count
private(set) var fetching = false
private(set) var next: String?
private(set) var count = 0
func fetchItems(limitPerPage: Int = 30, completion: (([Item]?) -> Void)? = nil) {
// Do your stuff here based on the API rules for paging like determining the URL etc...
if items.count == 0 || items.count < count {
let urlString = next ?? "https://somePagedAPI?limit=/(limitPerPage)"
fetchNextItems(url: urlString, completion: completion)
} else {
completion?(pokemon)
}
}
private func fetchNextItems(url: String, completion: (([Item]?) -> Void)?) {
guard !fetching else { return }
fetching = true
Networking.fetchItems(url: url) { [weak self] (result) in
DispatchQueue.main.async { [weak self] in
self?.fetching = false
switch result {
case .success(let response):
if let count = response.count {
self?.count = count
}
if let newItems = response.results {
self?.items += newItems
}
self?.next = response.next
case .failure(let error):
// Error state tracking not implemented but would go here...
os_log("Error fetching data: %#", error.localizedDescription)
}
}
}
}
}
Modify to fit whatever API you are calling and handle errors based on your app architecture.
Checkout SwiftUIPager. It's a pager built on top of SwiftUI native components:
If you would like to exploit the new PageTabViewStyle of TabView, but you need a vertical paged scroll view, you can make use of effect modifiers like .rotationEffect().
Using this method I wrote a library called VerticalTabView 🔝 that turns a TabView vertical just by changing your existing TabView to VTabView.
You can use such custom modifier:
struct ScrollViewPagingModifier: ViewModifier {
func body(content: Content) -> some View {
content
.onAppear {
UIScrollView.appearance().isPagingEnabled = true
}
.onDisappear {
UIScrollView.appearance().isPagingEnabled = false
}
}
}
extension ScrollView {
func isPagingEnabled() -> some View {
modifier(ScrollViewPagingModifier())
}
}
To simplify Lorenzos answer, you can basically add UIScrollView.appearance().isPagingEnabled = true to your scrollview as below:
VStack{
ScrollView(showsIndicators: false){
VStack(spacing: 0){ // to remove spacing between rows
ForEach(1..<10){ i in
ZStack{
Text(String(i))
Circle()
} .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
}
}
}.onAppear {
UIScrollView.appearance().isPagingEnabled = true
}
.onDisappear {
UIScrollView.appearance().isPagingEnabled = false
}
}

Cocoa: react to keyDown in QLPreviewPanel

I implemented quick look in my project in the following way in Swift 2 (I'm including this here for reference and because it might help someone else set it up).
My NSViewController contains a NSTableView subclass where I implemented keyDown to listen to the spacebar key being pressed (maybe not the best way but it works):
override func keyDown(theEvent: NSEvent) {
let s = theEvent.charactersIgnoringModifiers!
let s1 = s.unicodeScalars
let s2 = s1[s1.startIndex].value
let s3 = Int(s2)
if s3 == Int(" ".utf16.first!) {
NSNotificationCenter.defaultCenter().postNotification(NSNotification(name: "MyTableViewSpacebar", object: nil))
return
}
super.keyDown(theEvent)
}
In my view controller, I have an observer for this notification and the functions required by the QLPreviewPanel:
//...
class ViewController: NSViewController {
#IBOutlet weak var myTableView: MyTableView!
var files = [FilesListData]() //array of custom class
//...
override func viewDidLoad() {
//...
NSNotificationCenter.defaultCenter().addObserver(self, selector: "spaceBarKeyDown:", name: "MyTableViewSpacebar", object: nil)
}
func spaceBarKeyDown(notification: NSNotification) {
if let panel = QLPreviewPanel.sharedPreviewPanel() {
panel.makeKeyAndOrderFront(self)
}
}
override func acceptsPreviewPanelControl(panel: QLPreviewPanel!) -> Bool {
return true
}
override func beginPreviewPanelControl(panel: QLPreviewPanel!) {
panel.delegate = self
panel.dataSource = self
}
override func endPreviewPanelControl(panel: QLPreviewPanel!) {
}
}
extension ViewController: QLPreviewPanelDataSource {
func numberOfPreviewItemsInPreviewPanel(panel: QLPreviewPanel!) -> Int {
return self.myTableView.selectedRowIndexes.count
}
func previewPanel(panel: QLPreviewPanel!, previewItemAtIndex index: Int) -> QLPreviewItem! {
if self.myTableView.selectedRow != -1 {
var items = [QLPreviewItem]()
let manager = NSFileManager.defaultManager()
for i in self.myTableView.selectedRowIndexes {
let path = self.files[i].path //path to a MP3 file
if manager.fileExistsAtPath(path) {
items.append(NSURL(fileURLWithPath: path))
} else {
items.append(qm_url) //image of a question mark used as placeholder
}
}
return items[index]
} else {
return qm_url //image of a question mark used as placeholder
}
}
}
What I would like to do now is listen to the keys "up arrow" and "down arrow" being pressed while the quick look panel is open, in order to change the selected row in the NSTableView, much like Finder behaves when you preview files with quick look. I have no clue as to how I could implement this. Any ideas?
Thanks.
Finally found what I was looking for and it's actually pretty simple.
Since my main view controller is also my delegate for the QLPreviewPanel, I added this:
extension ViewController: QLPreviewPanelDelegate {
func previewPanel(panel: QLPreviewPanel!, handleEvent event: NSEvent!) -> Bool {
let kc = event.keyCode
if (kc == 126 || kc == 125) { //up and down arrows
if event.type == NSEventType.KeyDown {
self.myTableView.keyDown(event) //send the event to the table
} else if event.type == NSEventType.KeyUp {
self.myTableView.keyUp(event)
}
return true
}
return false
}
}
Then in my table view delegate:
func tableViewSelectionDidChange(notification: NSNotification) {
guard myTableView.numberOfSelectedRows > 0 else {
if let panel = QLPreviewPanel.sharedPreviewPanel() {
if panel.visible {
panel.close()
}
}
return
}
if let panel = QLPreviewPanel.sharedPreviewPanel() {
if panel.visible {
panel.reloadData()
}
}
}
That's it! The QLPreviewPanelDataSource handles the rest.

Present dictionary in several NSTableviews

I'm a beginner to cocoa and I've been trying to make a simple app for Mac using swift programming language, but I'm stuck and can't find a solution.
I want to present a data from dictionary in two or more tableViews, where the first table will show key, and the second table will show value.
For example, I have a dictionary
var worldDict:NSDictionary = ["Africa":["Egypt", "Togo"],"Europe": ["Austria", "Spain"]]
I can present all continents in the first table, but I can't find out how to make second table to display countries from continent I choose in the first table.
My ViewController is a DataSource and Delegate for both tables.
extension ViewController: NSTableViewDataSource {
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
if tableView == continentTable {
return self.worldDict.valueForKey("Continent")!.count
} else if tableView == countryTable {
return self.worldDict.valueForKey("Continent")!.allKeys.count
}
return 0
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
var cell = tableView.makeViewWithIdentifier(tableColumn!.identifier, owner: self) as! NSTableCellView
if tableView == self.continentTable {
let continent: AnyObject? = wordlDict.valueForKey("Continent")
var keys = continent!.allKeys
cell.textField?.stringValue = keys[row] as! String
} else if tableView == self.countryTable {
var countriesOfContinent: AnyObject? = worldDict.valueForKey("Continent")?.valueForKey("Africa")!
cell.textField?.stringValue = countriesOfContinent?.allKeys[row] as! String
}
return cell
}
}
Here I present data from dictionary in tables, but separately, and can't figure out how to make them work together.
Also I know how to get the number of row that has been selected
extension ViewController: NSTableViewDelegate {
func tableViewSelectionDidChange(notification: NSNotification) {
let continentSelected = rowSelected()
}}
func rowSelected() -> Int? {
let selectedRow = self.continentTable.selectedRow
if selectedRow >= 0 && selectedRow < self.worldDict.valueForKey("Continent")!.count {
return selectedRow
}
return nil
}
Part of the problem is that you're relying on the ordering of the keys returned by allKeys() to be reliable, which it's not. You need to keep a separate array of continents. It can basically be a copy of whatever allKeys() returned on one occasion, but you should not keep calling allKeys() each time.
In numberOfRowsInTableView(), for the countries table, you want to return the number of countries in the selected continent:
} else if tableView == countryTable {
if let selectedContinentRow = rowSelected() {
let selectedContinent = continentsArray[selectedContinentRow]
return self.worldDict[selectedContinent].count
}
return 0
}
For tableView(_:viewForTableColumn:row:), you want to return an element from the selected continent's array of countries:
} else if tableView == self.countryTable {
if let selectedContinentRow = rowSelected() {
let selectedContinent = continentsArray[selectedContinentRow]
return self.worldDict[selectedContinent][row]
}
}
Also, whenever the selected continent changes, you need to tell the countries table to reload its data:
func tableViewSelectionDidChange(notification: NSNotification) {
// ... whatever else ...
let tableView = notification.object as! NSTableView
if tableView == continentTable {
countryTable.reloadData()
}
}

Drag and Drop to NSArrayController

I am struggling to pass a variable received from a drag and drop (file from finder onto NSView) to a NSArrayController in AppDelegate.
NSView receives the drag and drop:
class MyView:NSView, NSDraggingDestination {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
}
let fileTypes = ["jpg", "jpeg", "bmp", "tif", "TIF"]
var fileTypeIsOk = false
var droppedFilePath = ""
required init?(coder: NSCoder) {
let types = [NSFilenamesPboardType, NSURLPboardType, NSPasteboardTypeTIFF]
super.init(coder: coder)
registerForDraggedTypes(types)
}
override func draggingEntered(sender: NSDraggingInfo) -> NSDragOperation {
if checkExtension(sender) == true {
fileTypeIsOk = true
return .Every
} else {
fileTypeIsOk = false
println("Dropped images rejected.")
return .None
}
}
override func performDragOperation(sender: NSDraggingInfo) -> Bool {
var go = false
if let board = sender.draggingPasteboard().propertyListForType("NSFilenamesPboardType") as? NSArray {
for indexFile in 0...board.count-1 {
println("Files dropped \(board[indexFile])")
//-> pass the variable board[indexFile] or board[] to NSArrayController
go = true
}
}
return go
}
func checkExtension(drag: NSDraggingInfo) -> Bool {
var go = false
var foundExtension = 0
var numberOfFiles = 0
if let board = drag.draggingPasteboard().propertyListForType("NSFilenamesPboardType") as? NSArray {
numberOfFiles = board.count
for indexFile in 0...numberOfFiles-1 {
if let url = NSURL(fileURLWithPath: (board[indexFile] as! String)) {
let suffix = url.pathExtension!
for ext in fileTypes {
if ext.lowercaseString == suffix {
++foundExtension
break
}
}
}
}
}
if foundExtension == numberOfFiles {
println("Dropped files have valid extension.")
go = true
} else {
println("At least one file dropped has an invalid extension.")
}
return go
}
}
in AppDelegate, I have set up an NSArrayController (pointing to data[] at the moment but I don't know how to populate data[] from board[])
func numberOfRowsInTableView(aTableView: NSTableView) -> Int {
return data.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
var cellView: NSTableCellView = tableView.makeViewWithIdentifier(tableColumn!.identifier, owner: self) as! NSTableCellView
println("Reload Array")
if tableColumn!.identifier == "fileNameColumn" {
cellView.imageView!.image = NSImage(named: "GreenCircle")
cellView.textField!.stringValue = data[row]
return cellView
}
...
thank you for your help
Call the "- (void)addObjects:(NSArray *)objects;" method on your NSArrayController and pass it board:
[self.myArrayController addObjects:board];
It worked by adding in NSView: performDragOperation:
let appDelegate = NSApplication.sharedApplication().delegate as! AppDelegate
then I can reach the 'data' variable using appDelegate.data.

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