Next object instance IGListKit - swift

Edit: I am new to the field, I did not get a response. Can anyone tell me if I am missing some information? or how I could improve it?
I want an instance from the next object to set a Label text in my current cell
cell.Label.text = talents(nextIndex).name //<- Something of this sort
Tried: passing array input to SectionController to use as
talents[index+1]
Error: File out of range
My Section Controller
class SectionController: ListSectionController {
var talents: Talent!
weak var delegate: SectionControllerDelegate?
}
extension SectionController {
override func sizeForItem(at index: Int) -> CGSize {
guard
let context = collectionContext
else {
return .zero
}
let width = context.containerSize.width
let height = context.containerSize.height
return CGSize(width: width, height: height)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
guard let cell = collectionContext?.dequeueReusableCellFromStoryboard(withIdentifier: "HomeCell",
for: self,
at: index) as? HomeCell else {
fatalError()
}
cell.nameLabel.text = talents.name
cell.descView.text = talents.largeDesc
cell.shortDesc.text = talents.smallDesc
// let nextTalent = talents[index+1]
// cell.nextIntroFunc(nextlabels: nextTalent)
return cell
}
override func didUpdate(to object: Any) {
talents = object as? Talent
}
}
My ListAdapterDataSource
extension Home: ListAdapterDataSource {
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
print(talents)
return talents
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any)
-> ListSectionController {
return SectionController()
}
func emptyView(for listAdapter: ListAdapter) -> UIView? {
print("emptyView")
return nil
}
}

talents in the section controller isn't an array. It's just a single Talent, so calling [index + 1] on it is going to throw an error. (You might want to rename the property of the model in the section controller to talent so it's less confusing.)
Best practice for ListSectionController is to keep a 1:1 relationship between your object model and the section controller. If you want to have reference to the next object, you could create a cell/view model layer based on talent that includes reference to the next talent as well.
For example:
class TalentSectionModel: ListDiffable {
let talent: Talent
let nextTalent: Talent?
init(_ talent: Talent, nextTalent: Talent?) {
self.talent = talent
self.nextTalent = nextTalent
}
// whatever properties you want to expose
var name: String {
return talent.name
}
// example of exposing info about next talent
var nextTalentName: String? {
return nextTalent?.name
}
// ListDiffable protocol
// make sure to implement these two so diffing works
func diffIdentifier() -> NSObjectProtocol {}
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {}
}
You could also just only pass in the necessary properties from the next talent without passing in the entire nextTalent if you don't want the section model to store the entire object.
Now with the TalentSectionModel you would map over your talents array in Home and return an array of view models in objects() under ListAdapterDataSource. Then, in your section controller you'll have the next talent's information available to you through the view model.
Resources:
ListKit Modeling and Binding (talks about the ListBindingSectionController, but also helpful to read for general understanding of model and section controller relationship),
ListKit best practices

Related

Binding to a datasource using RxDatasources

I have a list of models that I fetch from a server, and basically I get array of these:
struct ContactModel: Codable, Equatable {
static func == (lhs: ContactModel, rhs: ContactModel) -> Bool {
return lhs.name == rhs.name &&
lhs.avatar == rhs.avatar &&
lhs.job == rhs.job &&
lhs.follow == rhs.follow
}
let id: Int
let name: String
let avatar:String?
let job: JobModel?
let follow:Bool
enum CodingKeys: String, CodingKey {
case id, name, avatar, job, follow
}
}
So I want to show a list of contacts in my tableview.
Now I have this struct as well which is wrapper around this model:
struct ContactCellModel : Equatable, IdentifiableType {
static func == (lhs: ContactCellModel, rhs: ContactCellModel) -> Bool {
return lhs.model == rhs.model
}
var identity: Int {
return model.id
}
var model: ContactModel
var cellIdentifier = ContactTableViewCell.identifier
}
What I am trying to do, is to create datasource using RxDatasources, and bind to it, like this(ContactsViewController.swift):
let dataSource = RxTableViewSectionedAnimatedDataSource<ContactsSectionModel>(
configureCell: { dataSource, tableView, indexPath, item in
if let cell = tableView.dequeueReusableCell(withIdentifier: item.cellIdentifier, for: indexPath) as? BaseTableViewCell{
cell.setup(data: item.model)
return cell
}
return UITableViewCell()
})
but I am not sure what I should do right after. I tried something like this:
Observable.combineLatest(contactsViewModel.output.contacts, self.contactViewModel.changedStatusForContact)
.map{ (allContacts, changedContact) -> ContactsSectionModel in
//what should I return here?
}.bind(to: dataSource)
I use combineLatest, cause I have one more observable (self.contactViewModel.changedStatusForContact) that notifies when certain contact has been changed (this happens when you tap a certain button on a contact cell).
So what should I return from .map above in order to successfully bind to previously created dataSource?
You have to replace the contacts that have changed; all the contacts that were changed, but you can't do that because you aren't tracking all of them, only the most recent one. So you can't do it in a map. You need to use scan instead.
I made a lot of assumptions about code you didn't post, so the below is a compilable example. If your types are different, then you will have to make some changes:
func example(contacts: Observable<[ContactModel]>, changedStatusForContact: Observable<ContactModel>, tableView: UITableView, disposeBag: DisposeBag) {
let dataSource = RxTableViewSectionedAnimatedDataSource<ContactsSectionModel>(
configureCell: { dataSource, tableView, indexPath, item in
if let cell = tableView.dequeueReusableCell(withIdentifier: item.cellIdentifier, for: indexPath) as? BaseTableViewCell {
cell.setup(data: item.model)
return cell
}
return UITableViewCell() // this is quite dangerious. Better would be to crash IMO.
})
let contactsSectionModels = Observable.combineLatest(contacts, changedStatusForContact) {
(original: $0, changed: $1)
}
.scan([ContactsSectionModel]()) { state, updates in
// `state` is the last array handed to the table view.
// `updates` contains the values from the combineLatest above.
var contactModels = state.flatMap { $0.items.map { $0.model } }
// get all the contactModels out of the state.
if contactModels.isEmpty {
contactModels = updates.original
}
// if there aren't any, then update with the values coming from `contacts`
else {
guard let index = contactModels
.firstIndex(where: { $0.id == updates.changed.id })
else { return state }
contactModels[index] = updates.changed
}
// otherwise find the index of the contact that changed.
// and update the array with the changed contact.
return [ContactsSectionModel(
model: "",
items: updates.original
.map { ContactCellModel(model: $0) }
)]
// rebuild the section model and return it.
}
contactsSectionModels
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
typealias ContactsSectionModel = AnimatableSectionModel<String, ContactCellModel>
struct ContactCellModel: Equatable, IdentifiableType {
var identity: Int { model.id }
let model: ContactModel
let cellIdentifier = ContactTableViewCell.identifier
}
struct ContactModel: Equatable {
let id: Int
let name: String
let avatar: String?
let job: JobModel?
let follow: Bool
}
struct JobModel: Equatable { }
class BaseTableViewCell: UITableViewCell {
func setup(data: ContactModel) { }
}
class ContactTableViewCell: BaseTableViewCell {
static let identifier = "Cell"
}

Why View-Based NSOutlineView with autosaveExpandedItems true ignores expanded upon reloadData?

I use a NSOutlineView that auto saves expanded state. If I manually reload data when dataSource updates, the func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? datasource method is not called anymore and every cell collapses. Any idea why this might happen?
Tried to reloadItem with nil send as param but still no good.
I use this for persisting expanded rows:
func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? {
return NSKeyedArchiver.archivedData(withRootObject: item)
}
func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? {
guard let data = object as? Data,
let item = NSKeyedUnarchiver.unarchiveObject(with: data) as? Category else { return nil }
let foundItem = recursiveSearch(for: item, in: viewModel.dataSource.value)
return foundItem
}
And this to reloadData:
viewModel.dataSource.subscribe(onNext: { [weak self] _ in
self?.outlineView.reloadData()
}).disposed(by: disposeBag)
IMHO autosaving is sort of half-baked feature and it doesn't work as expected. In other words, it's implemented in a way that it restores the state when your application launches (just once) and then you're on your own.
Implement your own one utilizing outlineViewItemDidExpand(_:) & outlineViewItemDidCollapse(_:) (especially when we're reloading, ...).
Couple of tricks you can use if you do not want to implement custom autosaving. But I wouldn't rely on them.
First trick - tell the NSOutlineView to reload persistent state
NSOutlineView inherits from the NSTableView and the autosaveName property documentation says:
If you change the value of this property to a new name, the table reads in any saved information and sets the order and width of this table view’s columns to match. Setting the name to nil removes any previously stored state from the user defaults.
What is inaccurate here - setting it to nil doesn't remove previously stored expanded items state for NSOutlineView. We can use it to force the NSOutlineView to reload expanded items state:
class ViewController: NSViewController, NSOutlineViewDelegate, NSOutlineViewDataSource {
#IBOutlet var outlineView: NSOutlineView!
// It's for testing, to demonstrate the persistent state reloading
private var doNotLoad = true
override func viewDidAppear() {
super.viewDidAppear()
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.doNotLoad = false
let autosaveName = self.outlineView.autosaveName
self.outlineView.autosaveName = nil
self.outlineView.reloadData()
self.outlineView.autosaveName = autosaveName
}
}
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if (doNotLoad) {
return 0
}
return item == nil ? data.count : (item as! Node).children.count
}
}
If you'd like to comply with the documentation, do not use nil and set some fake name. But I would expect that once the bug is fixed, the persistent state will be removed if we change the autosaveName or if we set it set to nil.
Second trick - load & expand yourself
Imagine you have the following Node class:
class Node {
let id: Int
let children: [Node]
// ...
}
And your data source implements:
func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? {
(item as! Node).id
}
func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? {
guard let id = object as? Int else { return nil }
return data.firstNode { $0.id == id }
}
The firstNode is not related to this question, but here's the implementation (because it's mentioned in the code):
extension Array where Self.Element == Node {
// Search for a node (recursively) until a matching element is found
func firstNode(where predicate: (Element) throws -> Bool) rethrows -> Element? {
for element in self {
if try predicate(element) {
return element
}
if let matched = try element.children.firstNode(where: predicate) {
return matched
}
}
return nil
}
}
Then you can reloadData & expand all the items by yourself:
outlineView.reloadData()
if outlineView.autosaveExpandedItems,
let autosaveName = outlineView.autosaveName,
let persistentObjects = UserDefaults.standard.array(forKey: "NSOutlineView Items \(autosaveName)"),
let itemIds = persistentObjects as? [Int] {
itemIds.forEach {
let item = outlineView.dataSource?.outlineView?(self.outlineView, itemForPersistentObject: $0)
self.outlineView.expandItem(item)
}
}

Swift unit testing view model interface

As I understand, it is best to only test public methods of a class.
Let's have a look at this example. I have a view model for the view controller.
protocol MyViewModelProtocol {
var items: [SomeItem] { get }
var onInsertItemsAtIndexPaths: (([IndexPath]) -> Void)? { get set }
func viewLoaded()
}
class MyViewModel: MyViewModelProtocol {
func viewLoaded() {
let items = createDetailsCellModels()
updateCellModels(with: items)
requestDetails()
}
}
I want to test class viewLoaded(). This class calls two other methods - updateItems() and requestDetails()
One of the methods sets up the items and the other one call API to retrieve data and update those items. Items array us updated two times and onInsertItemsAtIndexPaths are called two times - when setting up those items and when updating with new data.
I can test whether after calling viewLoaded() expected items are set up and that onInsertItemsAtIndexPaths is called.
However, the test method will become rather complex.
What is your view, should I test those two methods separately or just write this one huge test?
By testing only viewLoaded(), my idea is that the implementation can change and I only care that results are what I expect.
I think the same thing, only public functions should be tested, since public ones use private ones, and your view on MVVM is correct. You can improve it by adding a DataSource and a Mapper that allows you to improve testing.
However, yes, the test seems huge to me, the tests should test simple units and ensure that small parts of the code work well, with the example you show is difficult, you need to divide by layers (clean code).
In the example you load the data into the viewModel and make it difficult to mockup the data. But if you have a Domain layer you can pass the UseCase mock to the viewModel and control the result. If you run a test on your example, the result will also depend on what the endpoint returns. (404, 200, empty array, data with error ...). So it is important, for testing purposes, to have a good separation by layers. (Presentation, Domain and Data) to be able to test each one separately.
I give you an example of how I would test a view mode, sure there are better and cooler examples, but it's an approach.
Here you can see a viewModel
protocol BeersListViewModel: BeersListViewModelInput, BeersListViewModelOutput {}
protocol BeersListViewModelInput {
func viewDidLoad()
func updateView()
func image(url: String?, index: Int) -> Cancellable?
}
protocol BeersListViewModelOutput {
var items: Box<BeersListModel?> { get }
var loadingStatus: Box<LoadingStatus?> { get }
var error: Box<Error?> { get }
}
final class DefaultBeersListViewModel {
private let beersListUseCase: BeersListUseCase
private var beersLoadTask: Cancellable? { willSet { beersLoadTask?.cancel() }}
var items: Box<BeersListModel?> = Box(nil)
var loadingStatus: Box<LoadingStatus?> = Box(.stop)
var error: Box<Error?> = Box(nil)
#discardableResult
init(beersListUseCase: BeersListUseCase) {
self.beersListUseCase = beersListUseCase
}
func viewDidLoad() {
updateView()
}
}
// MARK: Update View
extension DefaultBeersListViewModel: BeersListViewModel {
func updateView() {
self.loadingStatus.value = .start
beersLoadTask = beersListUseCase.execute(completion: { (result) in
switch result {
case .success(let beers):
let beers = beers.map { DefaultBeerModel(beer: $0) }
self.items.value = DefaultBeersListModel(beers: beers)
case .failure(let error):
self.error.value = error
}
self.loadingStatus.value = .stop
})
}
}
// MARK: - Images
extension DefaultBeersListViewModel {
func image(url: String?, index: Int) -> Cancellable? {
guard let url = url else { return nil }
return beersListUseCase.image(with: url, completion: { (result) in
switch result {
case .success(let imageData):
self.items.value?.items?[index].image.value = imageData
case .failure(let error ):
print("image error: \(error)")
}
})
}
}
Here you can see the viewModel test using mocks for the data and view.
class BeerListViewModelTest: XCTestCase {
private enum ErrorMock: Error {
case error
}
class BeersListUseCaseMock: BeersListUseCase {
var error: Error?
var expt: XCTestExpectation?
func execute(completion: #escaping (Result<[BeerEntity], Error>) -> Void) -> Cancellable? {
let beersMock = BeersMock.makeBeerListEntityMock()
if let error = error {
completion(.failure(error))
} else {
completion(.success(beersMock))
}
expt?.fulfill()
return nil
}
func image(with imageUrl: String, completion: #escaping (Result<Data, Error>) -> Void) -> Cancellable? {
return nil
}
}
func testWhenAPIReturnAllData() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "All OK")
beersListUseCaseMock.error = nil
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.items.bind { (_) in}
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNotNil(viewModel.items.value)
XCTAssertNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
func testWhenDataReturnsError() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "Error")
beersListUseCaseMock.error = ErrorMock.error
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNil(viewModel.items.value)
XCTAssertNotNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
}
in this way you can test the view, the business logic and the data separately, in addition to being a code that is very reusable.
Hope this helps you, I have it posted on github in case you need it.
https://github.com/cardona/MVVM

App Crashes Due to Binding to Table Cell View

So I've created an NSOutlineView to display the file & directory list in a hierarchical way. I'm building a BitTorrent client (stating so the class names make sense).
As you can see, this is pretty much how the outline view looks:
The problem is associated with the Name column. In the name column, for each row, I have a checkbox and a text field side by side. This will help you get a clearer idea:
Now, I use bindings to get the value for each textfield. However, since there are 2 views (checkbox and textfield) that needs to bound to the same NSTableCellView, I'm returning a struct, from the data source, containing 2 values: a string for the text field (which holds the file/directory name), and a boolean for enabling/disabling the checkbox.
To handle the outline view (especially its data), I've set its class to TorrentContent, which is defined as below:
import Cocoa
struct Name {
let value: String
let enabled: Bool
}
class TorrentContent: NSOutlineView, NSOutlineViewDelegate, NSOutlineViewDataSource {
var content: [TorrentContentItem]
required init?(coder: NSCoder) {
let srcDir = TorrentContentItem("src")
let mainJava = TorrentContentItem("main.java")
let mainCpp = TorrentContentItem("main.cpp")
srcDir.children.append(mainJava)
srcDir.children.append(mainCpp)
content = [srcDir]
super.init(coder: coder)
delegate = self
dataSource = self
}
func outlineView(_: NSOutlineView, isItemExpandable item: Any) -> Bool {
if let _item = item as? TorrentContentItem {
if _item.children.count > 0 {
return true
} else {
return false
}
} else {
return false
}
}
func outlineView(_: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if item == nil {
return content.count
} else {
if let _item = item as? TorrentContentItem {
return _item.children.count
}
}
return 0
}
func outlineView(_: NSOutlineView, child: Int, ofItem item: Any?) -> Any {
if item != nil {
if let _item = item as? TorrentContentItem {
return _item.children[child]
}
}
return content[child]
}
func outlineView(_: NSOutlineView, objectValueFor col: NSTableColumn?, byItem item: Any?) -> Any? {
if item != nil, col != nil {
if let _item = item as? TorrentContentItem {
switch col!.title {
case "Name":
return Name(value: _item.name, enabled: false)
default:
return nil
}
}
}
return nil
}
}
I've hard-coded the data so it'll be easier for you to understand what's going on.
Focusing only on the name column, here's the part of the above code which deals with that:
func outlineView(_: NSOutlineView, objectValueFor col: NSTableColumn?, byItem item: Any?) -> Any? {
if item != nil, col != nil {
if let _item = item as? TorrentContentItem {
switch col!.title {
case "Name":
return Name(value: _item.name, enabled: false)
default:
return nil
}
}
}
return nil
}
As you can see, it returns the Name struct, which contains values for both the views. I've hard-coded the enabled value to false just for testing purposes.
Now to bind that to the textfield's value property, I've done this:
My logic is that, since objectValue is an instance of the Name struct, objectValue.value should be the value of the Name struct's instance, which is a string.
I want to bind the enabled property of the checkbox in a similar way. However, none of the bindings work. They cause the app to crash. This is what XCode shows me after it crashes everytime I attempt to view the outline view during runtime:
Only got "(lldb)" in the console.
What am I doing wrong, and how do I achieve what I want? That is, setting the property values of multiple views from the data source class.
Cocoa Bindings uses Key Value Observing (KVO) and the observed object must be KVO compatible. See Using Key-Value Observing in Swift.
You can only use key-value observing with classes that inherit from NSObject.
Mark properties that you want to observe through key-value observing with both the #objc attribute and the dynamic modifier.
Solution A: Return a KVO compatble object from outlineView(_:objectValueFor:byItem:)
Solution B: Don't use Cocoa Bindings. Create a subclass of NSTableCellView and add a enabledCheckbox outlet. Set the values in outlineView(_:viewFor:item:).

Reload a NSWindow Xcode Swift2

I'm working on an NSOutlineView that uses NSView subclasses to generate custom cells in the outline. This I've gotten to work, BUT after the Outline sucks in the data from the model class and displays it correctly, the Outline is released(?) from memory / goes to nil and I haven't figured out a way to get it back.
Here is the MainViewController class
class MainWindowController: NSWindowController, ShareInfoDelegate, NSOutlineViewDelegate, NSOutlineViewDataSource {
override var windowNibName: String {
return "MainWindowController"
}
#IBOutlet var daOutline: NSOutlineView!
// The NSoutline I'm trying to get back to
Some stuff related to the test data (Omitted)
leading us to the NSOutlineViewDataSource stuff
//MARK: - NSOutlineViewDataSource
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
if let item: AnyObject = item {
switch item {
case let work as Work:
return work.movements[index]
case let movement as Movement:
return movement.tracks[index]
default:
let track = item as! Track
return track.credits[index]
}
} else {
if allWorks.count > 0 {
return allWorks[index]
}
}
let q = "patience"
return q
}
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
switch item {
case let work as Work:
return (work.movements.count > 0) ? true : false
case let movement as Movement:
return (movement.tracks.count > 0) ? true : false
case let track as Track:
return (track.credits.count > 0) ? true: false
default:
return false
}
}
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
if let item: AnyObject = item {
switch item {
case let work as Work:
return work.movements.count
case let movement as Movement:
return movement.tracks.count
case let track as Track:
return track.credits.count
default:
return 0
}
} else {
return allWorks.count
}
}
func outlineView(daOutline: NSOutlineView, viewForTableColumn theColumn: NSTableColumn?, item: AnyObject) -> NSView? {
switch item {
case let worked as Work:
let cell = daOutline.makeViewWithIdentifier("newTry", owner:self) as! newTry
cell.fourthLabel.stringValue = worked.composer
cell.fourthCell.stringValue = worked.title
return cell
case let moved as Movement:
let cell2 = daOutline.makeViewWithIdentifier("SecondTry", owner:self) as! SecondTry
cell2.roman.stringValue = moved.name!
cell2.details.stringValue = moved.sections!
cell2.track.stringValue = "0"
return cell2
default:
print("probably not")
}
print("not again")
return nil
}
func outlineView(daOutline: NSOutlineView, heightOfRowByItem item: AnyObject) -> CGFloat {
switch item {
case let worked as Work:
return 40
default:
return 24
}
}
And the stuff in WindowDidLoad
override func windowDidLoad() {
super.windowDidLoad()
let nib = NSNib(nibNamed: "newTry", bundle: NSBundle.mainBundle())
daOutline.registerNib(nib!, forIdentifier: "newTry")
let nib2 = NSNib(nibNamed: "SecondTry", bundle: NSBundle.mainBundle())
daOutline.registerNib(nib2!, forIdentifier: "SecondTry")
//give Sender it's Receiver
mailItOut.delegate = receiver
allWorks.append(work1)
allWorks.append(work2)
work1.movements.append(move1)
work1.movements.append(move2)
work1.movements.append(move3)
work1.movements.append(move4)
work2.movements.append(move5)
work2.movements.append(move6)
work2.movements.append(move7)
daOutline.reloadData()
daOutline?.expandItem(work1, expandChildren: false)
daOutline?.expandItem(work2, expandChildren: false)
}
}
And Finally what the newTry NSView class looks like
class newTry: NSView {
var delegate: ShareInfoDelegate?
#IBOutlet weak var fourthCell: NSTextField!
#IBOutlet weak var fourthLabel: NSTextField!
#IBAction func cellAdd(sender: NSTextField) {
var catchIt: String = String()
catchIt = sender.stringValue
if catchIt != "" {
tryAgain = catchIt
whichField = "title"
//Trigger the sender to send message to it's Receiver
mailItOut.sendMessage()
}
}
The cellAdd Action is used to try and get user input from the text cells back into the model. To do this I (AFAIK) need to access the NSOutline (daOutline) and get which row I'm at and put the data from the sender into the appropriate part of the Model class. Which is something that I've managed to get to work in a standard (1 cell / 1 data value) outline. But in this prototype, as far as I can tell, the MainWindowController has released all of its contents and daOutline is nil (bad).
How do I get XCode to bring / reload the completed outline (or never release it) and get daOutline to a non nil state?
For those who come after there appeared to be two problems that led to the NSOutline outlet becoming nil. The first one was that in implementing the delegate protocol "shareInfoDelegate" I was creating a new instance of the MainWindowController, not the one with the data in it. This new instance did NOT have the IBOutlets connected (or much of anything useful about it).
Once I scrapped the Delegate and moved to using NSNotification to update information about the NSView textFields my NSOutline came "back".
The second, more minor, problem was that in the NSView nib file I placed and NSBox to mimic the behavior of a group row (e.g. a gray background). As a side effect the NSBox was inhibiting the normal row select behavior of the outline. Which made it very hard to determine which row was selected. When I deleted the NSBox, row selection became much more easy to determine.
in particular this Question and the answer by Chuck were helpful in sniffing this out.
Why is my NSOutlineView datasource nil?
Thanks Indeed(!)