Customizing an image picker controller
At a glance
| Item |
Summary |
| Purpose |
Replaces the camera picker’s built-in controls with a custom overlay that supports immediate, delayed, and interval capture. |
| App architecture |
One feature controller owns the picker, overlay controls, capture timer, and captured-image session; UIKit and AVFoundation provide delegate and authorization boundaries. |
| Main patterns |
Framework-controller customization, delegate callbacks, overlay composition, permission gate, and timer-driven commands. |
| Project style |
Legacy storyboard sample with most feature logic intentionally colocated in one APLViewController file. |
Project structure
PhotoPicker/
├── AppDelegate.swift
├── ViewController.swift
├── Base.lproj/
│ ├── Main.storyboard
│ └── LaunchScreen.storyboard
├── Assets.xcassets/
└── Info.plist
Structure observations
ViewController.swift owns the entire camera/photo-picker feature; there is no separate camera service or session model.
- The storyboard supplies the main image view, camera overlay, and toolbar button outlets.
- AVFoundation is used only for camera authorization; capture itself goes through
UIImagePickerController.
Overall architecture
flowchart LR
Toolbar["Main toolbar action"] --> Controller["APLViewController"]
Controller -->|"authorization check"| CameraAuth["AVCaptureDevice"]
Controller --> Picker["UIImagePickerController"]
Overlay["Storyboard overlayView"] --> Picker
OverlayAction["Take / Delay / Interval / Done"] --> Controller
Controller --> Timer["Timer"]
Timer -->|"takePicture"| Picker
Picker -->|"delegate image / cancel"| Controller
Controller --> Captures["capturedImages"]
Captures --> Display["imageView / animationImages"]
Reference code
PhotoPicker/ViewController.swift:167 — source type selects presentation style; camera mode disables standard controls and installs the storyboard overlay before presentation.
imagePickerController.sourceType = sourceType
imagePickerController.modalPresentationStyle =
sourceType == .camera ? .fullScreen : .popover
let presentationController =
imagePickerController.popoverPresentationController
presentationController?.barButtonItem = button
if sourceType == .camera {
imagePickerController.showsCameraControls = false
overlayView?.frame =
(imagePickerController.cameraOverlayView?.frame)!
imagePickerController.cameraOverlayView = overlayView
}
present(imagePickerController, animated: true)
Interpretation
The app customizes a system-owned capture workflow rather than building an AVCaptureSession. APLViewController is both composition root and session coordinator: it chooses the picker mode, injects the overlay, translates overlay actions into picker commands, and consumes delegate results.
Ownership and state
classDiagram
APLViewController *-- UIImagePickerController : long-lived picker
UIImagePickerController --> APLViewController : delegate reference
APLViewController o-- UIView : overlay IBOutlet
UIImagePickerController o-- UIView : cameraOverlayView while presented
APLViewController *-- Timer : capture schedule
Timer --> APLViewController : block callback
APLViewController *-- UIImageArray : capturedImages
APLViewController o-- UIImageView : result outlet
Ownership evidence
PhotoPicker/ViewController.swift:13 — the controller stores all visual connections plus the picker, timer, and captured-image buffer.
@IBOutlet var imageView: UIImageView?
@IBOutlet var cameraButton: UIBarButtonItem?
@IBOutlet var overlayView: UIView?
@IBOutlet var takePictureButton: UIBarButtonItem?
@IBOutlet var startStopButton: UIBarButtonItem?
@IBOutlet var delayedPhotoButton: UIBarButtonItem?
@IBOutlet var doneButton: UIBarButtonItem?
var imagePickerController = UIImagePickerController()
var cameraTimer = Timer()
var capturedImages = [UIImage]()
| Owner |
Object or state |
Relationship |
Mutation authority |
APLViewController |
Image picker |
Creates and strongly retains one reusable picker |
Setup and source-selection methods |
APLViewController |
Overlay and toolbar outlets |
Strong storyboard connections |
Overlay action methods |
| Image picker |
Active camera overlay and delegate collaboration |
Retains overlay while configured; calls delegate |
Framework presentation lifecycle |
APLViewController |
cameraTimer |
Replaces/invalidates capture schedule |
Delay/start/stop/done actions |
APLViewController |
capturedImages |
Owns current capture batch |
Picker delegate and finishAndUpdate |
| Timer/RunLoop |
Scheduled callback |
Callback captures controller until completion/invalidation |
Run loop plus stop/done paths |
The timer and its closure make lifecycle explicit: repeating work must be invalidated before finishing the capture session.
Class and protocol design
| Type or protocol |
Responsibility |
Depends on or conforms to |
APLViewController |
Authorize, configure/present picker, handle overlay actions, collect/display results |
UIImagePickerControllerDelegate, UINavigationControllerDelegate |
UIImagePickerController |
Provide photo library or camera UI and still-image capture |
UIKit delegate contract |
AppDelegate |
Hold application window and startup lifecycle |
UIApplicationDelegate |
There is no app-defined protocol. The framework delegate is the inversion point: the picker owns the interaction flow, then calls the controller with an image or cancellation.
Access control
| Symbol |
Access |
Verified effect |
Why it fits this design |
APLViewController and feature state |
implicit internal |
Other app-module code could read/replace picker, timer, arrays, and outlets. |
Reflects an older teaching sample; production code could make most state private. |
| Overlay action and delegate methods |
implicit internal |
Reachable by storyboard/Objective-C action dispatch and UIKit callbacks. |
They form the framework/UI integration surface. |
| Info-key conversion helpers |
file-scope private |
Usable only inside ViewController.swift. |
Keeps compatibility conversion out of the app-module API. |
AppDelegate.window |
implicit internal |
Stores storyboard application window. |
Standard legacy app lifecycle surface. |
public / fileprivate / private(set) |
Not used |
No reusable public API or split read/write boundary. |
The sample emphasizes behavior rather than library encapsulation. |
Reference code
PhotoPicker/ViewController.swift:337 — the only explicit privacy boundary contains compatibility utilities used solely by picker-result handling.
private func convertFromUIImagePickerControllerInfoKeyDictionary(
_ input: [UIImagePickerController.InfoKey: Any]
) -> [String: Any] {
Dictionary(uniqueKeysWithValues:
input.map { key, value in (key.rawValue, value) })
}
private func convertFromUIImagePickerControllerInfoKey(
_ input: UIImagePickerController.InfoKey
) -> String {
input.rawValue
}
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Camera authorization and Settings alert |
APLViewController |
Requires presentation context and the initiating bar button. |
| Picker mode, popover/full-screen choice, overlay injection |
APLViewController |
It owns both picker and storyboard overlay. |
| Immediate/delayed/repeating capture commands |
APLViewController |
Toolbar actions map directly to takePicture and timer lifecycle. |
| Image acceptance/cancellation |
Picker delegate methods |
UIKit defines completion callbacks. |
| Batch display and cleanup |
finishAndUpdate |
One method closes the modal session and commits captured images to the main view. |
| Capture implementation |
UIImagePickerController |
System framework owns camera/media-library mechanics. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Framework-controller customization |
PhotoPicker/ViewController.swift:177 |
Reuses system capture while replacing its controls. |
| Overlay composition |
PhotoPicker/ViewController.swift:189 |
Injects a storyboard view into cameraOverlayView. |
| Permission gate |
PhotoPicker/ViewController.swift:90 |
Branches denied/not-determined/authorized before camera presentation. |
| Delegate callback |
PhotoPicker/ViewController.swift:298 |
Receives each captured image and decides when to finish. |
| Timer-driven command |
PhotoPicker/ViewController.swift:225 |
Adds delayed and repeating capture without owning camera hardware. |
| Session buffer |
PhotoPicker/ViewController.swift:28 |
Collects multiple delegate results before committing display. |
Naming conventions
APLViewController uses Apple’s older “APL” sample prefix rather than a feature-specific type name.
- Actions use explicit verbs:
showImagePickerForCamera, takePhoto, delayedTakePhoto, and stopTakingPicturesAtIntervals.
- State names reveal lifecycle:
cameraTimer and capturedImages.
finishAndUpdate combines two responsibilities in its name because it both dismisses and updates the result view.
Architecture takeaways
- Customize a system controller through its supported overlay and delegate seams before building a lower-level capture stack.
- Keep one clear owner for picker, timer, capture buffer, and presentation.
- Treat repeating timers as owned resources that must be invalidated on every finish path.
- Framework delegate methods are sufficient here; an app-defined abstraction would only be justified when capture logic must be reused or tested independently.
- The sample’s module-wide state is pedagogical, not a model of minimal access control.
Source map
| Source file |
Relevant symbols |
PhotoPicker/ViewController.swift |
Authorization, picker/overlay composition, timers, delegate results |
PhotoPicker/AppDelegate.swift |
Application/window lifecycle |
PhotoPicker/Base.lproj/Main.storyboard |
Main UI and camera overlay outlets/actions |
PhotoPicker/Info.plist |
Camera usage description |