scrollRowToVisible not working in windowDidLoad

Everything works except the default voice being scrolled into view when the app launches.
The proper voice is selected and changes when I change the system default.

If I add a test button and call scrollRowToVisible after the app is running, that works ok.

When the app launches, the voices are always scrolled to the place they were when the app quit.

I’m assuming there is some autosave of scroll position that is taking precedence to the scrollRowToVisible in windowDidLoad.
I’ve looked hard for a setting somewhere but didn’t find anything.

How can this be made to work properly?

It works for me. I set the default system voice to Yelda. When I launch the app it scrolls to Yelda. If I change the voice, in app, to another voice and then quit the app, it scrolls back to Yelda when I relaunch the app.

Have you tried the code supplied for this chapter? Does that work?

EDIT: I should add that I had to wait three minutes for the Yelda voice to download. Yes, three minutes. I presume that if I had run the app before it completed it would have scrolled to my previous default, Daniel.

I get the same results when I use the project downloaded from github.

It seems to work the very first time you launch the app from a new project.
But then scroll so the default is not visible, then quit and launch the app again.
On subsequent relaunching the table is always where it is left when the app was exited.

I also tested by using finder to launch the app as opposed to xcode. Same deal.

How are you quitting the app?

Here are the steps I’ve followed:
(1) scroll elsewhere in the table,
(2) select a different voice
(3) select Quit SpeakLine from the app menu
(4) relaunch the app

When I follow these steps and the app relaunches, the default voice is visible in the table (it has been scrolled to).

One thing you can do is disable state restoration for the window. In MainWindowController.xib, select the window; in the Attributes Inspector, uncheck Restorable.

It behaves the same whether I quit from xcode or the main menu.

If you select a voice and quit with the selection showing it will be showing on the next launch. My point was that the code says the selected row should be visible on launch each and every time. However, if it was not visible when you quit, it would not become visible when you relaunch.

It turns out that the answer I was looking for was the Restorable attribute. If that is unchecked, the selected voice is always scrolled to on launch.
The implication is that the window’s restoration code is run after windowDidLoad.

Further investigation shows that is indeed the case. To reap the benefits of restoration and have the table scrolled to the selected voice requires implementing a NSWindowDelegate method: didDecodeRestorableState. I moved the voice setting code there but it did not initially work. That call is apparently not on the main thread, so I had to dispatch the code to the main thread to get it to work properly.

func window(window: NSWindow, didDecodeRestorableState state: NSCoder) { dispatch_async(dispatch_get_main_queue()) { let defaultVoice = self.preferenceManager.activeVoice! if let defaultRow = find(self.voices, defaultVoice) { let indices = NSIndexSet(index: defaultRow) self.tableView.selectRowIndexes(indices, byExtendingSelection: false) self.tableView.scrollRowToVisible(defaultRow) } } }

Restoration happens on a background thread? That would explain my observations: the default voice is selected; sometimes the selection is visible, but not always. I hadn’t discerned any deeper pattern to it.

“If you find yourself writing many lines of code to do something ordinary…”

To be fair, there is only one extra line of code

and the conceptual challenge of background threads to swallow.

I’ve dabbled with background threads, so I have no problem understanding this. I doubt many newbies would grok it, but I’m willing to guess that 75% of the book’s audience are seasoned Objective-C programmers who will have no problem with it. And the code does work.

But it is not elegant. So far, (I’ve completed 7 chapters) the code in the book has been easy to understand and even elegant. So this solution seems out of place. If this is the only solution, so be it; surely, there is a better way?

I had the same problem as OP. I solved it slightly differently by using an NSTimer to schedule an update shortly after the window loaded.

In windowDidLoad I added this at the end

        NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(showSelectedRow(_:)), userInfo: nil, repeats: false)

and elsewhere in my controller I defined showSelectedRow(_:slight_smile:

    func showSelectedRow(sender: AnyObject) {

        let selectedRow = tableView.selectedRow
        if selectedRow == -1 {
            return
        }
        tableView.scrollRowToVisible(selectedRow)
    }

Note: After reading this thread I also turned off the Restorable property of the Window and commented out my timer code. That resolved the issue as well.

Here’s an update for Swift 3:

func window(_ window: NSWindow, didDecodeRestorableState state: NSCoder) {
        DispatchQueue.main.async {
            [unowned self] in
            let defaultVoice = NSSpeechSynthesizer.defaultVoice()
            if let defaultRow = self.voices.index(of: defaultVoice) {
                let indices = IndexSet(integer: defaultRow)
                self.tableView.selectRowIndexes(indices, byExtendingSelection: false)
                self.tableView.scrollRowToVisible(defaultRow)
            }
        }
    }

It isn’t necessary to use the delegate method window(_,state:). You can put the call to the dispatch manager right in the windowDidLoad() method:

 override func windowDidLoad() {
    super.windowDidLoad()
    updateButtons()
    speechSynth.delegate = self
//    for voice in voices {
//      print(voiceNameForIdentifier(identifier: voice)!)
//    }
    let defaultVoice = NSSpeechSynthesizer.defaultVoice
    DispatchQueue.main.async {
      if let defaultRow = self.voices.index(of: defaultVoice) {
        let indices = IndexSet.init(integer: defaultRow)
        self.tableView.selectRowIndexes(indices, byExtendingSelection: false)
        self.tableView.scrollRowToVisible(defaultRow)
      }
    }
  }