Xcode's Find Navigator & Search Scopes

Xcode’s built-in search and replace function, present in the find navigator is severely underrated and unknown to many developers. It is very powerful and comes with various options that may look complex at first, but the nice and simple UI should help in getting started with it. Let’s take a quick look at what the Xcode’s find navigator has to offer.

This is how the find navigator presents itself in the 4th tab on the left sidebar of Xcode (you can also use the ⌘ + 4 or the more common ⌘ + ⇧ + F shortcuts). As you can see, there’s a lot going on in this simple view. At first you may only see a text field and that’s it, but there are actually 6 customizable interactive buttons in this view alone. By pressing on “Find”, a popover with another option will appear. Instead of the standard search, you can switch to a more powerful “Find and Replace”. You can type the text that you’d like to find, and the text that you want to replace your match with. Using the “Replace” and “Replace All” buttons you’ll be able to replace all instances found or proceed more carefully by replacing instances one-by-one. Surprisingly, also the text “Text” is a button and will show different options based on what type of search you’re currently in (Find or Replace). You can search for a simple string, for a symbol reference or definition (where a specific symbol is being used or defined), using a regex or searching the call hierarchy of a method, property or function. Depending on the second option selected, a third column may be available allowing you to make your search even more powerful. The magnifying glass on the left is kind of a miscellaneous button. Upon pressing, a recent history of your search terms will appear, along an interesting “Insert Pattern” feature which allows you to create a regex-like search term without having to write a regex from scratch. The feature I like the most is actually a simple find and replace combined with a regex. You can do complex refactors by using lookup groups like in the following example: You can refer to the matched group in the regex with $1, $2 and apply any modification you would like. You can imagine how useful running regexes over your whole project could be when doing big refactors (pro tip: use regexr to create and validate your regexes).

Pressing on “In Project” will reveal all available scopes that you can perform a search on. You can create your own scopes pretty easily with the helpful UI that guides you through adding all filters needed. For example, I created multiple scopes to search the project by file type. These scopes actually persist across closing and reopening of Xcode, so now you may wonder where are these scopes actually stored?

The search scopes are stored in a file named IDEFindNavigatorScopes.plist . This file can be found either in ~/Library/Developer/Xcode/UserData/ or in the xcuserdata of your workspace, such as FindNavigator.xcodeproj/project.xcworkspace/xcuserdata/patrickbalestra.xcuserdatad/.

Knowing the location of the file, you can reverse engineer the format of this property list after creating a scope in Xcode. I wanted to automatically create a search scope for my repo that would search all files with a specific extension in a directory. After modifying the search scope in Xcode to look for “swift” files in my project root directory instead of my project, I opened ~/Library/Developer/Xcode/UserData/IDEFindNavigatorScopes.plist.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
	<dict>
		<key>name</key>
		<string>Swift</string>
		<key>predicate</key>
		<dict>
			<key>predicates</key>
			<array>
				<dict>
					<key>operand</key>
					<string>swift</string>
					<key>operator</key>
					<string>is-equal-to</string>
					<key>rule</key>
					<string>file-extension</string>
				</dict>
			</array>
			<key>rule</key>
			<string>all</string>
		</dict>
		<key>source</key>
		<dict>
			<key>base-path</key>
			<string>/Users/patrickbalestra/FindNavigator/</string>
			<key>file-source</key>
			<string>directory</string>
		</dict>
	</dict>
</array>
</plist>

As you can see, the plist file is just an array of dictionaries that specify what the search scopes look like. Each search scope has a name , a predicate dictionary that describes what to search for and a source dictionary that describes where to search. Now that you now the format, you can programmatically create your own custom search scopes and distribute them as part of the build tooling.

In big codebases for example, it may be useful to start distributing common search scopes to all developers to give a better and consistent experience when searching the project.