Multiple Word, Random Order Search – CoreData & SwiftUI

Today I worked on the search functionality for the upcoming Product Management app. Given that this app will contain lots of long notes, having good search is essential.

Screen Shot 2021 08 27 at 5 06 07 PM

In most cases & for most apps, a simple one to one comparison will do the job great -like in the above image-. Here’s how a predicate implementation one to one could look like. Lets say you have a list of names in Core Data and you want to search for John in them:

let pred = NSPredicate(format: “name CONTAINS[cd] %@”, searchTerm)

Great! Well… great for the use case mentioned, but what if instead of names you have long notes and the search words could be out of order? Not. So. Great…

Lets imagine the following two notes:

1. “Today John works at the factory, he delivered 10 new bottles”

2. “We’re not sure what we’ll do with John this weekend, maybe we’ll work on the paper due next week”

The NSPredicate we typed above would match the former but totally miss the latter. That’s because in the first line text contains the combination “John works” one to one (in that precise order) while in the second line the text does not. 

In the second example words are out of order so our predicate misses them. To solve this, let me introduce you to NSCompoundPredicate

NSCompoundPredicate allows you to combine multiple predicates in one array that is searched in one CoreData fetch, making it hugely powerful. 

Here’s a swiftUI implementation that would handle the out of order use-case:

    .onChange(of: searchableText) { searchableText in

        var predicateList: Array<NSPredicate> = []

        if !searchableText.isEmpty {

            for iterator in searchableText.components(separatedBy: ” “){

                if iterator == “”{

                    continue

                }

                let pred = NSPredicate(format: “text CONTAINS[cd] %@”, iterator)

                predicateList.append(pred)

            }

            someElement.nsPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicateList)

        }

    }

In this example I take the search query, split it in words and create a predicate for each. As long as the note contains the words, does not matter the order, it’ll find the right match. 

NOTE: If you don’t know what’s [c] and [d], that’s a way to tell NSPredicate it should not care about capitalization nor diacritics for the given keyword, in our case ‘CONTAINS’. 

That’s it, nice and easy. 

Happy Friday, 

Marc

Counting CoreData entries based on relationship entities

As I continue working on the new product management app, I was trying to get a quick CoreData count. To be precise,  I needed to get the count for how many of its relationships met a given criteria. There’s quite a few approaches but none as clean as “the filter”

Screen Shot 2021 08 22 at 4 49 59 PM

Using it for boolean arguments:

      entity.childOfEntity?.filter({ ($0 as! entityType).booleanAgument != true}).count

Using the image example above:

      noteObject.tasksOfNote?.filter({ ($0 as! Task).completed != true}).count

Using it for string arguments:

      entity.childOfEntity?.filter({ ($0 as! entityType).stringArgument != ”Something}).count

Definitely recommended it for most CoreData relationship work, simplifies things quite a bit. 

 

Marc

Developing on the cutting edge

If you’re working on a new app using the latest Xcode & macOS Monterey betas be aware of a bug that leads to SceneStorage failures appearing on the console:

Failed to restore SceneStorage with key selectedTaskId. at SwiftUI/SceneStorage.swift:105
[…]
Failed to restore SceneStorage with key sidebarSelection. at SwiftUI/SceneStorage.swift:105
Binary[2571:23261] Checked for defaults IMKUseDistributedObjects. result is 0
Binary[2571:23261] IMK will use XPC for App = com.Binary.Binary, euid=501
Binary[2571:23261] IMKInputSession [0x6000004e4a20 activate] Invoking activateServerWithReply:, from Main thread = 1, bundleID=com.apple.PressAndHold

It’s not you, it’s the Beta. Same if after updating to 12B3 you start getting EXC_BAD_ACCESS crashes, I fixed that by updating to XcodeB3.  

Screen Shot 2021 07 18 at 10 30 54 PM

Working with the latest betas is a double edged sword 🙂 

Happy Sunday, 

Marc

What Ever Happened to IBM’s Watson

Very interesting article from the NY Times on the ‘downfall’ of IBM’s Watson. 

Iu

Maps quite well with what Steve Jobs said many years ago:

“I have my own theory about why the decline happens at companies like IBM or Microsoft. The company does a great job, innovates and becomes a monopoly or close to it in some field, and then the quality of the product becomes less important. The product starts valuing the great salesmen, because they’re the ones who can move the needle on revenues, not the product engineers and designers. So the salespeople end up running the company.”

Read it here

Rich Barton: Expedia & Zillow

“How I built this” is a podcast where successful entrepreneurs are interviewed in a very candid way. The ups and downs, the complexities, the long nights… 

Today I listened to Rich Barton, the creator of Expedia, Zillow and GlassDoor. Both the show, and this specific episode are great: https://overcast.fm/+YsraNZ6eo

Iu

On a related note, here’s some of the improvement Zillow has done lately when it comes to AI: https://www.wired.com/story/zillow-taps-ai-improve-home-value-estimates/

 

Marc

Identifying combinations of dates in text using regex

For the upcoming app I’m working on I need a way to easily detect dates the user may have typed at the end of a line. It was time to re-visit old faithful regex

Ewrsit7pbw671

After lots of readings and tests I ended up settling on this beauty: 

     “.*[0-9]{2}/[0-9]{2}”

Here’s what it does:

The first section means take any character from the beginning of a line:

     .  Any character except new line

     * Zero or more charecters

provided it ends in the second part…

     [0-9]{2} Two numbers between 0 to 9

     /  This specific seperator

     [0-9]{2} Two numbers between 0 to 9

So in essence, it will take any line that ends in xx/xx where xx is any combination of two digits. That’s it, clear and simple!

***

Now, all we have to do is expand the number of cases to account for the user typing only one digit for the month or day.  

Here’s a swift array containing each case: [“.*[0-9]{2}/[0-9]{2}”,”.*[0-9]{1}/[0-9]{2}”,”.*[0-9]{2}/[0-9]{1}”,”.*[0-9]{1}/[0-9]{1}”]

One thing I’d point out is that the order of the regex pattern matters, it should go from most complex to most simple as regex will stop once it finds a suitable match. 

Here’s an example of all of it working together (in this case I’m also adding a year pattern):

func dateMatches(text: String) -> [String] {

    let regexPatternMonth = [“.*[0-9]{2}/[0-9]{2}”,“.*[0-9]{1}/[0-9]{2}”,“.*[0-9]{2}/[0-9]{1}”,“.*[0-9]{1}/[0-9]{1}”]

    let regexPatternYear = [“.*[0-9]{2}/[0-9]{2}/[0-9]{4}”,“.*[0-9]{1}/[0-9]{2}/[0-9]{4}”,“.*[0-9]{2}/[0-9]{1}/[0-9]{4}”,“.*[0-9]{1}/[0-9]{1}/[0-9]{4}”,“.*[0-9]{2}/[0-9]{2}/[0-9]{2}”,“.*[0-9]{1}/[0-9]{2}/[0-9]{2}”,“.*[0-9]{2}/[0-9]{1}/[0-9]{2}”,“.*[0-9]{1}/[0-9]{1}/[0-9]{2}”]

    

    

    let regexPatternsToMatchArray = regexPatternMonth + regexPatternYear

    do {

        let regex = try NSRegularExpression(pattern: “(\(regexPatternsToMatchArray.joined(separator:“|”)))”)

        let results = regex.matches(in: text,

                                range: NSRange(text.startIndex…, in: text))

        let finalResult = results.map {

            String(text[Range($0.range, in: text)!])

        }

        return finalResult

    } catch let error {

        print(“invalid regex: \(error.localizedDescription))

        return []

       }

}

That’s it, you can call this function and it will return the array of matching sub-strings.

From there you can process convert them into dates (make sure to account for localization as some countries use day/month and others month/day). 

Hope this helps, if you’re new to regex here are some useful references: regex cheatsheet, validatormatching valid dates and ios regex.

Comments, questions I’m at @MarcMasVi on Twitter

Marc

Struggling with a basic macOS SideBar, RowList & DetailView app in SwiftUI

Its amazing how fast you can prototype in SwiftUI, it seems like magic. At the same time, when you start to get into the details, it can be infuriating… 

Let me explain…

After completing the prototype last week and deciding on the key features for the app  I started implementing them. One of the very basic ones was having a button on the side bar to create new content. Well, basic in AppKit/UIKit, with SwiftUI on macOS not so much!

Here’s what I was trying to do:

Screen Shot 2021 06 27 at 8 04 11 PM

1. From the SideView (the column at the very left in the image above) you would be able to click on a Button that would create a new element.

2. This new element would appear as a new row (see above Added Content in the middle view) and it would become automatically selected.

3. The detail view, the one that shows the detail of the element, would automatically show all contents (see the rightmost view above).  

In AppKit this would be trivial, not so much in SwiftUI. Also, because most of the recommendations out there are for iOS and not for macOS, it was quite a fun ride. 

Here’s how I ended up doing it:

First, the SideView:

@SceneStorage(“sidebarSelection”) var selection: String?

@SceneStorage(“selectedElementId”) var selectedElementId: Int?

 

var body: some View {

    

    NavigationView{

        List(selection: $selection) {

            

            Button(action: {

                let addedElement = coreDataAddsElementHere()

                selectedElementId = Int(addedElement.id)

            }) {

                Text(“+ New Element”)

            }

            .foregroundColor(.blue)

 

            Label(“Category 1”, systemImage: “someIcon”).tag(tagThatWillBeUsedForSelection)

            Label(“Category 2”, systemImage: “someIcon”).tag(tagThatWillBeUsedForSelection2)

            Label(“Category 3”, systemImage: “someIcon”).tag(tagThatWillBeUsedForSelection3)

        }

            ElementList(typeOfList: selection ?? defaultOptionGoesHere)

    }

}

You’ll see I’m not using NavigationLink here, if you’re building a macOS app you should only use that for the RowList. To share information between the SideView and the RowList you should use SceneStorage.

Selection is used, as its name implies, to enable the selection of rows in combination with the tag property on each label: when both selection and tag match, the relevant row in the sideBar is selected.  

Second, the RowList:

@SceneStorage(“selectedElementId”) var selectedElementId: Int?

 

var body: some View {

    NavigationView{

        List(){

            ForEach(elements){element in

                NavigationLink(

                    destination: DetailView(contentsOfView: element),

                    tag: Int(element.id),

                    selection: $selectedElementId

                ) {

                    ElementListRow(title: firstLine, subtitle: otherContent, isPinned: element.pinned)

                }

                

            

                //Default View on Mac

                DetailView(contentsOfView: nil)

            }

        

        

        }

    }

}

You’ll see we capture the shared selectedElementId, which allows us to select the right row automatically (note the tag to the element id and the selection binding).

Here we can use the NavigationLink as we’re dealing with a row to detail view model. 

And finally, the detail view (optional):

@SceneStorage(“selectedElementId”) var selectedElementId: Int?

 

 

var body: some View {

 

    if selectedElementId == nil{

        Text(“Please select an element or create a new one”)

    }else{

        […]

    }

}

I use this specifically to make sure when a Core Data object is delated, if it was being shown in the detail view, I can invalidate the view. As before, I leverage the selectedElementId: if the selected element is deleted from any view, that SceneStorage id will be set to nil. The detail view detects it and immediately stops showing the cached data. 

***

And that’s it! It’s quite streightforward once you know how to do it, unfortunately most information out there is for iOS so I found myself spending way more time than anticipated. 

Hope it helps others, if you have suggestions on how to do this better please do let me know!

Do reach me @MarcMasVi on Twitter or marc.maset@hey.com

Marc

 

 

 

 

What differentiates good from great Project & Product Managers?

Although this blog is entirely focused on my developer-side, this is a part-time (nights and weekends) enterprise. My ‘main’ job is not in development, it has quite a bit to do with product management.

**I promise you this is going somewhere**

Over many years, in many roles, the one thing that I’ve seen consistently differentiate good Product & Project Managers from great ones is their ability to keep tabs on everything.

 

Great Product & Project Managers:

– Know the strategic intent of every Product Feature / Project.

– Remember the contents and agreements of every discussion.

– Timely act on any tasks they have, or follow up with others if a task is due.

– Know every single person involved and what makes them click.

– Are able to drive on many different initiatives in parallel without dropping a beat.

 

It seems magical, it seems like they have superpowers… And upon closer inspection, they do: they have a model they relentlessly follow.

 

To enable the model:

– Some use a notes app (Evernote, OneNote, Apple Notes…). 

– Others use a notebook / notes (NotePad++, vim/emacs, a real notebook…). 

– A majority uses a combination of ToDo app and a notes app (Evernote + Microsoft To Do, OneNote + Things, Apple Notes + OmniFocus…).

 

No one I’ve talked to seems to be satisfied with the apps they use. It’s not the app’s fault, they are general purpose apps after all.

So… What if we turn this around and create an app build specifically to enable great Project & Product managers?

That’s exactly what the next app I’ll be working on. And will be doing so in the open.

Screen Shot 2021 06 18 at 11 19 52 AM

More to come soon,

Marc