00:06 Today we'll have a look at the cool new refactoring features of
Xcode 9. Back in the Objective-C days, we actually used AppCode's refactoring a
lot. It changed the way we wrote our code: instead of copying and pasting lines,
we used the refactoring system to extract expressions and methods and to create
variables and properties.
00:52 Now with Xcode 9, we're getting a glimpse of what this way of
working can look like. But the refactoring features are still in beta, so they
don't always work perfectly. Therefore, it's good to have your project under
version control.
Renaming
01:19 The most important shortcut to remember is Cmd+Shift+A, which
brings up the new Actions menu at the current cursor position:
01:38 When we choose the Rename option, Xcode shows all the places where
ViewController
is used or mentioned, including the file name and the
storyboard reference. It even shows that the file header's comment block
mentions ViewController
. This mention of the name in the comment block is
initially greyed out, but when we click it, it'll also be replaced.
02:07 Now we can type a new name, like PlayerViewController
. When we
build, we find out something went wrong: Xcode updated the file reference in the
project, but it failed to rename the actual file. This shows that the system
isn't completely stable yet.
Extracting Expressions
03:04 Another thing Xcode can do is extract expressions. Let's say we
have the following statement:
if view.subviews.count > 0 {
}
03:26 Later we might decide we need to use the subview count in more
than one place, so it'd be good to extract it to a separate expression.
Unfortunately, the shortcut Cmd+Shift+A works only for the current identifier,
view
, and not the whole expression. Instead, we have to select the entire
expression, view.subviews.count
, and choose the menu option Editor > Refactor
> Extract Expression. This results in the following:
let extractedExpr: Int = view.subviews.count
if extractedExpr > 0 {
}
03:58 And of course, we can then quickly rename the expression to
viewCount
using the Actions menu again:
let viewCount: Int = view.subviews.count
if viewCount > 0 {
}
04:19 If we prefer to only use the keyboard to extract the expression,
we could select the expression and press Cmd+Shift+?. This opens the Help menu
search, in which we can simply start typing "extract" and then navigate to the
correct menu button using the arrow keys. This Help menu shortcut works
universally on macOS.
05:01 As an even quicker alternative to using the Help menu, we can
assign a custom shortcut. In Xcode's preferences, under Key Bindings, we can
search for "extract expression" and assign a unique shortcut, say
Cmd+Option+Ctrl+E. This is handy if we use the menu option a lot.
Various Useful Actions
06:17 By pressing Cmd+Shift+A on an if statement, we can quickly add
else
or else if
statements:
06:55 There's also more support for working with enums. When we write a
switch statement for an enum, we can wait for the compiler error saying that the
switch must be exhaustive:
class PlayerViewController: UIViewController {
enum PlayState {
case playing(title: String)
case stopped
}
var state: PlayState = .stopped
override func viewDidLoad() {
super.viewDidLoad()
switch state {
}
}
}
07:20 The compiler error now comes with a fix-it to add the missing
cases. It sometimes even adds the variable binding — for title in this case:
switch state {
case . playing(let title):
case .stopped:
}
08:00 If we add a new case to the enum, we can use the fix-it again.
08:21 You can also use the Cmd+Shift+A shortcut on the switch statement
itself. This gives you more naive options to add a blank case or a default,
without any knowledge of your enum.
09:07 Another useful bit is that you can press the Cmd key and hover
over your code with the mouse. This highlights the scope, which makes the
structure of your code visible. When clicking with the Cmd key pressed, you get
the same Actions menu as with the shortcut Cmd+Shift+A.
Extracting Methods
09:44 If we select our entire switch statement, we can extract a method
using the Refactor menu:
fileprivate func extractedFunc() {
switch state {
case . playing(let title):
print("Playing: \(title)"
case .stopped:
print("Stopped")
case .paused:
print("Paused")
}
}
override func viewDidLoad() {
extractedFunc()
}
10:11 Xcode moved the switch statement into a new method,
extractedFunc
, and added a call to this method at the line where the statement
used to be. Of course, we can easily rename the method to printState
via
refactoring.
10:46 It's interesting how method extraction deals with local variables.
Let's say that instead of printing the state, we want to work with a variable.
We undo the last changes and add a result
variable:
override func viewDidLoad() {
var result: String = ""
switch state {
case .playing(let title):
result = "Playing: \(title)"
case .stopped:
result = "Stopped"
case .paused:
result = "Paused"
}
}
11:16 If we extract the switch statement into a method, Xcode creates an
inout
parameter:
fileprivate func extractedFunc(_ result: inout String) {
switch state {
case .playing(let title):
result = "Playing: \(title)"
case .stopped:
result = "Stopped"
case .paused:
result = "Paused"
}
}
override func viewDidLoad() {
var result: String = ""
extractedFunc(&result)
}
11:22 It makes sense that Xcode uses an inout
parameter, since we're
writing to the result
variable. Xcode doesn't make unnecessary copies; it just
writes to the variable in place.
11:57 In the future, maybe we'll have a way to let the compiler refactor
the code to result = extractedFunc()
. We can also try to add this custom
refactoring ourselves in a future episode.