2

I would like to have the following functionality in my app:

When I type the DNA sequence (string) in the NSTextView window at the same time in my TableView for each enzyme (each of them representing small string) user immediately see the number of found sites (string) corresponding to each enzyme (0 or any number).

I have a function, which I can use to find all possible locations (returning NSRanges array) of string in string. In my case this will be to find in DNA sequence (string) all possible sites (strings NSRanges) corresponding for each enzyme.

Thus, one more time, question is how to implement this function: at the time of typing a string to find all sites (in form of array of NSRanges) in this string and put the numbers found site in table accordingly for each enzyme.

In other words, the function returning NSRanges array for positions of enzymes sites should start automatically.

Update

I am new in cocoa and after suggestions from R Menke (I have putted his code lines below in the code) I have more probably stupid questions. I have one controller class as subclass of NSWindowController. I cannot put code from R Menke to this class (see errors below). And, in my controller class I have my NSTextView where user will type the text as @IBOutlet, should I use this? Should I make another controller file ? Below the code and errors.

import Cocoa
//Error. Multiple inheritance from classes 'NSWindowController' and   'NSViewController'
class AllUnderControl: NSWindowController, NSViewController,NSTextViewDelegate
{
override var windowNibName: String?
{
return "AllUnderControl"
}

override func windowDidLoad() {
    super.windowDidLoad()
}
 //Error. Instance member 'inputDnaFromUser' cannot be used on type 'AllUnderControl'
var textView = inputDnaFromUser(frame: CGRectZero)

 //Error. Method does not override any method from its superclass
override func viewDidLoad() {
    textView.delegate = self
}

func textDidChange(notification: NSNotification) {
    // trigger your function
}

@IBOutlet var inputDnaFromUser: NSTextView!

Update 2 After reading the description of two controllers: NSWindowController and NSViewController I have made the following changes below. Is it correct for triggering function ?

import Cocoa

class AllUnderControl: NSWindowController, NSTextViewDelegate
{
override var windowNibName: String?
{
return "AllUnderControl"
}

override func windowDidLoad() {
    super.windowDidLoad()
    inputDnaFromUser.delegate = self
}

func textDidChange(notification: NSNotification) {

    // trigger your function
}

@IBOutlet var inputDnaFromUser: NSTextView! = NSTextView(frame: CGRectZero)
VYT
  • 1,071
  • 19
  • 35
  • Can you please show what your current attempt looks like, and what's not working about it? – Aaron Brager Oct 01 '15 at 14:33
  • Problem is I don't know how it should look like. I can find all positions for any site (enzyme) in dna sequence when I have dna sequence and click button to perform action, but I have no idea how to do this search to get NSRange array during the typing the dna sequence. – VYT Oct 01 '15 at 14:38
  • How do you store all of the possible substrings (enzymes)? Is that a pre-defined array you've created? – Russell Oct 01 '15 at 14:57
  • I am going to use predefined dictionary with names of enzymes(string) and corresponding sites(string) – VYT Oct 01 '15 at 15:01
  • 1
    inputDnaFromUser is an IBOutlet so I assume it is connected in a XIB. Assigning a view makes no sense. – Willeke Oct 02 '15 at 13:33
  • 1
    inputDnaFromUser's delegate can also be connected in the XIB. – Willeke Oct 02 '15 at 13:45
  • @VYT [IB tutorial](https://www.youtube.com/watch?v=iJHskQ27gVM) [Programmatically created tutorial](https://www.youtube.com/watch?v=5y5did8Nyzo) These are not exactly about your code. But just an example. Google is your best friend – R Menke Oct 02 '15 at 18:41
  • Thanks. Yes, a good friend if know exactly what to ask :). – VYT Oct 02 '15 at 18:47
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/91206/discussion-between-r-menke-and-vyt). – R Menke Oct 02 '15 at 20:26
  • @VYT I noticed that you answered a lot of your own questions with updated code. Don't do that when there is already a valid answer. Just update your question with the fixed code, mark it "Fix" in bold, but this is not even needed if the answer is really clear. Also please accept the answers people give you. Even if you did not get it to work because of other problems, you should still reward people for the time they took to help you. Please do this : [Take the tour!](http://stackoverflow.com/tour) – R Menke Oct 03 '15 at 03:10

2 Answers2

0

If you are asking: "how do I trigger a function when someone types in an NSTextView?"

Implement an NSTextViewDelegate set the delegate of your NSTextView to self and trigger your function inside textDidChange. The NSTextViewDelegate has a set of functions that will be triggered by user interaction. So code inside of them will be executed when the corresponding action happens.

I would suggest using an NSViewController for this and not an NSWindowController. A NSWindowController is used to manage NSViewControllers. NSViewControllers are a better place for things like buttons and textfields.

NSWindowController vs NSViewController

class ViewController: NSViewController, NSTextViewDelegate {


    @IBOutlet var inputDnaFromUser: NSTextView!


    override func viewDidLoad() {
        super.viewDidLoad()

        inputDnaFromUser.delegate = self

    }

    override var representedObject: AnyObject? {
        didSet {
        // Update the view, if already loaded.
        }
    }

    func textDidChange(notification: NSNotification) {
        print("editing stuff")
    }
}

If you are asking: "how can I find all occurrences of a string in another string?"

This will return an array of ranges. The count of that array is obviously the number of occurrences.

Another option is to use enumerateSubstringsInRange as stated in the answer by @Russel. I just always preferred to write my own loop.

let string = "The sky is blue today, super blue"
let searchString = "blue"

var ranges: [NSRange] = []

var copyString = NSMutableString(string: string)

while copyString.containsString(searchString) {
    ranges.append(copyString.rangeOfString(searchString))

    guard let lastRange = ranges.last else {
        break
    }
    var replaceString = ""
    for _ in 0..<lastRange.length { replaceString += "$" } // unnalowed character

    copyString.replaceCharactersInRange(lastRange, withString: replaceString)

}

As suggested in the comments: A faster method.

let string : NSString = "The sky is blue today, super blue"
let searchString = "blue"

var ranges: [NSRange] = []

var searchRange : NSRange = NSRange(location: 0, length: string.length)
var lastFoundRange : NSRange = string.rangeOfString(searchString, options: NSStringCompareOptions.LiteralSearch, range: searchRange)

while lastFoundRange.location != NSNotFound {

    ranges.append(lastFoundRange)

    let searchRangeLocation = lastFoundRange.location + lastFoundRange.length
    let searchRangeLength = string.length - searchRangeLocation
    searchRange = NSRange(location: searchRangeLocation, length: searchRangeLength)

    lastFoundRange = string.rangeOfString(searchString, options: NSStringCompareOptions.LiteralSearch, range: searchRange)
}

You will be wanting to do this on a background queue. But this gets tricky quickly. An enzyme can be one kind one moment and change the next. So you will need to do all the work with every character typed.

One possible solution is to cancel each ongoing search when a character is typed. If it finished before typing the next character you get results.

This is an iOS word highlighter I wrote that implements this logic. Except for the use of UIColor everything is pure Foundation. So easy to change it to Cocoa.

Community
  • 1
  • 1
R Menke
  • 8,183
  • 4
  • 35
  • 63
  • Thanks! This is very useful ! – VYT Oct 01 '15 at 17:18
  • I am new in Cocoa, sorry for stupid questions. I am lost. I have one controller class as subclass of NSWindowController. I cannot put your code above to this class. And, in my controller class I have my NSTextView where user will type the text as @IBOutlet, should I use this? Should I make another controller file ? – VYT Oct 01 '15 at 19:23
  • isn't it faster to do rangeOfString:options:range: in a loop? – Willeke Oct 01 '15 at 23:58
  • If rangeOfString:options:range: crashes you're doing something wrong. You end the loop when it returns location NSNotFound. – Willeke Oct 02 '15 at 13:30
  • @Willeke I was doing something wrong. Looked at it in more detail. I wasn't setting the searchRange correctly. Wasn't reducing the search length, only the location after each pass. Thx! – R Menke Oct 02 '15 at 14:23
0

You will want to implement the UISearchResultsUpdating protocol to achieve this. It uses a UISearchController (introduced in iOS 8) which has to be added programmatically instead of through the storyboard, but don't worry, it's pretty straight-forward.

The searching is handled using the updateSearchResultsForSearchController delegate method which is called anytime the search bar text is changed. I tried to keep it pretty self-documenting but let me know if you have any questions. Depending on how many enzymes you care to search, this could get inefficient very quickly because you have to search for substring occurrences for each enzyme.

Cheers, Russell

class YourTableViewController: UITableViewController, UISearchBarDelegate, UISearchResultsUpdating {
    // Array of searchable enzymes
    var enzymes: [String] = ["...", "...", "..."]

    // Dictionary of enzymes mapping to an array of NSRange 
    var enzymeSites: [String : [NSRange]] = [String : [NSRange]]()

    // Search controller
    var enzymeSearchController = UISearchController()

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()

        self.enzymeSearchController = UISearchController(searchResultsController: nil)
        self.enzymeSearchController.dimsBackgroundDuringPresentation = true

        // This is used for dynamic search results updating while the user types
        // Requires UISearchResultsUpdating delegate
        self.enzymeSearchController.searchResultsUpdater = self

        // Configure the search controller's search bar
        self.enzymeSearchController.searchBar.placeholder = "Enter DNA sequence"
        self.enzymeSearchController.searchBar.sizeToFit()
        self.enzymeSearchController.searchBar.delegate = self
        self.definesPresentationContext = true

        // Set the search controller to the header of the table
        self.tableView.tableHeaderView = self.enzymeSearchController.searchBar
    }

    // MARK: - Search Logic

    func searchEnzymeSites(searchString: String) {

        // Search through all of the enzymes
        for enzyme in enzymes {
            // See logic from here: https://stackoverflow.com/questions/27040924/nsrange-from-swift-range
            let nsEnzyme = searchString as NSString
            let enzymeRange = NSMakeRange(0, nsEnzyme.length)

            nsEnzyme.enumerateSubstringsInRange(enzymeRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in

                if (substring == enzyme) {
                    // Update the enzymeSites dictionary by appending to the range array
                    enzymeSites[enzyme]?.append(substringRange)
                }
            })
        }
    }

    // MARK: - Search Bar Delegate Methods

    func searchBarSearchButtonClicked(searchBar: UISearchBar) {

        // Force search if user pushes button
        let searchString: String = searchBar.text.lowercaseString
        if (searchString != "") {
            searchEnzymeSites(searchString)
        }
    }

    func searchBarCancelButtonClicked(searchBar: UISearchBar) {

        // Clear any search criteria
        searchBar.text = ""

        // Force reload of table data from normal data source
    }

    // MARK: - UISearchResultsUpdating Methods

    // This function is used along with UISearchResultsUpdating for dynamic search results processing
    // Called anytime the search bar text is changed
    func updateSearchResultsForSearchController(searchController: UISearchController) {

        let searchString: String = searchController.searchBar.text.lowercaseString
        if (searchString != "") {
            searchEnzymeSites(searchString)
        }
    }

    // MARK: - Table view data source

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if (self.enzymeSearchController.active) {
            return self.enzymeSites.count
        } else {
            // return whatever your normal data source is
        }
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCellWithIdentifier("userCell") as! UserCell

        if (self.enzymeSearchController.active && self.enzymeSites.count > indexPath.row) {
            // bind data to the enzymeSites cell
        } else {
            // bind data from your normal data source
        }

        return cell
    }

    // MARK: - UITableViewDelegate

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath, animated: true)

        if (self.enzymeSearchController.active && self.searchUsers.count > 0) {
            // Segue or whatever you want
        } else {
            // normal data source selection
        }
    }
}
Russell
  • 3,099
  • 2
  • 14
  • 18
  • OP seems to be looking for an answer for a Cocoa app, since the OP is referring to `NSTextView` – imas145 Oct 01 '15 at 16:13
  • I am writing for OSX, anyway Thanks ! – VYT Oct 01 '15 at 16:15
  • Ah, good point. I'm an iOS guy myself so I didn't even think about the Cocoa app side. If nothing else, you can extract some useful logic for the enzyme dictionary and searching through the array of enzymes. I hope here's something useful for ya! – Russell Oct 01 '15 at 16:16