Notes on Swift and Objective-C programming style and other Cocoa topics.
Notably Xcode may make non-functional changes to .xib
and project files which can be difficult to merge.
Additionally avoid trivial style, naming, or comment changes that are not directly related. Incidental changes add noise making code review harder.
Carthage and Cocoapods are the predominant options. One or the other is used in different projects
Currently Carthage is preferred because:
- easier for a new developer to get set up and install dependencies, no bundler bullshit dependency managers for dependency managers
- faster compile times, i.e. a clean build doesn't recompile dependencies
- more flexibility in project configuration
- less churn as Cocoapods updates. This is more of an issue with less vigilant developers. I suppose it's what bundler is supposed to address, but seriously.
Conversely:
- setting up and adding new dependencies is more involved
Flexibility includes configuring separate dependencies in test targets, breaking projects into multiple modules, or creating frameworks for Swift Playground Support. While configuring dependencies for multiple targets is possible with Cocoapods, it's typically done poorly, and creates a new place and new method of managing the project structure.
There is an incidental performance benefit with Carthage as dependencies are pre-compiled. With Cocoapods, a clean build will always recompile all dependencies.
Although the general wisdom seems to be to commit dependencies, let's not.
In the future the Swift Package Manager may supersede these options.
Assets should be included in one or more .xcassets
packages and grouped into logical folders.
Whenever possible use .pdf
assets for icons and other graphics. In most cases check 'Universal' and choose 'Single Scale' so only one image needs to be added.
For mono-colored icons, always use a black on transparent .pdf
and choose 'Render as: Template Image'. Template images can be tinted any color, making it possible to change the color later without re-creating assets.
Cocoa projects have two version attributes:
-
Short Bundle Version String (
CFBundleShortVersionString
), e.g. 1.4.2: should follow standard major.minor.patch version. This should be incremented accordingly with each release distribution -
Bundle version (
CFBundleVersion
) e.g. 23: should be incremented with each build distributed anywhere. Multiple versions may correspond to one 'version string' before it's officially released.
It's helpful to tag commits of a distributed version like Version 1.4.2 (23)
in order to reference against crash reports.
- Include the README in the Xcode project (though not in any target). Xcode formats markdown, and it's useful to both read and edit the README while working on the project.
Keep the sources directory flat, and organize things into groups in the Xcode project navigator. This is less ideal for browsing the project in finder or on github, but improves Xcode's built in git tools, allowing you to reorganize files without breaking git history.
Swift Playgrounds can be a great for both development and documentation. Using a playground to exercise a logic component, or display one or more states of a view can allow for quickly adjusting and verifying things during development. Often test logic can be easily translated into unit tests.
After development, a playground can document a component, demonstrating it's features interactively.
Playground can be created and dropped into the project navigator for a project. It's useful to have multiple playgrounds alongside components they document.
To run application sources in a playground, they must be included in a framework. Create a framework target called MyAppPlaygroundSupport
, or whatever, and add sources to that target in addition to the main application. Then import the library like @testable import MyAppPlaygroundSupport
. @testable
allows internal declarations to be accessed. Build the 'playground support' target to run playgrounds.
An alternative is to include some sources in a framework that the app target also relies on, but this requires altering the normal procedure for adding new files, and introduces some other complexities.
Documentation comments are distinguished as ///
or /** */
.
/**
Represents a recipe loaded from the API. May belong to a `Store`. Changes to this object
will not be persisted to the server until `save()` is called.
*/
class Recipe {
var title: String
/// In seconds.
// Should we create a type alias to clarify the unit?
var preparationTime: Double
// ...
}
Hard wrap comments to 90 characters.
Use documentation vs normal comment styles to distinguish between comments useful for the consumer of an interface and notes to explain details of implementation or comment on possible alternatives.
Use multi line comments (/** */
) for top level class
or struct
definitions or for
other comments that exceed three lines.
Xcode's "Counterparts" assistant editor is very useful for viewing an interface with documentation comments. Use it to verify a class is understandable.
class Recipe {
/// The title of the recipe
var title: String
}
Comments that restate what is already defined by syntax and name choices are useless noise. This likely applies to properties and method parameters.
class Recipe {
/// The sum of active and inactive time if either are defined. `nil` if neither are
/// defined.
var totalTime: Double? {
// ...
}
/// Expected to be positive or will be silently be converted to `nil`.
var activeTime: Double?
}
- why something is optional and what semantic value
nil
- what range of values is expected
- errors that may be thrown
- invalid parameters not enforced by parameters
- side effects of methods
Prefer 2 space indentation. More importantly, be consistent within a project.
Use // MARK: - Heading Name
to indicate logical chunks of content. For example 'actions', 'view lifecycle', or 'persistence'.
Avoid using extensions to split up segments of behavior. It's popular, but actually confusing. This makes the division between types less obvious, doesn't allow organizing stored properties, creates the illusion of decoupled logic that may in fact not be, and has subtle, unintended ramifications with subclassing and method dispatch.
Use struct
s with static
constants for things like global constants. Do not use enum
s, as has been suggested, because it's misleading and solves a problem (instantiating an object with no purpose) that does not exist.
Use assert()
as liberally as reasonable, to check assumptions. For example, check that UI updates occur on the main thread, or that strings match an expected format.
Catch unexpected parameters that can be reasonably recovered from or a default value substituted, but practically should be handled elsewhere. This is likely a case that should also be logged remotely to be fixed.
Assertions will only fail in debug builds, so it may be reasonable to pair with actual error handling, or reasonable default behavior.
Provide a message if the condition is not independently descriptive.
fatalError()
should only be used to catch programmer errors that are never expected to occur in production, and can not be triggered by invalid data from some external source.
A programmer error is one that only occurs from using an API in an invalid way.
There's a very good chance that any method which includes a fatalError()
should have some documentation
Assertions and fatal may be used liberally to check assumptions you make about input, or the current state. It's better to fail quickly and explicitly, than continue to run some meaningless operation.
There are a few things to consider carefully
Swift lint is fine. Swift tends to be strict enough it's not especially necessary, but a few rules are useful. Suggested Configuration.
Some rules catch simple cases where more descriptive methods can be used e.g. using contains. And some will catch simple spacing inconsistencies.
Custom drawing can be implemented many ways: creating a CALayer
subclass with a custom display()
or drawInContext(_:)
function, using multiple CAShapeLayer
objects, or overriding drawRect(_:)
in a UIView
subclass.
In general, prefer the highest level of abstraction and use a UIView
or NSView
subclass with a custom drawRect(_:)
function and prefer UIBezierPath
's and UIColor
's to handle drawing rather than lower level core graphics APIs. For example:
override func drawRect(rect:CGRect) {
let path = UIBezierPath()
path.moveToPoint(/*...*/)
path.addLineToPoint(/*...*/)
UIColor.redColor().setFill()
path.fill()
}
Be mindful that the rect
parameter provided to the function is the rectangle that should be drawn and is not the bounds of the view.
Using path objects is more flexible than using lower level drawing functions. It allows for extracting common drawing operations into methods returning paths. Generated paths can also then be cached or inspected.