iOS Developer Skill Matrix
Builds native iPhone and iPad apps in Swift: SwiftUI and UIKit interfaces, concurrency, networking and data, testing, performance, security, App Store release, and the AI-assisted and on-device AI practices around them.
Free for teams of up to 5 people. No credit card required.
One growth tree. Three clear positions.
This matrix connects 35 knowledge areas to clear expectations for Junior, Middle and Senior positions. 378 practical exercises show how that growth can be demonstrated.
A practical framework from expectations to evidence
The knowledge areas define the skills required for this role. Position requirements show the proficiency expected at each career step, and the exercises provide concrete ways to practise and demonstrate those skills.
- 35
- Knowledge areas
- 378
- Practical exercises
- 2
- Skill groups
- 01
Junior
Expected proficiency mapped across 31 of 35 knowledge areas.
- 02
Middle
Expected proficiency mapped across 35 of 35 knowledge areas.
- 03
Senior
Expected proficiency mapped across 35 of 35 knowledge areas.
Explore every knowledge area
Technical Skills
01Swift
Writing correct, idiomatic Swift: types and optionals, value vs reference semantics, protocols and generics, closures, error handling, method dispatch, and reading and bridging Objective-C code.
TargetNovice
Swift
Writing correct, idiomatic Swift: types and optionals, value vs reference semantics, protocols and generics, closures, error handling, method dispatch, and reading and bridging Objective-C code.
Learn Swift's core building blocks from A Swift Tour
Read 'A Swift Tour' in The Swift Programming Language, docs.swift.org/swift-book/documentation/the-swift-programming-language/guidedtour/, working through its sections on simple values (arrays and dictionaries), control flow (including optionals and if let), functions and closures, objects and classes, enumerations and structures, protocols and extensions, error handling and generics, and try the small experiments it suggests in an Xcode playground. Then open one feature folder of a real or test app (yours, a teammate's, or an open-source iOS app) and find one real example of each: an optional and how it is unwrapped (if let, guard let, ?? or a force unwrap with !), an array or dictionary transformed with map, filter or sorted, a struct and a class, an enum (with associated values if there is one), a protocol and a type that conforms to it, a closure passed to a function, and a function that throws together with the code that calls it. For each, write one or two sentences on what the code does and what would go wrong if it were written differently (e.g. what happens if that force-unwrapped value is ever nil). Evidence: the code links or snippets with your notes, your playground file, and your mentor's comments.
Implement a well-scoped Swift ticket with the core language features
On a real or test project, take a well-scoped ticket that adds logic in plain Swift (or write one with your mentor), for example the model and rules for an order's status, parsing and validating a sign-up form's fields, or grouping and sorting a list of transactions by month. Using what you noted in the exercise above, write it on your own so that: data is modeled with structs, with a class only where one shared instance must be changed in one place; a fixed set of states or options is an enum (with associated values where a state carries data, e.g. .failed(reason)) handled in a switch without a default case, so the compiler tells you when someone adds a case; values that can be missing are optionals unwrapped with if let, guard let or ??, with no force unwraps (!) or try! unless your reviewer agrees there is no other way; collections are transformed with map, filter, sorted or reduce and closures; at least one type conforms to a protocol it needs (e.g. Equatable, Hashable, Codable or one from your codebase); and a function that can fail throws its own Error enum, with the caller handling it in do/catch. If one ticket doesn't naturally need all of these (e.g. nothing in it can fail), cover the rest in a second small ticket rather than adding code just to use a feature. Open a PR and get it reviewed. Evidence: a link to the PR, a short note pointing to where you used each of these and why, and your reviewer's comments with how you resolved each one.
Work through generics and type errors with a senior's help
Over two to three weeks of normal tickets on a real or test project, keep a log of the compiler errors you can't understand on your own, especially ones about generics and types, for example 'Generic parameter 'T' could not be inferred', 'Cannot convert value of type ... to expected argument type ...', or 'Type 'any Shape' cannot conform to 'Shape''. For each, first spend about 15 minutes on your own: read the whole message and any notes Xcode attaches to it, and look up the feature involved in the Swift book's 'Generics' chapter, docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/. Then ask a senior or your mentor, fix it together, and write down in your own words what the compiler was telling you and how the fix answers it. Aim for three errors. If your tickets produce fewer, your mentor sets up the rest: they take a piece of generic code from your codebase (e.g. a generic cache, a function with a where clause, or a view that takes a generic content type) and change it so it fails to compile with a type error, and you work through each one the same way, getting their help after your own 15 minutes, then explain to them what each type parameter and constraint does. Evidence: the log with each error message, what you tried first, the help you got and the fix (PR links or snippets), and your mentor's confirmation that your explanations are right.
02Programming paradigms & design patterns (iOS)
Applying paradigms and patterns to units of Swift code: object-oriented, protocol-oriented, functional and reactive styles, SOLID, and Cocoa patterns such as delegation, target-action and observer.
TargetNovice
Programming paradigms & design patterns (iOS)
Applying paradigms and patterns to units of Swift code: object-oriented, protocol-oriented, functional and reactive styles, SOLID, and Cocoa patterns such as delegation, target-action and observer.
Learn delegation, target-action and observer, then find them in your app
Read the 'Delegation' section of the Swift book's 'Protocols' chapter, docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols/, and skim its sections on protocols as types, protocol composition and protocol extensions; the 'Respond to user interaction' part of Apple's UIControl documentation, developer.apple.com/documentation/uikit/uicontrol, on the target-action mechanism; and John Sundell's 'Observers in Swift, Part 1', www.swiftbysundell.com/articles/observers-in-swift-part-1/, on NotificationCenter and observation protocols. Then find one real example of each pattern in a real or test app (yours, a teammate's, or an open-source iOS app): a delegate (your own or a system one such as UITableViewDelegate or CLLocationManagerDelegate), a control wired with target-action (addTarget(_:action:for:), an @IBAction or a UIAction), and an observer (a NotificationCenter observer, key-value observing, or an observable model the UI watches). For each, write who sends the message, who receives it, how they are connected, and why this pattern fits better here than the other two (e.g. why an event many objects care about is a notification and not a delegate call). Evidence: the code links or snippets with your notes, and your mentor's comments.
Use the codebase's Cocoa patterns in a ticket and fix what review catches
On a real or test project, take a well-scoped ticket where one object must tell another that something happened, for example a custom cell telling its screen that a button inside it was tapped, a form view reporting that its input changed, or a screen that must refresh when the app returns to the foreground. Using what you noted in the exercise above, solve it with the pattern your codebase already uses for that kind of message: a delegate protocol, a control wired with target-action or a UIAction, or an observer (a NotificationCenter notification such as UIApplication.willEnterForegroundNotification, or the observable model your codebase uses). Use at least two of the three; if one ticket needs only one, take a second small ticket for another. If your codebase is SwiftUI-only and has no delegate or target-action code, do the second one on a test project (e.g. a small UIKit screen, or a UIViewRepresentable whose coordinator acts as a delegate) and say so. Copy the way existing code names and wires these patterns rather than inventing your own. Open a PR and ask your reviewer to check specifically whether each pattern fits and is wired correctly (e.g. a delegate property that should be weak, a notification used where only one object ever listens, a delegate protocol that grew methods nobody implements). Fix every point they raise. Evidence: links to the PRs with the review threads, the existing code you modeled your work on, the follow-up commits for each comment, and a two-sentence note on the misuse you would not have spotted yourself.
Design a small unit with a protocol and focused types of your own
On a real or test project, take a well-scoped ticket that needs a small piece of logic with more than one variant or a dependency you want to swap, for example sharing content through different channels, formatting prices for different regions, or a screen's data that comes from the network in the app and from fixed sample data in SwiftUI previews or tests. Using the protocol sections you skimmed in the exercise above, write it yourself as: one small protocol that describes only what callers need (e.g. a ContentSharing protocol with a single share method), at least two types that conform to it, and one class or struct with one clear job that works with the protocol rather than a concrete type and keeps its own state private. Get it reviewed, asking your reviewer to look for misuse: a protocol that forces conformers to implement methods they don't need, a type that does several jobs, a subclass created only to reuse a bit of code, or state that callers can change from outside. Fix every point raised. Evidence: a link to the PR with the review thread, a short note on why the protocol has exactly those requirements and what each type is responsible for, and the follow-up commits for each comment.
03App architecture (iOS)
Structuring an iOS app across screens and modules: MVC, MVVM, coordinators, VIPER or TCA, navigation architecture, app-wide state, modularization with Swift packages, dependency injection, and end-to-end feature design.
TargetNovice
App architecture (iOS)
Structuring an iOS app across screens and modules: MVC, MVVM, coordinators, VIPER or TCA, navigation architecture, app-wide state, modularization with Swift packages, dependency injection, and end-to-end feature design.
Learn MVVM in SwiftUI and map where your app puts each kind of code
Read Paul Hudson's 'Introducing MVVM into your SwiftUI project' on Hacking with Swift, www.hackingwithswift.com/books/ios-swiftui/introducing-mvvm-into-your-swiftui-project, on moving a screen's state and logic out of the view into an @Observable view model, and using private(set) so only the view model changes its data. If your app uses UIKit or another pattern (MVC, coordinators, VIPER, TCA), the idea of separating logic from layout still applies. Then map the app you work on (real or test project) on one page: the pattern its screens follow (e.g. a view and a view model per screen), its top-level folders, targets and local Swift packages and what each holds, where networking and storage code lives, how navigation works (a coordinator, a router that owns the navigation path, or screens pushing each other directly), and where app-wide state lives (e.g. the signed-in user or settings, shared through the SwiftUI environment or an object passed in). Walk a senior or your mentor through it and correct what they point out. Evidence: the map (a diagram or a doc), the reviewer's corrections and what you changed.
Plan where a new screen's code goes, then build it in the app's pattern
Take a real ticket that adds a new screen to your team's app, or a test-project ticket your mentor gives you (e.g. an order detail screen, a settings page, an edit form). Using your map from the exercise above, write a short placement plan before coding: the files you'll add, which layer and which module or package each belongs in (the view, its view model or presenter, models, calls to the existing networking or storage services), what each may use and what it must not (e.g. the view never calls the API directly), and which existing types you'll reuse instead of writing new ones. Ask a senior (on a test project, your mentor) to check the plan, then build the screen on your own in the app's existing pattern, following the plan and keeping logic out of the view. When the PR goes to review, ask the reviewer to check placement specifically, and fix whatever they flag. Evidence: the placement plan, the senior's comments on it, a link to the merged PR, the review comments about placement and what you changed because of them.
Connect a new screen to the app's navigation and shared state
On a real or test project, take a ticket where a screen is opened from another part of the app and uses state the whole app shares, for example a screen opened from the profile tab that shows the signed-in user's details and must update when they sign out, or a filter screen whose choice other screens read. Open it through the app's existing navigation (ask the coordinator or router to show it, or add it to the navigation path the app already owns) instead of pushing or presenting it directly from another screen. Read the shared state from where the app already keeps it (an object passed in or read from the SwiftUI environment) instead of creating a new singleton or a second copy that can go out of date. Check that the screen updates when the shared state changes and that going back still works. If you aren't sure where the navigation or the state lives, ask a senior (on a test project, your mentor) and write down their answer. Record with a test account, or hide real names and email addresses in the recording. Evidence: a link to the PR, a short recording showing the screen opened through the app's navigation and updating when the shared state changes, a note on which navigation entry point and which shared state you used, and your reviewer's comments on placement.
04SwiftUI
Building screens in SwiftUI: views and modifiers, layout across iPhone and iPad, navigation, data flow with state, bindings and observable models, list performance, and interop with UIKit.
TargetNovice
SwiftUI
Building screens in SwiftUI: views and modifiers, layout across iPhone and iPad, navigation, data flow with state, bindings and observable models, list performance, and interop with UIKit.
Learn who owns each piece of state in a SwiftUI screen
Read two of Apple's SwiftUI articles: 'Managing user interface state', developer.apple.com/documentation/swiftui/managing-user-interface-state, on keeping view-local values in private @State properties and sharing them with child views through bindings, and 'Managing model data in your app', developer.apple.com/documentation/swiftui/managing-model-data-in-your-app, on making a model class @Observable, creating its source of truth with @State, sharing it through the view hierarchy or the environment, and changing it from a view with @Bindable. Then pick one screen in a real or test app that has both view-local state and shared data (e.g. a settings screen with a toggle and a sheet, or a list with a filter and an add button) and annotate its code: for every stored value, write who owns it (its source of truth), which property wrapper it uses (@State, @Binding, @Bindable, @Environment or none), and which views read or change it. Mark one value whose wrapper looks wrong to you or that you don't understand, and ask your mentor about it. Evidence: the annotated code (a PR comment, gist or screenshot), your question and your mentor's answer.
Build a ticket's list-and-detail screen in SwiftUI
On a real or test project, take a well-scoped ticket that adds or rebuilds one screen with a list and a detail view (e.g. a list of orders that opens an order's details, or a list of saved articles that opens one to read) and build it in SwiftUI on your own. If you haven't built a list with navigation before, first work through Apple's tutorial 'Building lists and navigation', developer.apple.com/tutorials/swiftui/building-lists-and-navigation. Compose the screen from small views with stacks (VStack, HStack, ZStack) and modifiers, show the items with List or ForEach over Identifiable data, and open the detail with the navigation container your app already uses (NavigationStack or NavigationSplitView, with NavigationLink). Keep each value where the exercise above says it belongs. Then check the layout cases the design may not show: very long text in a row, an empty list, the smallest iPhone your app supports, and landscape. When one of them looks wrong and you can't fix it in about half an hour (e.g. text cut off, a row that doesn't fill the width, content hidden behind the keyboard or the home indicator), ask a senior developer, fix it with them and write down what caused it. If none of them gives you trouble, ask your mentor to set up a layout problem on a test screen (e.g. text cut off at a large text size) and solve it the same way, asking for help when you get stuck. Use test data in screenshots, or hide real people's names. Evidence: a link to the merged PR, screenshots of the screen in each of those cases, the layout problem you got help with, its cause and fix, and your reviewer's comments.
Pass state to an edit view and fix a state bug with help
On a real or test project, add a small editing flow to a SwiftUI screen, for example a sheet that edits an item's name and a favorite toggle, or a filter panel that changes what a list shows. Using what you noted in the first exercise above, keep one source of truth: the parent owns view-local values in @State and passes them to the child as a @Binding, and shared data lives in an @Observable model that the child edits through @Bindable (or reads from the environment, if your app passes it that way). Check that every change shows up on the parent screen straight away, and that canceling leaves the data unchanged if your design asks for that. State bugs are common here: a child that copies a value into its own @State and never writes it back, so edits never reach the parent (copying into a draft on purpose is fine for an edit the user can cancel, as long as Save writes it back), a model created inside a view's body so it's replaced on every update, or a model class that isn't marked @Observable (in older code, a property missing @Published) so the screen doesn't update. When a value doesn't update the way you expect, try for about half an hour, then ask a senior developer, fix it with them and write down the cause in your own words. If you hit no state bug, ask your mentor to plant one in a copy of your branch and find it, asking for help if you get stuck. Evidence: a link to the merged PR, a screen recording of the edit flow, a short note naming each value's owner and wrapper, the state bug with its cause, its fix and who helped, and your reviewer's comments.
05UIKit
Building screens in UIKit: view controllers and their lifecycle, Auto Layout in code and Interface Builder, table and collection views with diffable data sources, iPad layouts, and interop with SwiftUI.
TargetNovice
UIKit
Building screens in UIKit: view controllers and their lifecycle, Auto Layout in code and Interface Builder, table and collection views with diffable data sources, iPad layouts, and interop with SwiftUI.
Learn how a view controller shows its views and how constraints size them
Read Apple's article 'Displaying and managing views with a view controller', developer.apple.com/documentation/uikit/displaying-and-managing-views-with-a-view-controller, on adding views, outlets, handling events and preparing views to appear (viewDidLoad, viewWillAppear, viewIsAppearing, viewDidAppear), and 'Anatomy of a Constraint' in Apple's Auto Layout Guide, developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/AnatomyofaConstraint.html, on how constraints work as equations, why a layout must be neither ambiguous nor unsatisfiable, priorities, and intrinsic content size with content hugging and compression resistance. Then open one existing UIKit screen in a real or test project and annotate it: what each lifecycle method in its view controller does and why it belongs there, and, for two of its views, the constraints that fix their position and size (in code, or in Interface Builder's size inspector), naming any view whose size comes from its intrinsic content size. Write down one thing you couldn't explain and ask your mentor about it. Evidence: the annotated code or screenshots, your question and your mentor's answer.
Extend an existing UIKit screen and fix its layout with help
On a real or test project, take a well-scoped ticket that changes an existing UIKit screen, for example adding a subtitle label and a button under a title, showing a badge on an image, or moving two fields into one row. Work the way the screen is already built: in Interface Builder if it uses a storyboard or XIB, or in code with layout anchors (setting translatesAutoresizingMaskIntoConstraints to false on views you create) if it doesn't. Using what you noted in the exercise above, give each new view a full set of constraints, and use a UIStackView where the screen already does. Then check the cases the design may not show: very long text, an empty value, the smallest iPhone your app supports, and landscape. Run the screen with the Xcode console open and check for 'Unable to simultaneously satisfy constraints' messages. When the layout breaks and you can't fix it in about half an hour, read 'Debugging Tricks and Tips' in the Auto Layout Guide, developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/DebuggingTricksandTips.html, then ask a senior developer, fix it with them and write down the cause. If nothing breaks, ask your mentor to plant a layout problem (e.g. a missing or conflicting constraint) in a copy of your branch and find it, asking for help if you get stuck. Evidence: a link to the merged PR, screenshots in each of those cases, the console showing no constraint warnings for the screen, the layout problem you got help with and its cause and fix, and your reviewer's comments.
Change a table or collection view and get a lifecycle issue right with help
On a real or test project, take a ticket that changes an existing table or collection view, for example adding a new kind of row, showing an extra field in each cell, or opening a detail screen when a row is tapped. Dequeue cells with the reuse identifier or cell registration the screen already uses, set every property the cell shows each time you configure it (so a reused cell never shows the previous row's text or image), reset anything else in prepareForReuse if the cell needs it, and handle taps in the delegate. Then make the list show fresh data when the user comes back from the detail screen, choosing the lifecycle method from the first exercise above where the refresh belongs. Lifecycle issues are common here: data loaded in viewDidLoad that never refreshes, size-dependent code that runs before the view has its final size, or work that runs twice. When one appears and you can't explain it in about half an hour, ask a senior developer, fix it with them and write down the cause. If you hit none, ask your mentor to plant one in a copy of your branch and find it, asking for help if you get stuck. Use test data in screenshots, or hide real people's names. Evidence: a link to the merged PR, a recording that scrolls fast and returns from the detail screen, a note on where each piece of work runs and why, the lifecycle issue with its cause, its fix and who helped, and your reviewer's comments.
06Human Interface Guidelines & design handoff
Building apps that feel native: applying Apple's Human Interface Guidelines, implementing designs from Figma faithfully, pushing back on non-native patterns, and making UI ready for localization and right-to-left.
TargetNovice
Human Interface Guidelines & design handoff
Building apps that feel native: applying Apple's Human Interface Guidelines, implementing designs from Figma faithfully, pushing back on non-native patterns, and making UI ready for localization and right-to-left.
Learn how iOS handles dark mode, SF Symbols and system fonts, then audit a screen
Read three pages of Apple's Human Interface Guidelines: 'Dark Mode', developer.apple.com/design/human-interface-guidelines/dark-mode, on colors that adapt to the appearance; 'SF Symbols', developer.apple.com/design/human-interface-guidelines/sf-symbols, on rendering modes, weights and scales; and the 'Conveying hierarchy' and 'Using system fonts' parts of 'Typography', developer.apple.com/design/human-interface-guidelines/typography. Then open one existing screen of a real or test app next to its design file (or a screenshot if there is no design) and find up to eight places where the code doesn't follow them, for example a hard-coded color that stays the same in dark mode, a fixed font size where a text style such as .headline fits, a bitmap icon an SF Symbol could replace, a custom control drawn to look like a system toggle or button, or a user-facing string that never reaches the String Catalog (e.g. label.text = "Save" in UIKit, or a String variable shown with Text(title)). For each, write what it should use instead. Look at the screen in both light and dark appearance (e.g. in Xcode previews, or with the Environment Overrides button while debugging). Evidence: your list with file and line, screenshots in both appearances, and your mentor's comments.
Build a screen from a design file and ask the designer about the gaps
On a real or test project, take a well-scoped ticket to build one screen from a design file (e.g. in Figma), such as a settings page, a profile or an empty state, and build it on your own using what you noted in the exercise above. Read spacing, sizes and colors from the file instead of eyeballing them (Figma's Dev Mode needs a Full or Dev seat on a paid plan; without one, open the file with view access, select a layer to see its values in the Properties tab and hold Option, or Alt on Windows, to measure distances, as Figma's 'Guide to inspecting' shows, and ask the designer for anything you still can't read). Use the project's colors from the asset catalog, or system colors, with a dark variant for every color; text styles for type; SF Symbols for icons (find names in the SF Symbols app); and system components (List, Toggle, Button, Menu) instead of custom look-alikes. Put every user-facing string in the String Catalog, as Apple's 'Localizing and varying text with a string catalog' describes, developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog: SwiftUI Text literals are added when you build, and in UIKit or view models you use String(localized:). Wherever the design is unclear or doesn't fit (no dark version, an icon with no matching SF Symbol, a state that isn't drawn, text longer than its box), ask the designer before guessing (on a test project, or if your team has no designer, whoever owns the design answers: a PM, a developer who drew it, or your mentor playing the designer), and keep a short log of each question and answer. If everything seems clear, ask about one state the design doesn't draw. Evidence: a link to the PR, screenshots of the screen next to the design frame in light and dark appearance, the String Catalog entries for the screen, the question log with at least one answer from the designer (names hidden), and your reviewer's comments.
07Animation & graphics (iOS)
Adding motion and custom graphics to iOS apps: SwiftUI animations and transitions, Core Animation, Core Graphics drawing, Lottie, smooth frame rates, and respecting Reduce Motion.
TargetNovice
Animation & graphics (iOS)
Adding motion and custom graphics to iOS apps: SwiftUI animations and transitions, Core Animation, Core Graphics drawing, Lottie, smooth frame rates, and respecting Reduce Motion.
Learn when iOS motion helps and how to add it
Read the 'Motion' page of Apple's Human Interface Guidelines, developer.apple.com/design/human-interface-guidelines/motion, on adding motion purposefully, making it optional, keeping feedback animations brief and precise, and letting people cancel motion. Then, if your app uses SwiftUI, work through Apple's tutorial 'Animating views and transitions', developer.apple.com/tutorials/swiftui/animating-views-and-transitions, on animating changes and building custom transitions (the tutorial is older, but its animation and transition steps still apply); if it uses UIKit, read the overview of Apple's UIViewPropertyAnimator page, developer.apple.com/documentation/uikit/uiviewpropertyanimator, on animating view properties with a duration and a timing curve. Then pick two animations in a real or test app (yours, or one you use every day) and write for each what it moves or changes, roughly how long it takes and how it speeds up or slows down, what it tells the user, and whether the HIG would call it purposeful or decorative. Evidence: your notes on the two animations and your mentor's comments.
Add a standard animation and transition, with a Reduce Motion version
On a well-scoped ticket in a real or test project, add motion to one screen with the framework's standard tools, using what you noted in the exercise above: a state change that animates (e.g. a card that expands, a button that turns into a checkmark, a badge count that updates) and a view that appears and disappears with a transition (e.g. a banner, an inline error, a row being added or removed). In SwiftUI use withAnimation or .animation(_:value:) and .transition; in UIKit use UIView.animate or UIViewPropertyAnimator, or the table or collection view's own insert and delete animations. Keep durations short and choose the timing curve or spring on purpose. Then turn on Reduce Motion (Settings, Accessibility, Motion) and make the screen respond: read accessibilityReduceMotion from the SwiftUI environment or UIAccessibility.isReduceMotionEnabled in UIKit, and replace sliding or scaling with a fade or an instant change without losing the state change. Ask your mentor to watch it on a real device with Reduce Motion off and on and comment on timing, smoothness and your reduced version before you merge, and write down what you changed. Evidence: a link to the merged PR, a screen recording with Reduce Motion off and on, and a note on your mentor's suggestions and what you changed because of them.
Draw a simple custom shape and animate it
On a real or test project, build one simple custom shape the design needs, for example a progress ring, a speech-bubble tail, a tab indicator with a curved edge, or a bar for a small chart. In SwiftUI, write a type that conforms to Shape and builds its outline in path(in:) (see Apple's Shape page, developer.apple.com/documentation/swiftui/shape; the tutorial 'Drawing paths and shapes', developer.apple.com/tutorials/swiftui/drawing-paths-and-shapes, is older, but it still shows how to build a Path that scales with its frame); in UIKit, build a UIBezierPath and show it in a CAShapeLayer. Make it scale with its frame instead of using fixed coordinates, and animate one property (e.g. a progress ring filling with trim in SwiftUI or strokeEnd in UIKit), with a version that skips the movement when Reduce Motion is on. Show it to your mentor or a senior developer at two sizes and with Reduce Motion on, and write down what they suggested. Evidence: a link to the PR, screenshots at two sizes, a recording of the animation with Reduce Motion off and on, the guidance you got and what you changed, and your reviewer's comments.
08Accessibility (iOS)
Making iOS apps usable with assistive technology: VoiceOver labels, traits and focus order, Dynamic Type, contrast and other accessibility settings, and testing with Accessibility Inspector and real assistive features.
TargetNovice
Accessibility (iOS)
Making iOS apps usable with assistive technology: VoiceOver labels, traits and focus order, Dynamic Type, contrast and other accessibility settings, and testing with Accessibility Inspector and real assistive features.
Learn how VoiceOver and Dynamic Type read your screen, then inspect one
Watch the 'Fundamentals' part of Apple's WWDC24 session 'Catch up on accessibility in SwiftUI', developer.apple.com/videos/play/wwdc2024/10073/, on accessibility elements, their labels, traits and actions, and how several views can be combined into one element (UIKit views have the same properties, such as accessibilityLabel, accessibilityTraits and accessibilityCustomActions, but grouping works differently; if your screens are UIKit, also read Apple's 'Supporting VoiceOver in your app', developer.apple.com/documentation/uikit/supporting-voiceover-in-your-app, on setting labels and hints and grouping elements in UIKit). Then watch the 'Scaled text' part of WWDC24 'Get started with Dynamic Type', developer.apple.com/videos/play/wwdc2024/10074/, on text styles in SwiftUI and UIKit. Finally read Apple's 'Inspecting the accessibility of the screens in your app', developer.apple.com/documentation/accessibility/inspecting-the-accessibility-of-screens, on pointing Accessibility Inspector (Xcode > Open Developer Tool > Accessibility Inspector) at your app and stepping through the elements in the order VoiceOver reads them. Run one screen you built in a real or test app (e.g. a settings, profile or product detail screen) and step through it with the inspector. For each element, write what the inspector shows (label, value, traits), what it should say instead, and whether it comes in a sensible order. Then list each piece of text on the screen with its font, marking the ones set to a fixed point size instead of a text style such as body or headline. Evidence: your element table and font list for the screen, and your mentor's comments on them.
Fix your screen's labels, traits and reading order, with help on a custom control
On a real or test project, fix the VoiceOver problems you found in the exercise above, on that screen or on the next screen you build. Give every icon-only button a short label that says what it does ("Delete", not "trash icon"), hide purely decorative images from VoiceOver, mark section titles as headers (the isHeader trait in SwiftUI, .header in UIKit), make selected or on and off states heard (e.g. the isSelected trait in SwiftUI, .selected in UIKit, or a value), and make the pieces of a row read in an order that makes sense, combining them into one element where they belong together (e.g. a product name and its price). If the screen is UIKit, the UIKit page from the exercise above shows where to set labels, hints and groups. If the screen has a custom control built from shapes or gestures (e.g. a star rating, a custom toggle or a slider), ask a senior developer (on a test project, your mentor) how VoiceOver should present it and add that with them; if your screen has none, ask your mentor to point you to one elsewhere in the app, or on a test project to add a simple one with you (e.g. a star rating). Check the result with the inspector's navigation and speak buttons, and run its audit (the Audits pane) on the screen. If you have an iPhone or iPad, also swipe through the screen with VoiceOver on (Settings > Accessibility > VoiceOver). Evidence: a link to the merged PR, the inspector audit before and after, your table from the exercise above updated with what each element says now, the custom control with a note on what the senior showed you, and your reviewer's comments.
Make your screen's text grow with Dynamic Type
On a real or test project, make the text on your screen from the exercises above (or on the next screen you build) follow the user's text size, using the font list from the first exercise. Replace fixed font sizes with text styles: .font(.body) or .font(.headline) in SwiftUI, preferredFont(forTextStyle:) with adjustsFontForContentSizeCategory set to true in UIKit. If the design uses a custom font, scale it too, with Font.custom(_:size:relativeTo:) in SwiftUI or UIFontMetrics in UIKit. Remove what stops text from growing: fixed heights on rows and labels, and a one-line limit where wrapping is fine (lineLimit in SwiftUI, numberOfLines = 0 in UIKit). Check the screen at the default size, the largest standard size and the largest accessibility size, using the Dynamic Type variants of SwiftUI previews, Xcode's Environment Overrides while the app runs, or the Settings pane of Accessibility Inspector, and run the inspector's audit with the Dynamic Type and Clipped text checks on. Some layouts break at the largest sizes (e.g. items side by side run out of room): fix what is simple, and for the rest write down what happens and ask your reviewer how the team handles it. Evidence: a link to the merged PR, screenshots at the three sizes before and after, the audit result, and a note on what still breaks and what your reviewer said.
09Concurrency (iOS)
Writing correct concurrent Swift: async/await, tasks and task groups, actors, Sendable and main-actor isolation under Swift 6, GCD and operation queues in older code, async streams, and avoiding races and deadlocks.
TargetNovice
Concurrency (iOS)
Writing correct concurrent Swift: async/await, tasks and task groups, actors, Sendable and main-actor isolation under Swift 6, GCD and operation queues in older code, async streams, and avoiding races and deadlocks.
Learn how Swift async code suspends, runs in parallel and cancels
Read the 'Concurrency' chapter of The Swift Programming Language, docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/, in particular the sections on defining and calling asynchronous functions, calling them in parallel with async let, task cancellation and the main actor. Then pick one screen in a real or test app that loads data asynchronously (e.g. a profile or feed screen, yours or one you're about to build) and annotate its loading code: mark every await as a point where the function can pause, say which code must run on the main actor and why, and write what should happen if the user leaves the screen halfway through loading. If two of its requests don't depend on each other, say how async let would change the code. Evidence: the annotated code (a PR comment, gist or screenshot) and your mentor's comments on it.
Load a screen's data with async/await and cancel it when the user leaves
On a real or test project, take a ticket that loads data for one screen (e.g. a list from an API or a user profile) and implement it with async/await, using what you noted in the exercise above. Start the work from SwiftUI's .task modifier, or from a Task you keep and cancel in UIKit (for example in viewDidDisappear), so leaving the screen cancels the load. Update the UI only on the main actor. Treat cancellation as a normal outcome, not an error: don't show an error alert when the task was canceled (a CancellationError, or a URLError with code .cancelled from URLSession), and check Task.isCancelled (or call Task.checkCancellation()) before doing expensive work after a long await. Your loading code must also call one older completion-handler or DispatchQueue function: one the screen already uses or, if it has none, one a senior points you to in the codebase (on a test project, your mentor can give you one, e.g. a location or image-loading helper with a completion handler). Call it through an async wrapper the senior points you to, or ask them how to write one. When the compiler shows a concurrency error you don't understand (e.g. about Sendable or main actor isolation), ask a senior (on a test project, your mentor), fix it with them and write down what it meant; if you finish without meeting one, ask your mentor for one from the project's warnings or a recent PR and work through it with them. Evidence: a link to the merged PR, a short note on where the task starts and where it gets canceled, a screen recording or log showing you left the screen mid-load without an error, the older code you called and how, the compiler error you got help with and what it meant, and your reviewer's comments.
10Memory management (iOS)
Managing memory in iOS apps: ARC ownership, retain cycles and closure capture, weak and unowned references, finding leaks with the Memory Graph debugger and Instruments, and preventing out-of-memory crashes.
TargetNovice
Memory management (iOS)
Managing memory in iOS apps: ARC ownership, retain cycles and closure capture, weak and unowned references, finding leaks with the Memory Graph debugger and Instruments, and preventing out-of-memory crashes.
Learn how ARC frees objects and how retain cycles stop it
Read the 'Automatic Reference Counting' chapter of The Swift Programming Language, docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/, in particular strong reference cycles between class instances, weak and unowned references, and strong reference cycles for closures with capture lists. Then read the 'Inspect the debug memory graph' section of Apple's 'Gathering information about memory use', developer.apple.com/documentation/xcode/gathering-information-about-memory-use, on what the Debug Memory Graph button shows. Open a screen you wrote or are working on in a real or test app and mark every reference that could form a cycle: a delegate property, a closure stored on self (or on something self owns) that uses self, a timer or notification observer, and a parent and child that point at each other. For each, write who owns whom, whether the reference is strong, weak or unowned today, and whether that is right. Evidence: the annotated code (a PR comment, gist or screenshot) and your mentor's comments on it.
Find and fix a retain cycle in your own screen with the Memory Graph debugger
On a real or test project, run a screen you built on a simulator or device, open and close it five times, then click Debug Memory Graph and search the left-hand list for your screen's view controller, view or view model. If more than one instance (or any, after closing) is still alive, follow the arrows in the graph back to what is holding it, using what you noted in the exercise above. Typical causes: a closure that captures self strongly and is stored by something self owns, a delegate property declared without weak, a Timer that targets self, or a NotificationCenter block observer that is never removed. Fix it with [weak self] in the capture list (or unowned only where the captured object is sure to outlive the closure) or a weak delegate, then repeat the five opens and closes and confirm the instances are gone. If your screens have no cycle, ask your mentor to add one to a copy of your screen without telling you where. Open a PR and ask your reviewer to check the other closures and delegate references in it for cycles you might have missed; fix what they find and record what you missed. Evidence: memory graph screenshots before and after the fix showing the instance count, a link to the PR, a short note on what held the object and why your fix breaks the cycle, and the reviewer's comments with any case you missed and how you fixed it.
11Networking & APIs (iOS)
Talking to servers from an iOS app: URLSession, REST and JSON with Codable, auth tokens, errors, retries and caching, real-time connections such as WebSockets, GraphQL, and client-side API contracts.
TargetNovice
Networking & APIs (iOS)
Talking to servers from an iOS app: URLSession, REST and JSON with Codable, auth tokens, errors, retries and caching, real-time connections such as WebSockets, GraphQL, and client-side API contracts.
Learn how URLSession loads data and Codable decodes it
Watch Apple's WWDC21 session 'Use async/await with URLSession', developer.apple.com/videos/play/wwdc2021/10095/, on loading data with URLSession.shared.data(from:), checking the HTTPURLResponse status code, uploads and downloads, and cancellation. Then read Apple's 'Encoding and Decoding Custom Types', developer.apple.com/documentation/foundation/encoding-and-decoding-custom-types, on automatic Codable conformance, CodingKeys for JSON keys that differ from your property names, and decoding nested JSON by hand. Take one endpoint from the API documentation of a real or test project (your team's backend, or a public API with published docs) and write a Decodable model for its response, with CodingKeys where names differ and optionals only for fields the docs say can be missing or null. Sketch the async function that loads it and list the three ways it can fail: a transport error (a URLError such as .notConnectedToInternet or .timedOut), a response whose status code isn't 2xx (URLSession doesn't throw for a 404 or a 500, which is why the session's code checks the status), and JSON that doesn't match your model (a DecodingError). If the project uses GraphQL, do the same for one query, reading the models its client generates instead of writing them. Evidence: the model and the function (a gist, PR comment or screenshot), your list of failure cases, and your mentor's comments.
Connect a list and detail screen to a documented API with every state
Using what you noted in the exercise above, take a well-scoped ticket on a real or test project that connects a list screen and its detail screen to a documented REST endpoint with URLSession, or to a GraphQL query with the project's client (e.g. Apollo iOS), using async/await and Codable models (or the generated ones). Before coding, compare the docs with a real response (print the raw JSON, or look at it in a proxy such as Proxyman or Charles) and write down the fields you rely on. Show a loading state, an empty state (e.g. ContentUnavailableView in SwiftUI) and an error state, and tell apart no connection, a 404 on the detail screen and a server error, each with a way to retry. Docs are rarely complete, so write down at least one question they leave open (a field that may be null, a date format, what the error body looks like) and ask the backend developer or a senior; if everything matches, ask what an error response contains, since docs often leave it out. Record the question, the role of the person who answered and what you changed because of the answer. Get the PR reviewed. Evidence: a link to the PR, a screenshot of each state, your list of relied-on fields, the contract question with its answer, and your reviewer's comments.
Send a signed-in request and show the server's validation errors
On a real or test project, wire up a create or edit action that needs the signed-in user (e.g. editing a profile, posting a comment, adding an address) and sends a POST, PUT or PATCH request with a JSON body made by JSONEncoder, or a GraphQL mutation, to a documented endpoint. Auth is where most people need help at first, so before you start, ask a senior how the project attaches the user's token to requests (usually a shared client or request builder) and what happens when the token expires, then use that path instead of adding the header yourself. Write down what they explained. On a test project, use a public API that needs a token (e.g. creating an issue in your own test repository with the GitHub REST API) and keep the token out of source control. Disable the button while the request is in flight so it can't be sent twice, keep the user's input when the request fails, and when the server rejects the input (e.g. a 400 or 422 with field errors) show each error next to its field instead of a generic alert. Evidence: a link to the PR, the request and response bodies for one success and one validation failure with tokens and personal data removed, your notes on how auth works in the project and who helped (by role), and your reviewer's comments.
12Data persistence (iOS)
Storing data on device: UserDefaults, files, Core Data, SwiftData and SQLite, schema migrations, and offline-first apps with a local source of truth, a sync queue and conflict resolution.
TargetNovice
Data persistence (iOS)
Storing data on device: UserDefaults, files, Core Data, SwiftData and SQLite, schema migrations, and offline-first apps with a local source of truth, a sync queue and conflict resolution.
Learn where an iOS app keeps settings, files and model data
Read three Apple pages: the overview of UserDefaults, developer.apple.com/documentation/foundation/userdefaults, on what it is for (small, nonsensitive settings) and which value types it holds; the section 'Where You Should Put Your App's Files' in Apple's archived 'File System Basics', developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html, on the Documents, Application Support, Caches and tmp directories and which of them are backed up; and 'Preserving your app's model data across launches', developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches, on SwiftData models, the model container and context, saving and fetching. If your project uses Core Data instead, read 'Setting up a Core Data stack', developer.apple.com/documentation/coredata/setting-up-a-core-data-stack, in place of the SwiftData article. Then make a short inventory of a real or test app: every UserDefaults key it uses, every folder it writes files to, and every model or entity it stores, with what each one holds. Mark anything that looks misplaced (e.g. a long list of records in UserDefaults, user documents in Caches, large files that can be downloaded again sitting in Documents without being excluded from backup). Also mark anything sensitive, such as a token or personal details in UserDefaults: it belongs in the Keychain, which is security work to flag, not to fix here. Evidence: the inventory with your marks, and your mentor's comments on it.
Save a user setting and a file the way the project does
On a real or test project, take a small ticket that needs both a setting and a file, for example a notes screen that remembers the chosen sort order and keeps an unfinished draft, or a document viewer that remembers the last page read and keeps an imported PDF. Store the setting in UserDefaults through the project's settings wrapper if it has one, or with @AppStorage in SwiftUI, and register a default value so a fresh install behaves sensibly. Save the file in the directory the reading in the exercise above points to (Documents for files the user creates or imports, Application Support for files the app needs to run, Caches for anything it can re-create), build its URL with FileManager rather than a hard-coded path, write it with the .atomic option so a crash halfway through doesn't leave a broken file, and handle the file being missing when you read it. Check your work: change the setting and save the file, force-quit and relaunch the app, and confirm both are back; then delete and reinstall the app and confirm the defaults apply. Evidence: a link to the merged PR, a recording of the relaunch and reinstall checks, a sentence on why you chose that directory, and your reviewer's comments.
Add to a SwiftData or Core Data model, with help on the schema change
On a real or test project that uses SwiftData or Core Data, take a ticket that stores structured data, for example letting users mark saved items as favorites, or adding a new Note model linked to an existing one. Insert, fetch (with the sort order and filter the screen needs), update and delete records through the project's model context or data layer, following the patterns already in the codebase and the article from the first exercise. Your change alters the stored schema, so before you write it, ask a senior (on a test project, your mentor) how this project handles schema changes: whether this change can migrate automatically (e.g. a new optional property, or one with a default value), whether it needs a new model version (in Core Data, any change to a model that has shipped needs a new model version, even when the migration itself is automatic; SwiftData migrates simple additions without one), and how to test it. Write down what they told you. Then test the upgrade the way users will get it: run the app built from the main branch, create some records, install your branch over it without deleting the app, and confirm the old records are still there and work with the new feature. Evidence: a link to the merged PR, your notes from the conversation with the senior, a recording or screenshots of the upgrade test, and your reviewer's comments.
13App lifecycle, background & notifications (iOS)
Handling how and when an iOS app runs: app and scene lifecycle, state restoration, background tasks and transfers, and local, remote and silent push notifications.
TargetNovice
App lifecycle, background & notifications (iOS)
Handling how and when an iOS app runs: app and scene lifecycle, state restoration, background tasks and transfers, and local, remote and silent push notifications.
Learn the app and scene lifecycle and how little time the background gives you
Read Apple's 'Managing your app's life cycle', developer.apple.com/documentation/uikit/managing-your-app-s-life-cycle, on the states a scene moves through and what to do at each transition; the ScenePhase page, developer.apple.com/documentation/swiftui/scenephase, on reading the active, inactive and background phases in SwiftUI; and 'Extending your app's background execution time', developer.apple.com/documentation/uikit/extending-your-app-s-background-execution-time, on how soon an app is suspended after it goes to the background and what beginBackgroundTask(withName:expirationHandler:) is for. Then pick one screen of a real or test app that does something time-bound or holds unsaved input (e.g. a running timer, a video or audio player, a half-written form, a list of live data) and write a short table: for becoming active, becoming inactive (e.g. Control Center pulled down, an incoming call), going to the background and coming back, what the screen should do and which callback or phase you'd use (a scene delegate method, a UIScene or UIApplication notification, or scenePhase). Mark any work that might still be running when the app is suspended (e.g. an upload or a save to the server) and write what you think happens to it. Evidence: your table with the marked work, and your mentor's comments on it.
Make your feature pause, save and refresh at the right lifecycle moments
On a real or test project, take a ticket for a feature you're building or fixing where leaving and returning to the app matters, such as the screen from the exercise above. Using your table, handle the transitions: in SwiftUI with .onChange(of: scenePhase), in UIKit through the scene delegate or by observing UIScene notifications (e.g. UIScene.willDeactivateNotification, UIScene.didEnterBackgroundNotification) in the screen. Pause what shouldn't run unseen (a timer, playback, animations), save the user's unsaved input when the scene stops being active by calling the project's existing save code (how the data is stored isn't part of this exercise), and refresh stale data when it becomes active again. Test each transition on a device or the Simulator: go to the Home Screen, open the app switcher, pull down Control Center, lock the device, and come back. Then leave the app while longer work is still running (e.g. an upload, a sync, a large save) and watch in the Xcode console what happens to it. Background limits trip up most people at first, so take what you saw to a senior (on a test project, your mentor) and ask how the project handles work that must finish after the user leaves (asking the system for extra time with beginBackgroundTask, a background transfer, or retrying when the app returns); write down what they told you and apply it. Evidence: a link to the PR, your table updated with what each transition now does, a screen recording of the transitions, the console output from the leave-mid-work test, your note of the senior's advice and what you changed, and your reviewer's comments.
Add a local or remote notification to a documented flow
On a real or test project, take a ticket whose spec says when a notification fires and what it says, for example a reminder an hour before a booked appointment, 'your export is ready', or a remote 'your order has shipped'. For a local notification, follow Apple's 'Scheduling a notification locally from your app', developer.apple.com/documentation/usernotifications/scheduling-a-notification-locally-from-your-app: build the content, pick a time-interval or calendar trigger, give the request a stable identifier so rescheduling replaces it instead of adding a duplicate, and remove the pending request when it no longer applies (e.g. the booking is canceled). For a remote notification, ask a senior (on a test project, your mentor) how the project registers for push and sends test pushes. Real pushes need a paid Apple Developer Program membership and are best tested on a physical device; without them, test your payload on the Simulator by saving it as an .apns file with your app's bundle ID and dragging it onto the Simulator, or with xcrun simctl push (this only simulates delivery; a notification service extension or a background launch needs a real push). Use the permission request the project already has; on a test project, a plain requestAuthorization call is enough for now (when and how to ask is covered at the next level). When the user taps the notification, open the related screen through the app's existing navigation; ask where that code lives. Keep personal or sensitive details out of the notification text. Test it with the app in the foreground (where iOS doesn't show it unless your notification center delegate asks it to), in the background and not running. Evidence: a link to the PR, a screenshot of the notification with personal data hidden, the request code or payload, your test notes for the three app states, and your reviewer's comments.
14System integration & device capabilities (iOS)
Plugging an app into iOS and the device: deep and universal links, widgets, Live Activities and extensions, App Intents for Shortcuts, Spotlight and Siri, permission flows, location, maps, photos and camera.
TargetNovice
System integration & device capabilities (iOS)
Plugging an app into iOS and the device: deep and universal links, widgets, Live Activities and extensions, App Intents for Shortcuts, Spotlight and Siri, permission flows, location, maps, photos and camera.
Learn when iOS asks for permission and how to explain why
Read the 'Requesting permission' section of the privacy page in Apple's Human Interface Guidelines, developer.apple.com/design/human-interface-guidelines/privacy, on asking only when a feature needs the data, writing the purpose string (the sentence the system alert shows under your app's name) and the rules for a screen shown before the alert. Then read Apple's 'Requesting authorization to use location services', developer.apple.com/documentation/corelocation/requesting-authorization-to-use-location-services, on When in Use versus Always access, the Info.plist usage description keys and reacting when the user changes their answer, and the section 'Determine Your App's Access Needs' of 'Delivering an Enhanced Privacy Experience in Your Photos App', developer.apple.com/documentation/photokit/delivering-an-enhanced-privacy-experience-in-your-photos-app, on why the system photo picker needs no permission. Then, for a real or test app, list every feature that uses location, the camera or photos, or would in a planned feature (e.g. 'attach a photo to a report', 'show stores near me'). For each, write whether it needs a permission prompt at all, which Info.plist key, the purpose string you would write, and the moment in the flow when you would ask. Evidence: your table and your mentor's comments on it.
Add a location lookup with its permission request and a clear reason
On a real or test project, take a well-scoped ticket that needs the user's location once (e.g. 'fill in my city', 'sort stores by distance', 'tag a note with where it was written'). Using what you noted in the exercise above, add NSLocationWhenInUseUsageDescription with a purpose string that says what the location is used for, and ask for When in Use authorization only when the user taps the feature, never at launch. Then read one location: with CLLocationManager (requestWhenInUseAuthorization, then requestLocation and its delegate methods, including the error one) or, from iOS 17, with CLLocationUpdate.liveUpdates(), stopping after the first usable location. Handle the edge cases a first version must not break on: the user taps Don't Allow, no location arrives or it arrives slowly (in the simulator, set one under Features > Location), and the user turns off Precise Location so you only get an approximate one. When an edge case stumps you, ask a senior, fix it with them and write down what you learned. Before you merge, go through your edge-case notes with a senior and write down anything they added or would handle differently. Record with a simulated location, or hide your real address. Evidence: a link to the merged PR, your purpose string, a screen recording showing the prompt at the right moment and the app behaving sensibly after Don't Allow, your notes on each edge case and the help you got, and your reviewer's comments.
Let users choose a photo with the system photo picker
On a real or test project, add a photo picker to a screen that needs an image (e.g. a profile picture or an attachment to a support request), using SwiftUI's PhotosPicker, developer.apple.com/documentation/photosui/photospicker, or PHPickerViewController in UIKit. The picker runs outside your app, so it needs no permission and no Info.plist key: confirm your app shows no photo library alert. Load the chosen item (loadTransferable in SwiftUI, the item provider in UIKit) and handle what the PhotosPicker page warns about: loading can fail (e.g. a photo kept only in iCloud while the device is offline), and SwiftUI's Image loads only PNG and JPEG through Transferable, so a HEIC photo needs another way in (e.g. load the Data and create a UIImage). Also handle the user canceling, and scale a large photo down before showing or uploading it. When an edge case stumps you, ask a senior and write down the answer. Before you merge, go through your edge-case notes with a senior and write down anything they added or would handle differently. Evidence: a link to the merged PR, a recording of choosing a photo and of canceling, your note on each edge case you handled and the help you got, and your reviewer's comments.
Open a screen from a simple deep link and learn the app's entitlements
On a real or test project, make one screen reachable from a link (e.g. myapp://order/123 opens that order). Follow Apple's 'Defining a custom URL scheme for your app', developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app: register the scheme under URL Types in the target's Info tab, then receive the URL with SwiftUI's onOpenURL (the page shows only the UIKit methods), or in UIKit with scene(_:openURLContexts:) and, when the app wasn't running, scene(_:willConnectTo:options:). Parse it with URLComponents, check every part and ignore malformed links (e.g. a missing or non-numeric id) instead of crashing. Open the screen through the project's existing navigation; designing the app's routing isn't part of this. Test a cold start and a warm start, from Safari or Notes in the simulator, or with xcrun simctl openurl booted and your link. Before you start, open the target's Signing & Capabilities tab with a senior and ask them to explain each capability listed (each adds an entitlement, a permission written into the app's signature, such as Associated Domains for universal links or App Groups for sharing data with a widget). If the target lists none (common on a new test project), add App Groups with the senior (it works on a free account) and ask them to show you what it writes into the .entitlements file and the provisioning profile. If the project already uses universal links (https links that open the app), ask the senior to show you how they are set up and add your path through the existing handler instead, asking who updates the website's file if the path must be added there. Evidence: a link to the merged PR, the URL format, recordings of a cold open, a warm open and a malformed link being ignored, your notes on the capabilities the senior explained, and your reviewer's comments.
15Security & privacy (iOS)
Protecting users' data and identity: Keychain and data protection, secure networking, sign-in with OAuth, Sign in with Apple and passkeys, third-party SDK risk, and Apple's privacy rules such as privacy manifests.
TargetNovice
Security & privacy (iOS)
Protecting users' data and identity: Keychain and data protection, secure networking, sign-in with OAuth, Sign in with Apple and passkeys, third-party SDK risk, and Apple's privacy rules such as privacy manifests.
Learn where an iOS app may keep secrets and personal data
Read three short pieces: Apple's 'Using the keychain to manage user secrets', developer.apple.com/documentation/security/using-the-keychain-to-manage-user-secrets, on storing a credential after sign-in, reading it back and deleting it on sign-out; OWASP's 'M1: Improper Credential Usage', owasp.org/www-project-mobile-top-10/2023-risks/m1-improper-credential-usage, on why credentials hardcoded in source code or configuration files are easy for attackers to find; and the section 'Redact Sensitive User Data from a Log Message' in Apple's 'Generating Log Messages from Your Code', developer.apple.com/documentation/os/generating-log-messages-from-your-code, on which values Logger hides by default and what the private and public options do. Then find your team's security rules for the iOS app (a README section, a PR checklist or a wiki page; if nothing is written down, ask a senior what the rules are and note their answer). Pick one feature of a real or test app that handles something sensitive (e.g. a sign-in screen, a profile with an email address and phone number, a saved payment method) and make a table of every sensitive value it touches: what it is, where it lives now (Keychain, UserDefaults, a file, memory, source code), whether it ever appears in a log, and where it should live according to the rules and the reading. Evidence: the rules you found or were told, your table, and your mentor's comments on it.
Store a sign-in token in the Keychain and remove it on sign-out
On a real or test project, take a well-scoped ticket that keeps a credential on the device, for example keeping the user signed in after the app restarts, or saving the access token your sign-in endpoint returns. Store it in the Keychain, using your project's Keychain helper if it has one, or SecItemAdd, SecItemCopyMatching and SecItemDelete as in the article from the exercise above; never in UserDefaults, a plist or a file. Choose its accessibility (the kSecAttrAccessible value that says when the item can be read, e.g. only while the device is unlocked) with a senior, using Apple's 'Restricting keychain item accessibility', developer.apple.com/documentation/security/restricting-keychain-item-accessibility. Delete the item on sign-out. Attaching the token to requests and refreshing it are networking work; this ticket is about where it lives. Whenever you're unsure whether something is safe (e.g. whether a value counts as a secret, or which accessibility to pick), ask a senior before going on, and keep a short log of each question and answer. Then check your work: relaunch the app and confirm you're still signed in, sign out and confirm the item is gone (a read returns errSecItemNotFound), and make sure no print or log line in your change outputs the token. Leave real tokens and passwords out of everything you hand in. Evidence: a link to the merged PR, a screen recording of the relaunch and sign-out checks, your log of questions and answers, and your reviewer's comments.
Check an app's logs, code and Info.plist against the security rules
On a real or test app, check the other rules from the first exercise and fix what you find with a senior's help. Logs: run the app's main flows on a device connected to your Mac with the Console app filtered to your app's process, and watch the Xcode console too, looking for personal data or secrets (emails, names, phone numbers, locations, tokens). Fix each one by removing the value or logging it through Logger with the default redaction or privacy: .private, never .public. Secrets in code: search the repository for API keys, passwords, client secrets and tokens (e.g. git grep for 'apiKey', 'secret', 'token' or 'Bearer') in Swift files, Info.plist, .xcconfig files and build scripts, and run the strings command on the built app's binary, since everything shipped inside the app can be read by whoever downloads it. If you find a real secret, tell a senior straight away instead of just deleting it: it is still in the repository's history and may need to be replaced. HTTPS only: open the Info.plist and look for App Transport Security exceptions under NSAppTransportSecurity (such as NSAllowsArbitraryLoads or NSExceptionAllowsInsecureHTTPLoads) and for http:// URLs in code, using Apple's 'Preventing Insecure Network Connections', developer.apple.com/documentation/security/preventing-insecure-network-connections. For each exception, ask a senior why it exists and write down the answer. Evidence: your findings for each rule with what you did about each, links to the fix PRs, the questions you asked and the answers, with secret values and other people's personal data hidden, and your reviewer's comments.
16Performance & profiling (iOS)
Measuring and improving iOS app performance with Instruments and field metrics: launch time, hangs, scrolling smoothness, energy use and app size.
TargetNovice
Performance & profiling (iOS)
Measuring and improving iOS app performance with Instruments and field metrics: launch time, hangs, scrolling smoothness, energy use and app size.
Learn to find what blocks the main thread with Time Profiler and Hangs
Watch Apple's WWDC23 session 'Analyze hangs with Instruments', developer.apple.com/videos/play/wwdc2023/10248/, on what counts as a hang, why main-thread work should stay well under 100 ms, and how the Time Profiler and Hangs instruments tell a busy main thread from a blocked one. Read Apple's 'Understanding hangs in your app', developer.apple.com/documentation/xcode/understanding-hangs-in-your-app, on the main thread and the main run loop. Then profile one screen you built in a real or test app: choose Product > Profile in Xcode, pick the Time Profiler template (it includes the Hangs instrument), and record yourself opening and using the screen. Use a real device if you can get one, since the Simulator runs on your Mac's processor and its timings don't match a phone; say so if you used it. In the call tree, turn on Hide System Libraries to find the heaviest main-thread work in your own code, and look at every hang Instruments marks. For each, write what the code is doing, whether the main thread was busy or blocked, and whether that work needs to run on the main thread at all. Evidence: annotated screenshots of the trace, your notes, and your mentor's comments on them.
Move slow work off the main thread in your screen and measure again
Using the trace and notes from the exercise above, fix the straightforward main-thread problems in your screen on a real or test project. Typical ones: parsing a large JSON response or reading a file synchronously when the screen opens, creating a new DateFormatter or NumberFormatter for every row, filtering or sorting a big array inside a SwiftUI body or a cell configuration method, or a synchronous network call. Move the work off the main thread the way your project already does it (ask a senior if you're not sure which way), do it once instead of every time (e.g. keep one formatter), or do less of it, and keep only the UI update on the main thread. Then record the same steps again with the same template and device, and compare. While profiling you'll likely meet something you can't fix alone, for example a hang inside a third-party SDK, the main thread waiting on a lock, or slow layout in a complex screen. Don't guess: write down what you saw, ask a senior developer or your mentor how they would approach it, and note what they told you. If you don't meet one, ask your mentor to point you to a deeper issue elsewhere in the app (or one they set up) and do the same. Evidence: a link to the PR, before and after screenshots of the trace with the main-thread time or hang duration for the same steps, a short note on what you changed and why, the deeper issue with the guidance you got, and your reviewer's comments.
Stop large images from slowing down your screen
On a real or test project, take a screen of yours that shows photos or other large images (a feed, a gallery, a profile header). If you have none, build a test screen that shows 50 photos taken with an iPhone camera as small thumbnails in a grid. Profile opening and scrolling it with the Time Profiler template, as in the first exercise above, and with Hide System Libraries turned off look for image decoding on the main thread (frames from the ImageIO library while images are drawn). A photo several thousand pixels wide is often decoded at full size on the main thread just before it's shown, even when it's displayed as a small thumbnail. Fix it in one of these ways, whichever fits your code: prepare the image at the size it's shown with UIImage's prepareThumbnail(of:completionHandler:), decode it ahead of time with prepareForDisplay(completionHandler:) (or the async versions of either), ask your server or image library for a smaller version, or in SwiftUI load the image in a .task modifier instead of inside body, as the WWDC23 session from the first exercise shows. Measure again under the same conditions. The memory this saves is the Memory management skill's topic; here, record how much main-thread time and how many hangs went away. Ask your reviewer to check that images still look sharp on the device. Evidence: a link to the PR, before and after traces with the main-thread decoding time for the same steps, a short recording of the screen, and your reviewer's comments.
17Debugging & production monitoring (iOS)
Finding and fixing defects in iOS apps, locally and in production: Xcode debugger and LLDB, view debugging, crash reports and symbolication, logging, release health, and reproducing field bugs.
TargetNovice
Debugging & production monitoring (iOS)
Finding and fixing defects in iOS apps, locally and in production: Xcode debugger and LLDB, view debugging, crash reports and symbolication, logging, release health, and reproducing field bugs.
Learn to pause, inspect and narrow down a bug in Xcode
Read Apple's 'Diagnosing and resolving bugs in your running app', developer.apple.com/documentation/xcode/diagnosing-and-resolving-bugs-in-your-running-app, for the reproduce, pause, inspect and step loop, how a crash's symptoms tell you which kind of breakpoint to set, and when to use the view debugger for appearance and layout problems. Then watch Apple's WWDC24 session 'Run, Break, Inspect: Explore effective debugging in LLDB', developer.apple.com/videos/play/wwdc2024/10198/, on debugging as a search problem, breakpoints with conditions and actions, and inspecting values with the p command. Apply both to one bug you meet in a real or test project (yours, or one your mentor points you to): instead of adding print calls, stop at the right place with a breakpoint (a conditional breakpoint, or a Swift error or exception breakpoint if it fits), step through, and inspect values with p until you see where they go wrong. Evidence: a short note with the bug, the breakpoint you used and why, the LLDB output at the moment the value went wrong, and a link to the commit with the fix, checked by your mentor.
Reproduce and fix logic bugs with the debugger, and ask when one won't reproduce
Take a small bug ticket from your real or test project's backlog where the app shows wrong data or behaves wrongly (e.g. a total that doesn't add up, a filter that drops items, a button that does nothing after the screen reappears). Using what you practiced in the exercise above, first write exact reproduction steps (device or simulator, iOS version, test account or data, the taps), then find the cause on your own: read the Xcode console for errors and warnings, set breakpoints, inspect values with p, and step until you find the line that goes wrong. Fix it and confirm the steps no longer reproduce it. Also take one ticket that doesn't reproduce the first time (marked 'sometimes', 'can't reproduce' or 'only on some devices'; if your backlog has none, ask your mentor to pick or set one up): try other devices, iOS versions, data and timing, write down everything you tried, then ask a senior developer or your mentor for help and note what they suggested and what you found. Evidence: the PR link for the fix with its reproduction steps and a two- or three-sentence root cause, your notes on the bug that wouldn't reproduce with the help you got and the outcome, and a reviewer's comments.
Find a layout bug with the view debugger
Take a layout or appearance bug from your real or test project (e.g. a label cut off at a large text size, a view hidden behind another, a button that can't be tapped, a wrong color in dark mode). Reproduce it, using Xcode's environment overrides for text size or appearance if it only happens with those settings, then pause the app with Debug View Hierarchy and find the view that is wrong: check its frame, which views overlap it, and its constraints or modifiers in the inspector. On UIKit screens, also read the console for unsatisfiable constraint messages and match them to the views involved. Fix it and check the same settings again. Evidence: a screenshot of the view debugger showing the problem view, a short note on what you found, before and after screenshots, and the PR link with a reviewer's comments.
18Automated testing (iOS)
Testing iOS apps automatically: unit tests with Swift Testing and XCTest, UI tests with XCUITest, snapshot tests, testable design through dependency injection, and a balanced test pyramid.
TargetNovice
Automated testing (iOS)
Testing iOS apps automatically: unit tests with Swift Testing and XCTest, UI tests with XCUITest, snapshot tests, testable design through dependency injection, and a balanced test pyramid.
Learn Swift Testing's building blocks and your project's test patterns
Watch Apple's WWDC24 session 'Meet Swift Testing', developer.apple.com/videos/play/wwdc2024/10179/, on @Test functions, the #expect and #require expectations, traits, suites, tests that run with different arguments (parameterized tests) and how Swift Testing and XCTest compare. If your project's unit tests use XCTest, also skim Apple's 'Migrating a test from XCTest', developer.apple.com/documentation/testing/migratingfromxctest, which shows how XCTest assertions such as XCTAssertEqual translate to Swift Testing. Then open two existing unit test files in a real or test project (your team's app, or a sample app you add tests to) and write down the patterns they follow: which framework they use, where test files live and how they are named, how the code under test is created, and how fake data or fake services are set up. Finally, pick a function or type you wrote recently and list three behaviors you would test, each with its input and expected result, including one edge case (e.g. an empty list, a nil value, an invalid email address). Evidence: your pattern notes with links to the two test files, your three behaviors, and your mentor's comments.
Ship your next ticket with unit tests in the same pull request
On a real or test project, take your next ticket that adds or changes logic (e.g. formatting a price, validating a form, sorting or filtering a list, or a view model that turns an API response into screen state) and include unit tests in the same pull request without anyone asking for them. Follow the patterns you noted in the exercise above: the same framework (Swift Testing or XCTest), file layout, naming and way of creating fake data. Test what the code does from the outside, not its private details, and cover at least one failure or edge case. Ask a senior or your mentor about at least one case where you aren't sure what or how to test (e.g. code that reads the current date, UserDefaults, a network call, a timer, or async work in a view model); if your ticket has none, your mentor points one out in nearby code for you to test. Write down what they suggested and why. Evidence: the PR link with tests passing locally or in CI, the PR history showing nobody had to ask you for tests, the existing test you used as a pattern, and the tricky case with the advice you got and what you did with it.
Pin a bug with a failing test before you fix it
On a real or test project, take a bug assigned to you (or one you find) in logic a unit test can reach, such as a wrong total, a date shown in the wrong format, or a list that drops an item. Before changing any app code, write a unit test that reproduces the bug and fails for the right reason: the failure shows the wrong value the user saw, not a crash in your test setup. If the bug depends on the input, use a Swift Testing parameterized test (the arguments: parameter of @Test) or several XCTest cases to cover the failing value and its neighbors. Commit the test, then fix the bug and show the same test passing. Revert the fix locally to confirm the test fails again. Evidence: the PR link with separate commits for the failing test and the fix, the failing then passing test output, and two or three sentences on why this test will stop the bug from coming back, reviewed by a senior or your mentor.
19Code quality & code review
Keeping a codebase healthy (linting and formatting, complexity analysis, readable code) and reviewing others' changes, including AI-generated ones, so problems are caught and knowledge spreads.
TargetNovice
Code quality & code review
Keeping a codebase healthy (linting and formatting, complexity analysis, readable code) and reviewing others' changes, including AI-generated ones, so problems are caught and knowledge spreads.
Ship four PRs that pass lint and need few comments, and track the feedback
Over your next four pull requests on a real or test project, run the team's linter, formatter and type check locally before pushing, so CI passes on the first try, and do a self-review of the full diff before asking anyone to look (remove debug code, unclear names, dead code, missing tests). When comments arrive, handle each one yourself: fix it, or reply with a short reason if you disagree, and resolve the thread only once the reviewer is satisfied. Keep a simple log per PR: how many comments you got, what kind they were (bug, naming, structure, style, tests) and what you changed. At the end, pick the most common kind and add a line for it to your personal self-review checklist. Evidence: links to the four PRs with green lint and CI, your comment log, your self-review checklist, and a short note to your mentor on the trend you see.
Self-review a merged PR of yours against Google's reviewer checklist
Read Google's engineering practices page 'What to look for in a code review,' google.github.io/eng-practices/review/reviewer/looking-for.html. Then pick one of your own merged pull requests from a real or test project and review it as if a teammate had written it, going through each area the page lists. Write down everything you would now flag, which area it falls under, and whether the original reviewer caught it. Pick the two findings that matter most and say what you will do differently in your next PR. Evidence: a link to the PR, your written self-review organized by the page's areas, and your mentor's response to it.
Review two small PRs from peers with your mentor checking your comments
Ask to be added as a reviewer on two small pull requests from teammates on a real or test project (a bug fix, a copy change, a small component). For each one, check out or open the preview, confirm the change does what the ticket says, and leave comments: at least one question or suggestion if you find something and at least one note on something done well. Label minor points as nits (small, non-blocking suggestions such as a nicer name) so the author knows what is blocking. Before posting, show your draft comments to your mentor or a senior reviewer and adjust them based on their feedback. Evidence: links to both reviews, your draft comments next to what you actually posted, and one sentence on what the senior reviewer caught that you missed.
20Version control (iOS)
Using Git on iOS projects: clean commits and branches, pull requests, branching strategies, resolving conflicts including Xcode project files, and handling large binary assets.
TargetNovice
Version control (iOS)
Using Git on iOS projects: clean commits and branches, pull requests, branching strategies, resolving conflicts including Xcode project files, and handling large binary assets.
Learn to write commit messages a reviewer can follow
Read cbeams' 'How to Write a Git Commit Message', cbea.ms/git-commit/, and note its seven rules and its test for a good subject line. Then take the last ten commits you authored (or all of them, if you have fewer) on a real or test iOS project and judge each message against the article and your team's own convention if it has one (for example a ticket number prefix or Conventional Commits). Also look at what each commit contains, and mark any that mix unrelated changes, such as a Swift fix together with a storyboard change Xcode made when you only opened the file, or that include files that don't belong in Git, such as your xcuserdata folder (your personal Xcode state, like breakpoints and open tabs). Pick the five weakest commits and rewrite each message (a subject and, where the change needs it, a body that says why), without actually rewriting shared history, and say how you would have split the mixed ones. Evidence: a before and after list of the five messages with links to the original commits, one sentence per message on what you fixed, your note on the mixed commits, and your mentor's comments.
Learn how branches merge and what Git sees in an Xcode project
Read Pro Git's 'Basic Branching and Merging', git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging, in particular 'Basic Merge Conflicts', which shows what conflict markers look like and how to finish a merge once you've fixed them. Then read the 'Convert groups to folders' section of Apple's 'Managing files and folders in your Xcode project', developer.apple.com/documentation/xcode/managing-files-and-folders-in-your-xcode-project, which explains why adding a file to a group changes the project file and adding it to a folder doesn't. The project file is project.pbxproj, a text file inside your .xcodeproj that lists the project's files, targets and build settings. On a real or test iOS project, find out how the team works with Git (from a CONTRIBUTING file, the README or a teammate): how branches are named, which branch pull requests go into and how they get merged. On a test project or if you work alone, agree a simple workflow with your mentor instead (e.g. branches named feature/short-name, pull requests into main, squash merge) and write it down. Then check what Git sees: on a scratch branch, add an empty Swift file in Xcode, run git status and note whether project.pbxproj changed, then discard the change (Integrate > Discard All Changes) and delete the branch. Evidence: a short note on the team's workflow, the git status output, and your explanation of what it means when two people add files on different branches at the same time, reviewed with your mentor.
Deliver a ticket through the team's Git workflow on an iOS project
On a real or test iOS project, take a well-scoped ticket (e.g. a copy change and a new button on one screen) and deliver it through your team's Git workflow on your own, using what you noted in the two exercises above. Create a branch named to the team's convention. Make small commits that each do one thing: stage only the lines you mean to commit, in Xcode's Integrate > Commit view or with git add -p, and leave out changes Xcode made that have nothing to do with the ticket. Keep your branch up to date with the main branch, open a pull request with a clear description (what, why, how to test, and a Simulator screenshot or recording for UI changes, with no real personal data on screen) and answer review comments with follow-up commits. On a test project, your mentor reviews the pull request. Resolve at least one simple merge conflict in a Swift file yourself, in Xcode's conflict view or in a text editor; if none happens naturally, create one on a test branch by changing the same lines on two branches. Evidence: the pull request link with its commit list and review thread, the commit where you resolved the conflict, and a note on how you chose what to keep.
Get help through a project file conflict, a rebase or a Git mistake
Some Git situations are risky to handle alone when you're new: a conflict in project.pbxproj, a rebase the team asks you to do, or a mistake such as a commit on the wrong branch or a branch you reset by accident. On a real or test iOS project, the first time one of these happens, stop and ask your mentor or a senior teammate to help you through it instead of guessing (for example, keeping only one side of a project file conflict can silently drop files from the project). If none happens within two weeks, ask your mentor to set one up with you: on two test branches, each of you sets the app target's Minimum Deployments version (in the target's General tab) to a different value, then you merge one branch into the other, which conflicts in project.pbxproj. If the project adds files to groups rather than folders (the Convert groups to folders section you read in the second exercise explains the difference), also have each branch add a new Swift file to the same group, so you also see a conflict where both sides must be kept. Work through it with them: read the conflict, decide what to keep, finish the merge and check that the project still opens and builds. Write down what happened, what you would have done on your own, the commands or Xcode steps that fixed it and what you'll check next time. Evidence: your note, the resolved commit or the terminal output, a screenshot of the project building afterwards, and your mentor's or the senior's comments.
21Dependency management (iOS)
Managing an iOS app's third-party and internal dependencies: Swift Package Manager, binary frameworks, choosing, updating and removing dependencies, and migrating off CocoaPods.
TargetNovice
Dependency management (iOS)
Managing an iOS app's third-party and internal dependencies: Swift Package Manager, binary frameworks, choosing, updating and removing dependencies, and migrating off CocoaPods.
Learn Swift Package Manager's version rules and what Package.resolved pins
Read Apple's 'Adding package dependencies to your app', developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app, in particular 'Decide on package requirements' (the version rules Xcode offers: up to the next major or minor version, a range, an exact version, a branch or a commit) and 'Coordinate package versions across your team', on the Package.resolved file and why it's committed. Then read the summary at the top of Semantic Versioning, semver.org/, on what a major, minor and patch number promise. In a real or test iOS project, select the project in the Project navigator, open its Package Dependencies tab and pick up to five packages, ideally with different rules (if your project has no Swift packages, use a fork of an open source iOS app that has some). For each, write the rule, which versions it allows, the exact version Package.resolved pins today, and what File > Packages > Update to Latest Package Versions would be allowed to change. Finish with two sentences on why Package.resolved is committed and what would go wrong if it weren't. Evidence: a link to Package.resolved at that commit (or a screenshot of the Package Dependencies tab), your table and the two sentences, checked by your mentor.
Update packages within their version rules and commit a clean Package.resolved
On your own branch of a real or test iOS project, update the Swift packages that have newer versions within their rules, using what you noted in the exercise above: choose File > Packages > Update to Latest Package Versions in Xcode (Apple's article above notes that the update and resolve actions live in the File > Packages menu). If every package is already at the newest version its rule allows, do this on a fork of an open source iOS app whose Package.resolved is a few months old, or ask your mentor for an older branch of your project. Read the Package.resolved diff and list which packages moved, from which version to which, and skim each one's release notes for anything that touches your code. Build, run the tests, check the screens that use the updated packages and fix anything that breaks. Commit Package.resolved with a message that lists the updates, and check that your CI build passes with it (if your project has no CI, build it from Terminal the way CI would, xcodebuild -resolvePackageDependencies -disableAutomaticPackageResolution followed by a build of your scheme, and attach the output); Apple's 'Building Swift packages or apps that use them in continuous integration workflows', developer.apple.com/documentation/xcode/building-swift-packages-or-apps-that-use-them-in-continuous-integration-workflows, explains why CI should use the committed file and the -disableAutomaticPackageResolution flag that makes xcodebuild stick to it. If Package.resolved conflicts with another branch, don't merge it by hand: take the main branch's version, let Xcode resolve again (File > Packages > Resolve Package Versions) and check the result. Evidence: the pull request link with a passing CI run (or your command-line build output), your list of updated packages with versions, and a short note on anything the release notes made you check.
Ask before adding a package or doing a major upgrade, then add it the right way
The next time a task seems to need a new Swift package, or a new major version of one your real or test iOS project already uses, stop and write a short request to your reviewer or a senior (on a test project, your mentor) before adding it. If nothing comes up within two weeks, pick a need you would otherwise solve with a package (e.g. image caching, a chart, Keychain access) and write the request for that. Say what you need it for and the alternatives you checked: a package already in the project, an Apple framework (e.g. Swift Charts or FormatStyle), or a few lines of your own code. Add the package's latest release date, its license, whether it's source or binary, and the version rule you'd use; for a major upgrade, link the release notes and name the breaking changes that touch your code. If it's approved, add it with the + button in the project's Package Dependencies tab (or the add-package item in Xcode's File menu), choose the rule you proposed and add its product only to the target that needs it. If it isn't, use the alternative, and add a package your mentor agrees on to a test project so you still practice the steps. Evidence: your request, the reply (with the person's name hidden), the pull request or test branch that adds the package with its Package.resolved change, and what you did instead if the answer was no.
22Build, signing & CI/CD (iOS)
Producing signed iOS builds reliably: Xcode schemes, targets and configurations, code signing and provisioning, CI with fastlane, Xcode Cloud or similar, branching strategy, and automated dependency updates.
TargetNovice
Build, signing & CI/CD (iOS)
Producing signed iOS builds reliably: Xcode schemes, targets and configurations, code signing and provisioning, CI with fastlane, Xcode Cloud or similar, branching strategy, and automated dependency updates.
Learn how schemes, configurations and run destinations decide what you build
Read Apple's 'Running your app on simulated or physical devices', developer.apple.com/documentation/xcode/running-your-app-on-simulated-or-physical-devices, on choosing a scheme and a run destination and setting up signing for a physical device, and 'Customizing the build schemes for a project', developer.apple.com/documentation/xcode/customizing-the-build-schemes-for-a-project, on what a scheme holds: the targets it builds, the build configuration each action (Run, Test, Profile, Archive) uses, and launch arguments and environment variables. Then open a real or test project and write a short map: each shared scheme and what it's for, which build configuration (e.g. Debug, Release, or a project-specific one such as Staging) each of its actions uses, which server or environment the app talks to in each configuration and how you can tell (e.g. a different app name or icon, a settings screen, a log line), and which targets the main scheme builds. Mark anything you couldn't work out as a question. Evidence: your map with its open questions, and your mentor's answers.
Get the app running on simulators and your own device from a clean clone
On a real or test project, start from a fresh clone and get the app building and running on your own, using the map from the exercise above. Pick the scheme and configuration your team uses for daily work (e.g. Debug against the staging server), let Xcode resolve the Swift packages, and run the app on two simulators that differ in screen size and iOS version (download a simulator runtime if Xcode lacks one). Then run it on a physical iPhone or iPad: pair it with your Mac, turn on Developer Mode as Apple's 'Enabling Developer Mode on a device', developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device, shows, and let Xcode sign it. For your team's app you need to be on the team's Apple Developer account and your device may need registering, so ask whoever manages the account; for a test project, a personal team with a free Apple Account is enough to run on your own device. No device of your own? Borrow one from the team or your mentor. Signing errors are where most people need help at first (e.g. no profile found for the bundle identifier, a device missing from the profile): when you hit one, ask a senior or your mentor, fix it with them and write down what it meant. If you don't hit one, ask your mentor to set one up on a test project (e.g. a bundle identifier another team already uses) and work through it with them. Then switch to the configuration your team uses for QA or release testing and check the app talks to the right server. Fix whatever the README got wrong in a PR. Evidence: screenshots of the app on both simulators and the device (with personal data hidden), the signing error you got help with and what it meant, how you confirmed which server each configuration uses, and the README PR.
Fix your own CI failures by reproducing them locally
Over your next few PRs on a real or test project, fix your own CI failures: read the failing job's log (Xcode Cloud's build report, your GitHub Actions or other CI run, or fastlane's output), find the exact command or action that failed, reproduce it on your Mac with the same scheme, configuration and destination (e.g. xcodebuild test -scheme YourApp -destination 'platform=iOS Simulator,name=iPhone 17'), fix it and push. Aim for three failures of different kinds, for example a compile error that only shows with CI's Xcode version, a test that fails on CI's simulator, a lint error, or a new file that wasn't added to the target or wasn't committed. If you don't get three, cause one on purpose on a throwaway branch. If a failure comes from signing, build settings or the pipeline's own setup rather than your code (e.g. an expired certificate, a changed build setting, a missing CI secret), don't change the pipeline yourself: ask whoever owns it, and note the cause and what they did. On a test project without CI, set up a minimal GitHub Actions workflow with your mentor that builds the app and runs the tests on a simulator. Evidence: links to the failed and fixed runs, for each a two-line note with the failing command, the cause and how you reproduced it locally, and your note on any failure you got help with.
23Analytics instrumentation & feature flags (iOS)
Instrumenting iOS apps to learn from real use: tracking events from a plan and verifying them, remote config and feature flags, A/B test assignment and exposure logging, and consent-aware analytics setup.
TargetNovice
Analytics instrumentation & feature flags (iOS)
Instrumenting iOS apps to learn from real use: tracking events from a plan and verifying them, remote config and feature flags, A/B test assignment and exposure logging, and consent-aware analytics setup.
Learn how tracking plans name events and how a feature flag hides unfinished work
Read Twilio Segment's 'Naming conventions: why you need them for clean data', www.twilio.com/en-us/resource-center/naming-conventions-for-clean-data, on the object-action framework (e.g. 'Product Viewed', 'Order Completed'), choosing the properties for each event, consistent casing, and why event names must never be built from changing values. Ignore the article's examples that send an email address as a property: on your project, personal data never goes into an event (see the next exercise). Then read the opening story ('A Toggling Tale') and the 'Release Toggles' section of Pete Hodgson's 'Feature Toggles (aka Feature Flags)' on martinfowler.com, martinfowler.com/articles/feature-toggles.html, on shipping unfinished code switched off. Now open your real or test project's tracking plan (the spreadsheet or tool where the team lists its events; on a test project, ask your mentor for a small one) and pick five events. For each, write its name, its properties with their types, which naming rule it follows or breaks, and where in the app it should fire. Then find one feature flag in the code (on a test project with no flags, ask your mentor to add one, e.g. a Firebase Remote Config boolean read through a small wrapper) and write where its value comes from, what the app does when it is off, and how you would turn it on locally while developing. Evidence: your notes on the five events and the flag, and your mentor's comments.
Add your feature's events from the tracking plan and check they fire
On a real or test project, take a ticket that adds or changes a screen and has events in the tracking plan (e.g. a search screen with 'Search Performed' and 'Search Result Selected'), and add them through the project's analytics wrapper or SDK, using exactly the names, properties and types the plan lists. On a test project, add Firebase Analytics (free) if it has no SDK yet, and take two events from the tracking plan your mentor gave you in the exercise above. Never put personal data in an event: no names, email addresses, phone numbers, text the user typed or precise location; use the IDs or categories the plan asks for. Check that every event fires once, at the right moment, with the right values, using your SDK's debug tools: for Firebase, the -FIRAnalyticsVerboseLoggingEnabled launch argument prints events in the Xcode console and -FIRDebugEnabled sends them to DebugView, as Firebase's 'Log events', firebase.google.com/docs/analytics/events?platform=ios, and 'DebugView', firebase.google.com/docs/analytics/debugview, pages show; other SDKs have a similar debug log. Watch for events sent twice (e.g. from both a button action and the screen it opens) and for missing values. Ask your reviewer to check the names and properties against the plan, and fix what they find. Evidence: a link to the merged PR, a screenshot or log of each event with its properties (user identifiers hidden), the reviewer's comments on names and properties, and what you changed because of them.
Build new work behind an existing feature flag
On a real or test project, build your next ticket's change behind a feature flag, using what you noted in the exercise above: a flag the team already has for this work or, on a test project, the one your mentor set up for the exercise above, or a new one created the same way as the existing ones if your senior asks for that. Read the flag through the project's flag service or wrapper, not straight from the vendor SDK inside a view, and check it in as few places as possible (e.g. one check that picks the old or the new screen, not checks spread through the view). With the flag off, the app must behave exactly as before; with it on, the new behavior appears. Turn the flag on and off locally (e.g. with a debug menu, a launch argument or the local override your project provides) and check both paths, including what happens when the flag has no value yet, such as on a first launch without network. If the project's tests can set flag values, add a test for each side. Ask your reviewer to check the flag's name and default value. Evidence: a link to the merged PR, screenshots or recordings with the flag off and on, the tests or a note on how you checked both paths, and the reviewer's comments.
24AI-assisted development (iOS)
Using AI coding assistants and agents, in Xcode and outside it, well: giving good context, breaking work down, verifying and reviewing their output, knowing when not to trust it, and managing security and licensing risk.
TargetNovice
AI-assisted development (iOS)
Using AI coding assistants and agents, in Xcode and outside it, well: giving good context, breaking work down, verifying and reviewing their output, knowing when not to trust it, and managing security and licensing risk.
Learn Xcode's coding assistant and a habit for checking AI code
Read Apple's 'Writing code with intelligence in Xcode', developer.apple.com/documentation/xcode/writing-code-with-intelligence-in-xcode, on starting a conversation with an agent or chat model, adding context with @ references to files and symbols, reviewing changes in the artifacts pane, undoing them and rolling back with the conversation history. Then read Simon Willison's 'Here's how I use LLMs to help me write code', simonwillison.net/2025/Mar/11/using-llms-for-code/, especially 'Account for training cut-off dates', 'Context is king' and 'You have to test what it writes!'. The same habits apply if you use a tool outside Xcode. Before you use any assistant on a real project, find out your team's rules: which tools are allowed and what may be shared with them (in Xcode, also read 'About Intelligence in Xcode & Privacy' in Xcode > Settings > Intelligence). Write a short personal checklist of what you'll do before committing AI output (what you read line by line, what you build and run, which tests you run, what you look up in Apple's documentation) and apply it to one piece of code an assistant produced for you on a real or test iOS project. Evidence: your checklist, the team's rules you found (or a note that there are none), the code before and after your check (a commit or pull request link), and at least one thing the checklist caught, reviewed with your mentor.
Ship a well-scoped ticket with AI help, checked and disclosed
Pick a well-scoped ticket on a real or test iOS project (e.g. a small SwiftUI view, a Codable model for one API response, a formatter with its unit tests, or a compiler warning you fix with Generate Fix for Issue) and use an AI assistant for part of it on your own: the coding assistant in Xcode (agents need Xcode 26.3 or later; where it's available, ChatGPT in Xcode works without an account, Claude and most agents need you to sign in, and a model running on your own Mac needs none; if your company has turned the assistant off, use a tool your team allows outside Xcode) or a tool your team allows outside Xcode. Follow the team's rules from the exercise above, and keep secrets, API keys and real user data out of prompts and attached files. Apply your checklist: read every line it wrote, build, run the screen in the Simulator, run the tests, add or update tests for the AI-written parts, and change anything you don't fully understand or agree with. If it used an API you don't know, look it up in Apple's documentation and check that it exists on your app's minimum iOS version. In the pull request description, tell reviewers which parts came from AI and how you checked them. Evidence: the pull request link with the AI disclosure and passing tests, and a note listing what you changed in the AI output and why.
Soft Skills
01Communication
Clearly and effectively conveying information, setting expectations, and aligning with others in written and verbal form.
TargetNovice
Communication
Clearly and effectively conveying information, setting expectations, and aligning with others in written and verbal form.
Spot a missing BLUF and rewrite it
Read Wikipedia's article on the BLUF (bottom line up front) technique, en.wikipedia.org/wiki/BLUF_(communication), which explains the practice of opening a message with its key information, responding to the five Ws (who, what, where, when, why), before any narrative detail or supporting context. Take this draft status update: 'Hey team, just wanted to give an update on the onboarding redesign. We've been working on wireframes this week and met with two stakeholders. There were some questions about scope timing. Let me know if you have questions.' Identify what's missing per the BLUF technique, then rewrite its opening sentence as a proper BLUF. Evidence: your written note on what was missing plus the rewritten BLUF opening sentence.
Write a status update that needs zero follow-ups
Using the BLUF technique from the previous exercise, write a full status update (what's done, what's next, what's blocked) for a real or test project you're working on. Show it to a real reader who doesn't already know the details and ask them to tell you what's happening without asking you a single follow-up question. Evidence: the status update plus a note on whether your reader needed a follow-up question, and what it was if so.
02Collaboration
Working well with others: understanding different perspectives, managing shared priorities, and being a reliable team member.
TargetNovice
Collaboration
Working well with others: understanding different perspectives, managing shared priorities, and being a reliable team member.
Confirm what 'reliable help' means for this ask
Read Google's re:Work guide on team effectiveness, rework.withgoogle.com/intl/en/guides/understand-team-effectiveness, which names dependability (reliably completing quality work on time) as one of the five dynamics that separate effective teams. Pick one specific task a teammate has asked you for help with recently, or one you expect to be asked for soon, and write down exactly what 'reliably complete quality work on time' means for that ask: the concrete deliverable, the deadline, and how you'll confirm to the teammate that it's done. Evidence: your write-up naming the task, the deliverable, the deadline, and your confirmation plan.
Help a teammate and name their part in it
The next time a teammate asks you for help with a specific task (or, if none is pending, ask one to give you a real task to help with), do the work, then when it's shared with others (a demo, a doc, a status update, a message to the team), explicitly name your teammate's contribution alongside your own rather than presenting it as solo work. Evidence: the completed help plus a copy or screenshot of the moment you shared credit (the message, doc line, or demo note), showing both your names attached to the shared work.
03Conflict management
Navigating and resolving workplace disagreement in a way that keeps the team functional and relationships intact.
TargetNovice
Conflict management
Navigating and resolving workplace disagreement in a way that keeps the team functional and relationships intact.
Learn what intellectual humility looks like in a disagreement
Read Greater Good Magazine's 'What Does Intellectual Humility Look Like?,' greatergood.berkeley.edu/article/item/what_does_intellectual_humility_look_like, which frames disagreement as healthy when handled with intellectual humility, being open to other people's views instead of contentious about them, and willing to reconsider your own position when shown new evidence. As you read, pull out one behavior the article names for sharing a differing opinion without it becoming contentious, and one behavior it names for updating your own view when shown new evidence. Evidence: the two behaviors you pulled out, each in your own words, with a one-line example of how you'd use it.
Document a real disagreement you handled
Recall a real disagreement you had in a work discussion recently (a design decision, a process choice, a scope call; use the most recent real one you can, or a realistic one if nothing comes to mind) and, with a mentor or reviewer available to read it, write a short account of what your differing opinion was, how you shared it respectfully, and whether any new information came up that changed your mind and how you responded to it. Evidence: the written account plus your reviewer's confirmation that the disagreement, as described, was shared respectfully and that any view change was genuine rather than just conflict avoidance.
04Planning / Time management
Managing your own time and, at higher levels, the team's: prioritizing work so it's delivered efficiently and predictably.
TargetNovice
Planning / Time management
Managing your own time and, at higher levels, the team's: prioritizing work so it's delivered efficiently and predictably.
Learn why context-switching breaks your task list
Read Psychology Today's 'The True Cost of Multi-Tasking,' www.psychologytoday.com/us/blog/brain-wise/201209/the-true-cost-of-multi-tasking, which argues that what feels like multitasking is actually rapid task-switching, and that switching quietly derails commitments you'd otherwise meet by costing up to 40% of your productive time. Then look at your own task list for the coming week and mark the one task you know you'd normally split across multiple sittings instead of finishing in one go, and write 2-3 sentences on the single block of time you'll protect for it instead. Evidence: your task list with the marked task, plus the 2-3 sentence plan for protecting a single block of time for it.
Apply the capture-and-clarify step of GTD to your own list
Read the official overview of the Getting Things Done method, gettingthingsdone.com/what-is-gtd/, focusing on the 'capture' and 'clarify' steps for turning loose commitments into a trustworthy task list. Then take every commitment currently living in your head, chat threads, or email for your individually-scoped work and capture them into one list, clarifying each into a single concrete next action rather than a vague reminder. Evidence: your captured list with each item rewritten as a concrete next action.
Run your own task list for a real work week and report what nearly slipped
For a real (or test) week of individually-scoped work, keep a single task list of your commitments, updating it daily as items get added or finished, and share the starting list with your mentor or reviewer so they know what you're committing to before the week begins. At the end of the week, identify the one commitment you nearly missed and write 2-3 sentences on what caused the near-miss and what you changed on your list to catch it in time. Evidence: your week's task list, the 2-3 sentence note on the near-missed commitment, and your mentor's confirmation they saw your starting list.
05Self-Direction
Motivating yourself to complete work effectively, on time, and without close supervision.
TargetNovice
Self-Direction
Motivating yourself to complete work effectively, on time, and without close supervision.
Read what makes check-ins land, then apply one habit
Read Culture Amp's guide to managing up, www.cultureamp.com/blog/managing-up-importance, which explains that keeping your manager informed in the right way, adapted to their preferred communication style and with issues flagged early rather than saved for a review, is something you actively manage, not something that just happens on its own. Pick one small task you're currently working on with light check-ins from your manager or lead, then pick one habit from the article you don't yet do consistently, and write 2-3 sentences on exactly how you'll change your next check-in on that task because of it. Evidence: your 2-3 sentence write-up naming the task, the habit, and the specific change you're making to your next check-in.
Log your check-ins on one assigned task and review the pattern with your manager
Pick one small task assigned to you and, for its full duration, keep a short log every time you check in with your manager or lead: the date, what you reported, and anything that came up. When the task is done, sit down with your manager and walk through the log together, asking them directly whether the check-in cadence and content felt right, or whether they ever had to chase you for an update instead of you offering one. Evidence: the check-in log covering the whole task, plus your manager's written or verbal reaction on whether the cadence worked and whether they ever had to ask rather than being told.
06Estimating (iOS)
Predicting the realistic effort to build and ship iOS work (sizing tickets, running spikes on unknowns, flagging risks such as App Review and release timing) to inform sprint plans, roadmaps and commitments.
TargetNovice
Estimating (iOS)
Predicting the realistic effort to build and ship iOS work (sizing tickets, running spikes on unknowns, flagging risks such as App Review and release timing) to inform sprint plans, roadmaps and commitments.
Build your reference scale from past iOS tickets
Read Mike Cohn's 'What Are Story Points?', www.mountaingoatsoftware.com/blog/what-are-story-points, which explains relative estimates in terms of amount of work, complexity and uncertainty. Then look through recently finished iOS tickets on a real or test project (your team's board, or your own past tasks) and pick three reference tickets: a small, a medium and a large one, in whatever unit your team uses (points, T-shirt sizes or days). For each, note what made it that size in iOS terms, for example the number of screen states, iPad or landscape layouts, Dynamic Type and VoiceOver checks, a new or an existing API, offline behavior, and tests. Evidence: your three reference tickets with links and sizing notes, checked by a senior or your mentor.
Estimate your own iOS tickets in sprint planning and compare with actuals
In your next sprint planning (or a mock one on a test project with your mentor as the senior), estimate at least three well-defined iOS tickets assigned to you before hearing others' numbers, comparing each with your reference tickets from the exercise above. For each, write down what your estimate includes: loading, empty and error states, iPhone and iPad layouts, accessibility checks, tests, fixes from code review, and testing on a real device. Have a senior sanity-check each number and note where and why they disagreed. At the end of the sprint, record the actual effort and compare. Evidence: a table with each ticket link, your estimate and what it covered, the senior's comment, the actual effort, and two or three sentences on what you would include next time.
07Critical Thinking / Questioning (iOS)
Thinking a technical problem through before solving it: questioning requirements, weighing trade-offs and cost against value, and challenging the first workable answer, including one an AI suggests.
TargetNovice
Critical Thinking / Questioning (iOS)
Thinking a technical problem through before solving it: questioning requirements, weighing trade-offs and cost against value, and challenging the first workable answer, including one an AI suggests.
Learn to ask questions that surface what an iOS ticket leaves out
Read Julia Evans's 'How to ask good questions', jvns.ca/blog/good-questions/, especially the sections on stating what you know and asking 'is that right?', asking questions where the answer is a fact, and asking questions that show what's not obvious. Then take an iOS ticket you already finished on a real or test project and read it again as if you hadn't built it yet. List five questions you now wish you had asked the ticket owner before starting, for example which iOS versions and devices it had to support, how it should look on iPad or in landscape, what happens offline or when a request fails, what the screen shows when the user denies a permission (camera, photos, location, notifications), or how it behaves with large Dynamic Type sizes. Rewrite each one using the article's advice so the owner can answer it in a line, for example: 'I'm assuming that if the user denies photo access we show a message with a button to Settings. Is that right?' Mark which answers would have changed what you built. Evidence: the ticket link, your five rewritten questions, and your mentor's comments on them.
Question an unclear iOS ticket before you start building
For your next two iOS tickets on a real or test project, stop before writing code and list what the ticket doesn't say, phrasing each question the way you practiced in the exercise above. Check at least these: loading, empty and error states; the oldest iOS version the app supports and whether the feature needs anything newer; iPhone sizes, iPad and landscape; what happens offline, on a slow connection, or when the app goes to the background halfway through; what happens when the user denies a permission the feature needs; Dynamic Type and VoiceOver; long or translated text; and anything App Review may look at, such as a paid feature, sign-in or new data the app collects. Send the questions to the ticket owner (product manager, designer or lead; on a test project, your mentor playing one), get answers, and note which ones changed what you built. Evidence: for each ticket, your list of questions, the answers you got (a linked comment thread is fine), and one or two sentences on what you would have built wrong without asking.
Name the trade-off between two ways to build it and talk it through with a senior
Pick a ticket on a real or test project where you can see two workable ways to build it, for example a new screen in SwiftUI or in UIKit in a codebase that has both, a system component (PhotosPicker, ShareLink, a standard List or Form) or a custom one the design shows, a Swift package or a small helper of your own, an API that needs a newer iOS version than the app supports (behind an if #available check with a fallback) or the older API that works everywhere, or extending an existing view versus making a new one. Before choosing, write a short note: what each option costs (time, which iOS versions it works on, code to maintain, app size, accessibility and Dark Mode you'd have to build yourself) and what each gains, and which one you lean toward. Then talk it through with a senior teammate or your mentor and record where they agreed, what they questioned and the final choice. Evidence: your trade-off note, a summary of the conversation with the senior's challenges and your answers, and a link to the PR with the option you built.
Use this matrix with your team in Skiloope
Assign positions, track progress, attach evidence and validate skills in one shared workspace.
Get Started Free with SkiloopeFree up to 5 users · No credit card required