30 tips to make you a better iOS developer

If you’d like to know more about the ins and outs of iOS/macOS development, you can find me on Twitter.

I’ve compiled a list of dev tips that I feel every iOS developer should know. Ready? Here we go!

1) How to QuickLook an object in Xcode if you only have its address:

  1. click on the pause button and choose a non-Swift frame (left-side).
  2. Right-click → “Add Expression”.
  3. Paste in the address (typecast to object’s type).
  4. Press Space.

2) App metadata localization:

For App Store Optimization (ASO), you can localize your app’s name, description and keywords without having to localize the app itself.

  1. In App Store Connect, create a new app version.
  2. Click the language selector on the right.
  3. Enter localized metadata.
  4. Click Save.

3) How to demangle Swift symbols:

Here’s how you can demangle a mangled Swift symbol, e.g. from a crash log:

$ xcrun swift-demangle <your-mangled-symbol>

4) Apple Search ads credit:

Have an iOS app that you’d like to promote?

If you haven’t tried Apple Search ads before, you can get $100 USD in free credit when you first sign up.

5) Referencing your Identifiable objects’ ID type:

This is a neat trick first shared by @DonnyWals:

Use YourObject.ID instead of the actual type when possible to make refactoring easier, and to make your intent clearer.

struct User: Identifiable {
  let id: Int
  // ... other properties ...
}

// accurate for now
func findByUserId(_ id: Int) -> User {
  // ... implementation ...
}

// easier to refactor, clearer in our intention
func findByUserId(_ id: User.ID) -> User {
  // ... implementation ...
}

6) Using #warning instead of TODO:

Consider using #warning instead of // TODO comments to make sure you keep TODOs prominent in Xcode.

👆 Before
👆 After

7) Asserting on which queue your code is running:

Call dispatchPrecondition in your codebase to control on which queues methods and callbacks are executed.

For example, to make sure a callback is executed on the main queue:

methodThatCallsBackOnMain(completion: { result in
	dispatchPrecondition(.onQueue(.main))
	// process `result`
	// ...
})

8) Save an extra boolean @State variable when using alerts or sheets:

Save an extra @State var showAlert/Sheet/... variable in your views to show alerts, sheets and modals by using an extension on Binding:

extension Binding {
    func presence<T>() -> Binding<Bool> where Value == Optional<T> {
        return .init {
            self.wrappedValue != nil
        } set: { newValue in
            precondition(newValue == false)
            self.wrappedValue = nil
        }
    }
}

// Usage
.alert("An error occurred", isPresented: $error.presence(), actions: {
  Button("Ok", role: .cancel) {}
}, message: {
  Text(error?.localizedDescription ?? "")
})

9) Reading a view’s size without affecting its layout:

I find myself using @zntfdr‘s readSize in all my SwiftUI projects. It’s extremely useful, and doesn’t mess with a view’s layout the way GeometryReader does.

Full guide available here.

// Example of using `readSize` to make a rounded corner image whose
// `cornerRadius` depends on its width:
struct AppIcon: View {
    var name: String
    @State private var width: CGFloat?
    private static let cornerRadiusMultiplier = 0.2

    var body: some View {
        Image(name)
            .resizable()
            .readSize { width = $0.width }
            .clipShape(
                RoundedRectangle(cornerRadius: width.map {
                	$0 * Self.cornerRadiusMultiplier
                } ?? 0, style: .continuous)
            )
    }
}

10) Navigation bar large title gradients:

Final result

To add a gradient to your navigation bar large title like the image above:

  • Create a CAGradientLayer from a color array.
  • Convert the layer to an image.
  • Creating a UIColor from the image via UIColor.init(patternImage:).
  • set scrollEdgeAppearance.largeTitleTextAttributes.

Method devised by bhansmeyer and full guide available here.

11) Break Combine reference cycles:

Combine’s assign(to:on:) captures the on parameter strongly, potentially creating reference cycles.

Likewise, I’m forced to do the [weak self] dance in Combine’s sink(receiveValue:) to avoid cycles.

weakAssign and weakSink to the rescue:

extension Publisher where Failure == Never {
    func weakAssign<T: AnyObject>(
        to keyPath: ReferenceWritableKeyPath<T, Output>,
        on object: T
    ) -> AnyCancellable {
        sink { [weak object] value in
            object?[keyPath: keyPath] = value
        }
    }

    func weakSink<T: AnyObject>(
        _ weaklyCaptured: T,
        receiveValue: @escaping (T, Self.Output) -> Void
    ) -> AnyCancellable {
        sink { [weak weaklyCaptured] output in
            guard let strongRef = weaklyCaptured else { return }
            receiveValue(strongRef, output)
        }
    }
}

12) Using Accessibility Inspector:

Audit your app through the Accessibility Inspector (Xcode menu → Open Developer Tool → Accessibility Inspector) to find potential usability/accessibility issues in seconds:

13) Rounded text design:

Give your text a friendlier, more fun vibe by using a .rounded design instead of the default one. The system will opt for “SF Pro Rounded”.

Text("Welcome Back")
	.font(.system(.largeTitle, design: .rounded).bold())

14) @UsesAutoLayout property wrapper:

Using auto-layout and occasionally forget to call translatesAutoresizingMaskIntoConstraints = false?

We’ve all been there.

Instead, use a @UsesAutoLayout property wrapper that automatically makes the call for you:

@propertyWrapper
public struct UsesAutoLayout<T: UIView> {
	public var wrappedValue: T {
		didSet {
			_setTranslatesAutoresizingMaskIntoConstraints()
		}
	}
	
	public init(wrappedValue: T) {
		self.wrappedValue = wrappedValue
		_setTranslatesAutoresizingMaskIntoConstraints()
	}
	
	private func _setTranslatesAutoresizingMaskIntoConstraints() {
		wrappedValue.translatesAutoresizingMaskIntoConstraints = false
	}
}

// Usage:
class MyView: UIView {
	@UsesAutoLayout var button: UIButton
	...
}

15) Rounded rectangle corners:

Want a rounded rectangle, with a corner radius only applied to some corners?

Use this extension on View:

// MARK: Corner Radius
private struct RoundedCornersRectangle: Shape {
	var radius: CGFloat
	var corners: UIRectCorner
	
	func path (in rect: CGRect) -> Path {
		let cornerRadii = CGSize(width: radius, height: radius)
		let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: cornerRadii)
		return Path(path.cgPath)
	}
}

extension View {
	func cornerRadius( radius: CGFloat, corners: UIRectCorner) -> some View {
		clipShape(RoundedCornersRectangle(radius: radius, corners: corners))
	}
}
Result

16) libdispatch target queues:

Have you used libdispatch’s target queues before?

Coupled with resume/suspend, it makes it super easy to pause work and resume it later. Libdispatch handles it all for you!

Can you guess what happens in this example?

let notificationQueue = DispatchQueue(label: "notifications queue", target: .main)

// pause notifications for a while
notificationQueue.suspend()

notificationQueue.async {
	dispatchPrecondition(condition: . onQueue (notificationQueue)) // does this pass?
	dispatchPrecondition(condition: .onQueue( .main)) // does this pass?	
	print ("A notification!")
}

notificationQueue.async {
	print ("Another notification!")
}

DispatchQueue.main.async {
	print ("Work running on main queue" )
}

// resume notifications
notificationQueue.resume()

// what is printed?

17) Avoiding code repetition:

Here’s a list of small examples that could help make your Swift code cleaner and avoid repetition.

Did you know all of them?

let myViewController: UIViewController?
// instead of this:
if myViewController is UICollectionViewController {
	/* ... */
} else if myViewController is UITableViewController{
	/* ... */
} else {
	/* ... */
}

// you can use a switch statement:
switch myViewController {
	case is UICollectionViewController:
	/* ... */
	case is UITableViewController:
	/* ... */
	default:
	/* ... */
}
// instead of this:
enum WeekDay {
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
	case sunday
}

// you can put it all on one line:
enum WeekDay {
	case monday, tuesday, wednesday, thursday, friday, saturday, sunday
}
// enum value binding with compound cases
enum State {
	case idle
	case loading (value: CGFloat)
	case loaded (value: CGFloat)
}

let state = State.idle

// instead of this:
switch state {
	case idle:
		print ("idle")
	case .loading(let value):
		print ("value: \(value)")
	case .loaded(let value):
		print ("value: \(value) " )
}

// you can do this:
switch state {
case idle:
	print("idle")
case .loading(let value), .loaded(let value):
	print ("value: \(value)")
}
// instead of this:
switch authorizationStatus {
	case authorized:
		/* save to camera roll */
	case notDetermined:
		/* ask for permission */
	case denied:
		self.error = createError() // notice the code duplication, here.
	default:
		self.error = createError() // and here.
}

// you can use fallthrough:
switch authorizationStatus {
	case authorized:
		/* save to camera roll */
	case notDetermined:
		/* ask for permission */
	case . denied:
		fallthrough
	default:
		self.error = createError()
}

18) Centering views in SwiftUI:

I use a simple extension on View to center my views horizontally or vertically:

extension View {
	@ViewBuilder
	func center ( _ axis: Axis)-> some View {
		switch axis {
		case horizontal:
			HStack {
				Spacer()
				self
				Spacer()
			}
		case vertical:
			VStack {
				Spacer()
				self
				Spacer()
			}
		}
	}
}

// Usage:
Button ("Push me") {}
	.center(.horizontal)

19) Hiding SwiftUI view labels:

At first, I found it weird that some SwiftUI views required a label or a title (e.g. Picker, ProgressView, ColorPicker, etc..)

If you don’t want the label to be visible, don’t use EmptyView, but instead use the labelsHidden view modifier.

// Don't do this:
ColorPicker (selection: $backgroundColor) {
	EmptyView()
}

// Do this instead:
ColorPicker("Background color", selection: $backgroundColor)
	.labelsHidden()

20) GroupBox:

You don’t need to think about corner radius and background color every time: SwiftUI does it for you!

Credits: @gaudioaffectus for image and discovery.

21) Free trial text:

If your app offers a free trial, make sure to dynamically check the user’s eligibility status before displaying your e.g. “7 day free trial” text.

The user might have already consumed their introductory price (→ used trial and then canceled).

If you’re using RevenueCat, it’s very easy to check: just pass your productIDs to Purchases.checkTrialOrIntroductoryPriceEligibility.

22) Optionals and unsafelyUnwrapped:

If you have: var myVar: Int?

Did you know that myVar! and myVar.unsafelyUnwrapped don’t behave the same way?

unsafelyUnwrapped trades safety for performance.

Debug build → similar behavior.

Optimized build → undefined behavior if value is nil.

23) Doubling your app’s keywords:

2x your app’s keyword (100 → 200 chars) on App Store Connect by utilizing unused localizations.

Example: a US App Store search uses both en_US and es_MX.

Add your extra english keywords to es_MX if your app isn’t localized for it.

24) Calling view.layoutIfNeeded in animation blocks:

Instead of calling view.layoutIfNeeded on your animated views in an animation block, you can use the .layoutSubviews animation option.

UIKit will take care of laying out the views you animate.

// Calling `layoutIfNeeded` explicitly
UIView.animate(withDuration: duration, delay: 0) {
	/* animations */
	view.layoutIfNeeded()
}

// Using `layoutSubviews` option
UIView.animate(withDuration: duration, delay: 0, options: .layoutSubviews) {
	/* animations */
}

25) importing individual symbols

Instead of importing a whole module, you can import individual structs, classes, enums, functions, variables, and more.

It helps reduce the API surface your swift file is exposed to.

// import single struct
import struct SwiftUI.Alert

// import single variable
import var Foundation.FoundationErrors.NSUserCancelledError

// import single class
import class Photos.PHPhotoLibrary

26) UIGraphicsImageRenderer performance:

Using UIGraphicsImageRenderer?

Perf tip: Keep a reference to your renderer to reuse it.

The renderer keeps a cache of Core Graphics contexts, so reusing the same renderer can be more efficient than creating new renderers.

27) Diagnosing slow Swift build times:

Add:

-Xfrontend -warn-long-function-bodies=100
-Xfrontend -warn-long-expression-type-checking=100

to your “Other Swift Flags” to see which functions/expressions are taking more than 100ms to type-check.

I was amazed to see some methods taking >450ms:

28) Sorting by Name in Xcode:

Did you know that you can sort your files/groups by name in Xcode? 🤯

I was manually sorting them before learning that.

29) Never fill an encryption form ever again on AppStoreConnect:

📝🔐 You can set ITSAppUsesNonExemptEncryption to NO in your Info.plist, after which App Store Connect won’t ask you to fill the encryption exemption form ever again.

(that is, if your usage is exempt of course, e.g. using HTTPS).

30) Pre-processing Info.plist:

Did you know that you could add pre-processor directives to your Info.plist?

Useful when maintaining many schemes for the same target.

  1. set “Preprocess Info.plist File” to YES in your Xcode project.
  2. set your “Info.plist Preprocessor Definitions”.
Xcode target settings
Info.plist

And that’s it! I hope you learned something new reading this post.

Plug

I make neat Mac utilities like Mission Control Plus and Batteries for Mac. Make sure to check them out!

If you’ve enjoyed this post and want to see more deep dives into iOS/macOS development, you can find me on Twitter.

comments powered by Disqus