iOS Development Reference
Kip Landergren
(Updated: )
My cheat sheet for iOS development covering common scenarios and helpful resources.
Contents
-
Common Scenarios
- Symbolic Breakpoints
- Custom Colors
- images
- How to Tell If a View Controller’s View is Visible?
- How to Get the User’s Locale?
- How to Log?
- Updating Table Views
- Dealing with View Controller Editing State
- Single-Object View Controllers and Core Data
- List View Controllers and Core Data
- Understand Difference Between Testing Targets
- How to Create a Unit Test Suite
- How to Create a Unit Test Suite
- How to Create a Test
- How to Test a View Controller in Isolation?
- How To Load a Single View Controller For Isolated Interaction?
- How To Create an Archive
- How To View Archives
- How To Automatically Bump / Increment App Version or Build Number
- Using agvtool With Apple Generic Versioning System
- How To Upload Archive
- How To Add Compliance
- How To Configure Beta Testing
- How To Register For All Notifications
- How To Deal with UITableViewCell Labels Being Compressed
- How to create custom section titles using NSFetchedResultsController
- Logging
- Resources
- FAQ
Common Scenarios
The following are not definitive or exclusive solutions.
Symbolic Breakpoints
Prerequisite understanding:
- UIKit methods are written in Objective-C; your symbolic breakpoints must match the Objective-C selectors, not the Swift ones
- an explicit
self
is not available unless you own the implementation of the class, like in a subclass ofUIViewController
where the desired method is overridden (note: I do not understand why self is unavailable) - if the breakpoint is set on the method signature itself, as appears to be the case with symbolic breakpoints, the LLDB variable
$arg1
will likely refer toself
; subsequent lines are not guaranteed as the register may be filled with new data - Facebook’s chisel’s
bmessage
command may be useful - LLDB may be in a Swift frame or in an Objective-C frame depending where the breakpoint fired
An example symbolic breakpoint that logs the class name on every call to viewWillAppear
:
Symbol: -[UIViewController viewWillAppear:]
Action: Log Message
viewWillAppear - @(char *)object_getClassName($arg1)@
results in:
viewWillAppear - "UITabBarController"
Note: this pattern of breakpoint can be expanded to any method you wish to observe.
Custom Colors
Custom user interfaces should:
- respect light or dark mode
- offer high contrast
- support inverted colors
- be accessible for colorblind users
Tint color is the base color iOS will use to apply visual effects, like blur and vibrancy changes, to. By convention, tint color is typically used for interactive elements. The value is the first non-default color specified. “barTintColor” is the navigation bar’s background color (but not the backgrounColor property). I think mainly because the navigation bar contains interactive elements, and therefore has some degree of interactivity. Tint is also used in combination with different rendering modes to color a UIImage.
Navigation Bars have translucency
Blur
Vibrancy
Is there a difference between alpha and translucency? Yes. translucent can be thought of as “semi-opaque”. It allows light to pass through but not detailed shapes. Opacity refers to the value of the alpha channel. Full opacity, or opaque, at a value of 1.0 means that no light goes through. Zero opacity, at a value of 0.0, means fully transparent. What is difference between a UIView’s alpha and a UIView’s layer’s opacity?
masks
shadow
images
template images
How to Tell If a View Controller’s View is Visible?
Make use of viewWillAppear
and viewWillDisappear
to set a Bool
.
How to Get the User’s Locale?
Locale.current
How to Log?
import os.log
os_log("hello world!")
Updating Table Views
The term reload, and the associated methods prefixed with reload, is for when it is significant that the user to know that data has changed. Otherwise just manipulate the cell directly.
Dealing with View Controller Editing State
Somehow, it appears that setting property isEditing
triggers a call to setEditing(:)
, which in turn calls loadView()
. This can result in unexpected behavior, especially with UITableViewController
instances.
Generally, do not rely on external manipulation of isEditing
, and only use setEditing(:)
when you know the view is loaded and visible.
Single-Object View Controllers and Core Data
- should take an
NSManagedObjectContext
- should take an
NSManagedObjectID
(to make it clear the view controller is responsible for correctly associating any fetched managed object with the context correctly) - should contain an
NSFetchRequest
- should be named
TypeViewController
, or with additional specificity
List View Controllers and Core Data
- should take an
NSManagedObjectContext
- should take an
NSPredicate
, or other constraining data, if needed - should contain an
NSFetchRequest
- should contain an
[NSSortDescriptor]
- should contain an
NSFetchedResultsController
- should be named
TypesViewController
, or with additional specificity
Understand Difference Between Testing Targets
Unit tests are run in a host application, UI tests have a target application.
How to Create a Unit Test Suite
Create new class that subclasses XCTestCase
.
How to Create a Unit Test Suite
Create new class that subclasses XCTestCase
.
How to Create a Test
Prefix function name with “test”.
How to Test a View Controller in Isolation?
The broad strokes of this approach are to create a new application target with separate @UIApplicationMain
/ UIApplicationDelegate
that switches the root view controller based on launch arguments. Then, within the UITests target for this new application target, construct a UI test suite passing a launch argument to initialize the
Some problems:
- changes (e.g. to
init()
on the view controller are only compile-time checked when the news isolated target is built; meaning that it is possible they can get out of sync if tests are not regularly run - maintaining files in multiple targets is stupid annoying
- this is co-opting the original UI testing goal of whole-app black-box testing for something that may be natively supported in future
Broad initial setup steps:
- create a new app target
- add all existing files to the new target
- create a new
UIApplicationDelegate
that inspectsProcessInfo
arguments to switch root view controller
And then steps for each view controller:
- create new UI test suite
- invoke app with arguments
- modify
UIApplicationDelegate
to switch the root view controller based on these arguments
Example code for the UIApplicationDelegate
:
if ProcessInfo.processInfo.arguments.contains("AlphaViewController") {
window?.rootViewController = AlphaViewController()
} else {
window?.rootViewController = ViewController()
}
Example code for the UI test:
let app = XCUIApplication()
app.launchArguments.append("AlphaViewController")
app.launch()
XCTAssertTrue(app.staticTexts["Alpha"].exists)
Alternatives considered include:
- creating a custom
main.swift
that loads a test application delegate based on launch arguments. the only reason I did not go down this path was I was not confident in my ability to correctly implement the functionality of@UIApplicationMain
. This may be a good thing to investigate in future.
How To Load a Single View Controller For Isolated Interaction?
The broad approach is to write a UI test that loads your view controller and then call wait(for:timeout:)
:
let app = XCUIApplication()
app.launchArguments.append("AlphaViewController")
app.launch()
app.wait(for: .unknown, timeout: 10000000)
How To Create an Archive
- select target
- select Generic iOS Device
- search help menu for Archive
How To View Archives
- search help for Organizer
How To Automatically Bump / Increment App Version or Build Number
Up front, Apple’s terminology is:
version number | CFBundleShortVersionString ; string using semantic versioning; user visible |
build string | CFBundleVersion ; string using semantic versioning |
The internet seems to fall into two camps:
- use
agvtool
, bundled with Xcode tools, to manage - manually set values in
Info.plist
agvtool
will modify project.pbxproj
, which may cause issues with distributed teams not using a conflict mitigation strategy.
Note: under the hood fastlane just uses agvtool
.
If you configure a Run ScriptBuild Phase to run something like:
xcrun agvtool bump -all
you will have to consider the following:
- Xcode may crash / lock up (specifically because of changing the version changes the project file, which I think causes issues)
- you will be locked into using Apple Generic for your Versioning System, which may not be what you want.
Using agvtool With Apple Generic Versioning System
Note: this may not fit with the semantic versioning system you expect. An alternative is to set the Versioning System to None and manually set the version via:
xcrun agvtool new-version -all x.y.z
To use the Apple Generic system with agvtool
:
- select project
- select target
- select Build Settings
- filter by typing version
- select Apple Generic under Versioning System
- enter some value for Current Project Version, which corresponds to
CURRENT_PROJECT_VERSION
And then use agvtool
from the command line:
$ xcrun agvtool what-version
and:
$ xcrun agvtool what-marketing-version
How To Upload Archive
- link Apple Developer profile with Xcode
- open Organizer
- select archive
- click Distribute App
- follow prompts
How To Add Compliance
When you upload an app, Apple checks to see if you have handled compliance. In Info.plist
you can set key ITSAppUsesNonExemptEncryption
appropriate to your situation, and ITSEncryptionExportComplianceCode
if you have completed that process.
How To Configure Beta Testing
- sign in to App Store Connect
- navigate to TestFlight
- add / invite internal users to specific builds
How To Register For All Notifications
// hack
let center = NotificationCenter.default
center.addObserver(forName: .NSManagedObjectContextDidSave, object: nil, queue: nil) { notification in
print("\(notification.name): \(notification.userInfo ?? [:])")
}
center.addObserver(forName: .NSManagedObjectContextObjectsDidChange, object: nil, queue: nil) { notification in
print("\(notification.name): \(notification.userInfo ?? [:])")
}
How To Deal with UITableViewCell Labels Being Compressed
If your .value1
type cell is losing its detailTextLabel
, consider changing the type to .subtitle
.
How to create custom section titles using NSFetchedResultsController
- create computed property on your managed object
- expose with
@objc
, otherwise you will get key value coding complaints - pass the keypath in the initializer for NSfetchedResultsController
- implement the UITableViewDataSource method
tableView(_:titleForHeaderInSection:)
Sample code:
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let sectionInfo = _fetchedResultsController.sections?[section] else {
return nil
}
return sectionInfo.name
}
Logging
Viewing streaming log message using log, adapted from this stackoverflow post. Note that a predicate must be used as the --subsystem
flag does not apply:
xcrun simctl spawn booted log stream --level debug --predicate '(subsystem BEGINSWITH "com.example.app")' --source
creating a log:
import Foundation
import os.log
struct Log {
private static var _subsystem = "land.kip.AppName"
static let coreData = OSLog(subsystem: _subsystem, category: "core-data")
static let pointsOfInterest = OSLog(subsystem: _subsystem, category: .pointsOfInterest)
static let userInterface = OSLog(subsystem: _subsystem, category: "user-interface")
}
creating a default level log message:
os_log("default msg 1")
creating a debug level log message:
os_log(.debug, log: Log.userInterface, "debug msg 1")
creating an info level log message:
os_log(.info, log: Log.userInterface, "info msg 1")
creating a signpost:
os_signpost(.event, log: Log.pointsOfInterest, name: "View Controller Tapped")
creating a signpost with a message (format string and arguments are supported):
os_signpost(.event, log: Log.pointsOfInterest, name: "View Controller Tapped", "my message!")
important:
- do not wrap
os_log
APIs; otherwise file name and line number is lost - when creating
.begin
and.end
signposts, if the actions will overlap and the name is not regenerated, create a signpost ID.
Resources
Documentation
- agvtool
- XCTest
Questions
FAQ
How to print out an NSString containing the class name via a symbolic breakpoint?
po [[NSString alloc] initWithFormat:@"viewDidAppear - %@", @((char *)object_getClassName($arg1))]