The Ultimate Guide to Data Sharing Between ViewControllers in Swift 5 UIKit
The Ultimate Guide to Data Sharing Between ViewControllers in Swift 5 UIKit
Data sharing between ViewControllers is one of the most fundamental concepts in iOS development. Whether you're building a simple app or a complex enterprise solution, understanding how to effectively pass data between screens is crucial. This comprehensive guide covers every method, pattern, and technique for sharing data between ViewControllers in Swift 5 using UIKit.
Table of Contents
- Introduction to Data Flow
- Method 1: Direct Property Assignment
- Method 2: Segue-Based Data Passing
- Method 3: Delegation Pattern
- Method 4: Closures and Callbacks
- Method 5: NotificationCenter
- Method 6: Singleton Pattern
- Method 7: UserDefaults
- Method 8: Key-Value Observing (KVO)
- Method 9: Protocol-Oriented Programming
- Method 10: Dependency Injection
- Method 11: Coordinator Pattern
- Method 12: MVVM Architecture
- Method 13: Reactive Programming
- Advanced Patterns
- Best Practices
Introduction to Data Flow
Before diving into specific methods, it's essential to understand the different types of data flow in iOS applications.
Types of Data Flow
Forward Data Flow (Parent → Child)
Data flows from a source ViewController to a destination ViewController. This is the most common pattern when navigating forward in your app.
Use Cases:
- Passing user selection to detail screen
- Sending configuration data to new screen
- Providing context to presented controllers
Backward Data Flow (Child → Parent)
Data flows back from a presented/pushed ViewController to the presenting/pushing ViewController. This happens when user completes an action and needs to send results back.
Use Cases:
- Form submission results
- User selection from picker
- Filter/search criteria
- Photo selection
Sibling Data Flow
Data flows between ViewControllers at the same level in the hierarchy, typically managed through a common parent or shared data source.
Use Cases:
- Tab bar controllers sharing state
- Split view controllers
- Container view controllers
Navigation Patterns and Data Flow
Push Navigation (UINavigationController)
ViewController A → ViewController B → ViewController C
Data typically flows forward during push, and backward when popping.
Modal Presentation
ViewController A presents ViewController B
Can flow in both directions, but requires explicit handling.
Tab Bar Navigation
Tab 1 (VC A) ←→ Shared Data ←→ Tab 2 (VC B)
Requires shared data source or communication mechanism.
Container ViewControllers
Parent VC
├── Child VC 1
├── Child VC 2
└── Child VC 3
Parent manages data distribution to children.
Method 1: Direct Property Assignment
Direct property assignment is the simplest and most straightforward method for passing data forward from one ViewController to another. This method is ideal for one-way data flow when presenting or pushing a new ViewController.
How It Works
When you create or access a destination ViewController, you directly set its properties before presenting or pushing it. This method is type-safe, explicit, and easy to understand.
Advantages
- ✅ Simple and straightforward
- ✅ Type-safe
- ✅ Explicit and easy to understand
- ✅ No additional dependencies
- ✅ Compile-time checking
Disadvantages
- ❌ Only works for forward data flow
- ❌ Creates tight coupling between ViewControllers
- ❌ Doesn't work for backward data flow
- ❌ Can lead to massive ViewControllers
Implementation - Basic Example
import UIKit
// MARK: - First ViewController (Source)
class FirstViewController: UIViewController {
// UI Elements
private let nameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let ageTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your age"
textField.borderStyle = .roundedRect
textField.keyboardType = .numberPad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your email"
textField.borderStyle = .roundedRect
textField.keyboardType = .emailAddress
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let nextButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Next", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Enter Details"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(nameTextField)
view.addSubview(ageTextField)
view.addSubview(emailTextField)
view.addSubview(nextButton)
NSLayoutConstraint.activate([
nameTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
nameTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nameTextField.heightAnchor.constraint(equalToConstant: 44),
ageTextField.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 20),
ageTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
ageTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
ageTextField.heightAnchor.constraint(equalToConstant: 44),
emailTextField.topAnchor.constraint(equalTo: ageTextField.bottomAnchor, constant: 20),
emailTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
emailTextField.heightAnchor.constraint(equalToConstant: 44),
nextButton.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 40),
nextButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nextButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside)
}
@objc private func nextButtonTapped() {
guard let name = nameTextField.text, !name.isEmpty,
let ageText = ageTextField.text, let age = Int(ageText),
let email = emailTextField.text, !email.isEmpty else {
showAlert(message: "Please fill all fields correctly")
return
}
// Create the destination ViewController
let secondVC = SecondViewController()
// Direct property assignment - passing data forward
secondVC.userName = name
secondVC.userAge = age
secondVC.userEmail = email
// Navigate to second ViewController
navigationController?.pushViewController(secondVC, animated: true)
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
// MARK: - Second ViewController (Destination)
class SecondViewController: UIViewController {
// Properties to receive data
var userName: String?
var userAge: Int?
var userEmail: String?
// UI Elements
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let ageLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let emailLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 20
stack.alignment = .leading
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "User Details"
view.backgroundColor = .systemBackground
setupUI()
displayUserData()
}
private func setupUI() {
stackView.addArrangedSubview(nameLabel)
stackView.addArrangedSubview(ageLabel)
stackView.addArrangedSubview(emailLabel)
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
private func displayUserData() {
nameLabel.text = "Name: \(userName ?? "N/A")"
ageLabel.text = "Age: \(userAge != nil ? String(userAge!) : "N/A")"
emailLabel.text = "Email: \(userEmail ?? "N/A")"
}
}
Advanced Implementation with Custom Models
import UIKit
// MARK: - Data Models
struct User {
let id: UUID
let name: String
let age: Int
let email: String
let phoneNumber: String?
let address: Address?
let profileImage: UIImage?
init(id: UUID = UUID(), name: String, age: Int, email: String,
phoneNumber: String? = nil, address: Address? = nil, profileImage: UIImage? = nil) {
self.id = id
self.name = name
self.age = age
self.email = email
self.phoneNumber = phoneNumber
self.address = address
self.profileImage = profileImage
}
}
struct Address {
let street: String
let city: String
let state: String
let zipCode: String
let country: String
}
enum UserRole {
case admin
case moderator
case user
case guest
var displayName: String {
switch self {
case .admin: return "Administrator"
case .moderator: return "Moderator"
case .user: return "User"
case .guest: return "Guest"
}
}
}
// MARK: - User List ViewController
class UserListViewController: UIViewController {
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "UserCell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
private var users: [User] = []
override func viewDidLoad() {
super.viewDidLoad()
title = "Users"
view.backgroundColor = .systemBackground
setupUI()
loadUsers()
}
private func setupUI() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func loadUsers() {
// Sample data
users = [
User(name: "John Doe", age: 28, email: "john@example.com",
phoneNumber: "+1234567890",
address: Address(street: "123 Main St", city: "New York",
state: "NY", zipCode: "10001", country: "USA")),
User(name: "Jane Smith", age: 32, email: "jane@example.com",
phoneNumber: "+1987654321",
address: Address(street: "456 Oak Ave", city: "Los Angeles",
state: "CA", zipCode: "90001", country: "USA")),
User(name: "Bob Johnson", age: 45, email: "bob@example.com")
]
tableView.reloadData()
}
private func navigateToUserDetail(user: User) {
let detailVC = UserDetailViewController()
// Direct property assignment with complex model
detailVC.user = user
detailVC.userRole = .user
detailVC.isEditingEnabled = true
navigationController?.pushViewController(detailVC, animated: true)
}
}
extension UserListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath)
let user = users[indexPath.row]
cell.textLabel?.text = user.name
cell.accessoryType = .disclosureIndicator
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
navigateToUserDetail(user: users[indexPath.row])
}
}
// MARK: - User Detail ViewController
class UserDetailViewController: UIViewController {
// Properties to receive data
var user: User?
var userRole: UserRole = .user
var isEditingEnabled: Bool = false
// UI Elements
private let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
private let contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 50
imageView.backgroundColor = .systemGray5
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let roleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = .systemBlue
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let infoStackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 16
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
private let editButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Edit", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "User Details"
view.backgroundColor = .systemBackground
setupUI()
displayUserData()
}
private func setupUI() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(profileImageView)
contentView.addSubview(nameLabel)
contentView.addSubview(roleLabel)
contentView.addSubview(infoStackView)
if isEditingEnabled {
contentView.addSubview(editButton)
editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside)
}
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
profileImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
profileImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
profileImageView.widthAnchor.constraint(equalToConstant: 100),
profileImageView.heightAnchor.constraint(equalToConstant: 100),
nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 16),
nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
roleLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
roleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
roleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
infoStackView.topAnchor.constraint(equalTo: roleLabel.bottomAnchor, constant: 30),
infoStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
infoStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20)
])
if isEditingEnabled {
NSLayoutConstraint.activate([
editButton.topAnchor.constraint(equalTo: infoStackView.bottomAnchor, constant: 30),
editButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
editButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
editButton.heightAnchor.constraint(equalToConstant: 50),
editButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
])
} else {
infoStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20).isActive = true
}
}
private func displayUserData() {
guard let user = user else { return }
nameLabel.text = user.name
roleLabel.text = userRole.displayName
profileImageView.image = user.profileImage ?? UIImage(systemName: "person.circle.fill")
// Add user information
addInfoRow(title: "Email", value: user.email)
addInfoRow(title: "Age", value: "\(user.age) years old")
if let phone = user.phoneNumber {
addInfoRow(title: "Phone", value: phone)
}
if let address = user.address {
let fullAddress = "\(address.street)\n\(address.city), \(address.state) \(address.zipCode)\n\(address.country)"
addInfoRow(title: "Address", value: fullAddress)
}
}
private func addInfoRow(title: String, value: String) {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 14, weight: .semibold)
titleLabel.textColor = .secondaryLabel
titleLabel.translatesAutoresizingMaskIntoConstraints = false
let valueLabel = UILabel()
valueLabel.text = value
valueLabel.font = .systemFont(ofSize: 16)
valueLabel.numberOfLines = 0
valueLabel.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(titleLabel)
container.addSubview(valueLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: container.topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
valueLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
valueLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
valueLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
valueLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])
infoStackView.addArrangedSubview(container)
}
@objc private func editButtonTapped() {
guard let user = user else { return }
let editVC = UserEditViewController()
editVC.user = user
navigationController?.pushViewController(editVC, animated: true)
}
}
// MARK: - User Edit ViewController
class UserEditViewController: UIViewController {
var user: User?
// UI implementation similar to above
override func viewDidLoad() {
super.viewDidLoad()
title = "Edit User"
view.backgroundColor = .systemBackground
// Setup UI for editing
}
}
Modal Presentation with Direct Assignment
import UIKit
// MARK: - Presenting ViewController
class PresentingViewController: UIViewController {
private let presentButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Present Modal", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupUI()
}
private func setupUI() {
view.addSubview(presentButton)
presentButton.addTarget(self, action: #selector(presentButtonTapped), for: .touchUpInside)
NSLayoutConstraint.activate([
presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
presentButton.widthAnchor.constraint(equalToConstant: 200),
presentButton.heightAnchor.constraint(equalToConstant: 50)
])
}
@objc private func presentButtonTapped() {
let modalVC = ModalViewController()
// Direct property assignment
modalVC.titleText = "Welcome"
modalVC.messageText = "This is a modal presentation"
modalVC.buttonTitle = "Got it!"
modalVC.presentationStyle = .fullScreen
// Configure presentation style
if modalVC.presentationStyle == .fullScreen {
modalVC.modalPresentationStyle = .fullScreen
} else {
modalVC.modalPresentationStyle = .pageSheet
}
present(modalVC, animated: true)
}
}
// MARK: - Modal ViewController
class ModalViewController: UIViewController {
enum PresentationStyle {
case fullScreen
case pageSheet
}
// Properties to receive data
var titleText: String?
var messageText: String?
var buttonTitle: String?
var presentationStyle: PresentationStyle = .pageSheet
// UI Elements
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 28, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let messageLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18)
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let dismissButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupUI()
configureWithData()
}
private func setupUI() {
view.addSubview(titleLabel)
view.addSubview(messageLabel)
view.addSubview(dismissButton)
dismissButton.addTarget(self, action: #selector(dismissButtonTapped), for: .touchUpInside)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 60),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
messageLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
messageLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
dismissButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
dismissButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
dismissButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
dismissButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func configureWithData() {
titleLabel.text = titleText ?? "Title"
messageLabel.text = messageText ?? "Message"
dismissButton.setTitle(buttonTitle ?? "Dismiss", for: .normal)
}
@objc private func dismissButtonTapped() {
dismiss(animated: true)
}
}
Method 2: Segue-Based Data Passing
Segues are a storyboard-based way to define transitions between ViewControllers. The prepare(for:sender:)
method is called before a segue is performed, allowing you to configure the destination ViewController.
How It Works
When a segue is triggered (either programmatically or through storyboard connections), the prepare(for:sender:)
method is called. You can identify the segue by its identifier and cast the destination ViewController to set its
Advantages
- ✅ Swift-native and modern
- ✅ Less boilerplate than delegates
- ✅ Perfect for simple callbacks
- ✅ Type-safe with generics
- ✅ Can capture context easily
- ✅ Inline code execution
Disadvantages
- ❌ Can lead to retain cycles if not careful
- ❌ Less discoverable than protocols
- ❌ Can become complex with multiple callbacks
- ❌ Harder to test than delegates
Basic Closure Implementation
import UIKit
// MARK: - Child ViewController with Closure
class ImagePickerViewController: UIViewController {
// Define closure type alias for readability
typealias ImageSelectionHandler = (UIImage) -> Void
// Closure property
var onImageSelected: ImageSelectionHandler?
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .systemGray6
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let selectButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Select Image", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let chooseButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Choose from Library", for: .normal)
button.backgroundColor = .systemGreen
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Choose Image"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(imageView)
view.addSubview(selectButton)
view.addSubview(chooseButton)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
imageView.heightAnchor.constraint(equalToConstant: 300),
chooseButton.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 20),
chooseButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
chooseButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
chooseButton.heightAnchor.constraint(equalToConstant: 50),
selectButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
selectButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
selectButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
selectButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
selectButton.addTarget(self, action: #selector(selectButtonTapped), for: .touchUpInside)
chooseButton.addTarget(self, action: #selector(chooseButtonTapped), for: .touchUpInside)
}
@objc private func selectButtonTapped() {
guard let image = imageView.image else {
showAlert(message: "Please choose an image first")
return
}
// Execute the closure to pass data back
onImageSelected?(image)
navigationController?.popViewController(animated: true)
}
@objc private func chooseButtonTapped() {
let picker = UIImagePickerController()
picker.delegate = self
picker.sourceType = .photoLibrary
present(picker, animated: true)
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
extension ImagePickerViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let selectedImage = info[.originalImage] as? UIImage {
imageView.image = selectedImage
}
dismiss(animated: true)
}
}
// MARK: - Parent ViewController Using Closure
class MainImageViewController: UIViewController {
private let displayImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .systemGray6
imageView.layer.cornerRadius = 12
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let changeImageButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Change Image", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Image Display"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(displayImageView)
view.addSubview(changeImageButton)
NSLayoutConstraint.activate([
displayImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
displayImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
displayImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
displayImageView.heightAnchor.constraint(equalToConstant: 300),
changeImageButton.topAnchor.constraint(equalTo: displayImageView.bottomAnchor, constant: 40),
changeImageButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
changeImageButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
changeImageButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
changeImageButton.addTarget(self, action: #selector(changeImageButtonTapped), for: .touchUpInside)
}
@objc private func changeImageButtonTapped() {
let pickerVC = ImagePickerViewController()
// Assign closure to receive data - capturing self weakly to avoid retain cycle
pickerVC.onImageSelected = { [weak self] selectedImage in
self?.displayImageView.image = selectedImage
self?.saveImage(selectedImage)
self?.showSuccessAnimation()
}
navigationController?.pushViewController(pickerVC, animated: true)
}
private func saveImage(_ image: UIImage) {
// Save image logic
print("Image saved successfully")
}
private func showSuccessAnimation() {
UIView.animate(withDuration: 0.3) {
self.displayImageView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
} completion: { _ in
UIView.animate(withDuration: 0.3) {
self.displayImageView.transform = .identity
}
}
}
}
Multiple Closures for Different Events
import UIKit
// MARK: - Form ViewController with Multiple Closures
class FormViewController: UIViewController {
// Multiple closure properties for different events
var onSubmit: ((FormData) -> Void)?
var onCancel: (() -> Void)?
var onValidationError: ((String) -> Void)?
var onFieldChange: ((String, Any) -> Void)?
private var formData = FormData()
private let nameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Email"
textField.borderStyle = .roundedRect
textField.keyboardType = .emailAddress
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let ageTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Age"
textField.borderStyle = .roundedRect
textField.keyboardType = .numberPad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let submitButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Submit", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let cancelButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Cancel", for: .normal)
button.backgroundColor = .systemGray5
button.setTitleColor(.systemBlue, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "User Form"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(nameTextField)
view.addSubview(emailTextField)
view.addSubview(ageTextField)
view.addSubview(submitButton)
view.addSubview(cancelButton)
NSLayoutConstraint.activate([
nameTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
nameTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nameTextField.heightAnchor.constraint(equalToConstant: 44),
emailTextField.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 16),
emailTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
emailTextField.heightAnchor.constraint(equalToConstant: 44),
ageTextField.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 16),
ageTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
ageTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
ageTextField.heightAnchor.constraint(equalToConstant: 44),
submitButton.topAnchor.constraint(equalTo: ageTextField.bottomAnchor, constant: 40),
submitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
submitButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
submitButton.heightAnchor.constraint(equalToConstant: 50),
cancelButton.topAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: 12),
cancelButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
cancelButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
cancelButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
nameTextField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
emailTextField.addTarget(self, action: #selector(emailChanged), for: .editingChanged)
ageTextField.addTarget(self, action: #selector(ageChanged), for: .editingChanged)
submitButton.addTarget(self, action: #selector(submitButtonTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside)
}
@objc private func nameChanged() {
formData.name = nameTextField.text ?? ""
onFieldChange?("name", formData.name)
}
@objc private func emailChanged() {
formData.email = emailTextField.text ?? ""
onFieldChange?("email", formData.email)
}
@objc private func ageChanged() {
if let ageText = ageTextField.text, let age = Int(ageText) {
formData.age = age
onFieldChange?("age", age)
}
}
@objc private func submitButtonTapped() {
// Validate form
if !validateForm() {
return
}
// Execute submit closure
onSubmit?(formData)
navigationController?.popViewController(animated: true)
}
@objc private func cancelButtonTapped() {
// Execute cancel closure
onCancel?()
navigationController?.popViewController(animated: true)
}
private func validateForm() -> Bool {
if formData.name.isEmpty {
onValidationError?("Name is required")
return false
}
if formData.email.isEmpty {
onValidationError?("Email is required")
return false
}
if !isValidEmail(formData.email) {
onValidationError?("Invalid email format")
return false
}
if formData.age <= 0 {
onValidationError?("Please enter a valid age")
return false
}
return true
}
private func isValidEmail(_ email: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
}
// MARK: - Form Data Model
struct FormData {
var name: String = ""
var email: String = ""
var age: Int = 0
}
// MARK: - Parent ViewController Using Multiple Closures
class FormContainerViewController: UIViewController {
private let resultLabel: UILabel = {
let label = UILabel()
label.text = "No form submitted yet"
label.textAlignment = .center
label.numberOfLines = 0
label.font = .systemFont(ofSize: 16)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let showFormButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Show Form", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Form Manager"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(resultLabel)
view.addSubview(showFormButton)
NSLayoutConstraint.activate([
resultLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
resultLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
resultLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
showFormButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
showFormButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
showFormButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
showFormButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
showFormButton.addTarget(self, action: #selector(showFormButtonTapped), for: .touchUpInside)
}
@objc private func showFormButtonTapped() {
let formVC = FormViewController()
// Setup all closures
formVC.onSubmit = { [weak self] formData in
self?.handleFormSubmission(formData)
}
formVC.onCancel = { [weak self] in
self?.handleFormCancellation()
}
formVC.onValidationError = { [weak self] errorMessage in
self?.handleValidationError(errorMessage)
}
formVC.onFieldChange = { [weak self] fieldName, value in
self?.handleFieldChange(fieldName: fieldName, value: value)
}
navigationController?.pushViewController(formVC, animated: true)
}
private func handleFormSubmission(_ formData: FormData) {
resultLabel.text = """
Form Submitted!
Name: \(formData.name)
Email: \(formData.email)
Age: \(formData.age)
"""
// Save data to database
saveFormData(formData)
// Show success alert
showSuccessAlert()
}
private func handleFormCancellation() {
resultLabel.text = "Form submission cancelled"
print("User cancelled the form")
}
private func handleValidationError(_ errorMessage: String) {
// Show error alert
let alert = UIAlertController(title: "Validation Error", message: errorMessage, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func handleFieldChange(fieldName: String, value: Any) {
print("Field '\(fieldName)' changed to: \(value)")
// Could update UI or perform real-time validation
}
private func saveFormData(_ formData: FormData) {
// Save to UserDefaults, CoreData, or send to API
print("Saving form data: \(formData)")
}
private func showSuccessAlert() {
let alert = UIAlertController(title: "Success", message: "Form submitted successfully!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
Escaping and Non-Escaping Closures
import UIKit
// MARK: - Understanding Escaping Closures
class DataFetchViewController: UIViewController {
// Escaping closure - executes after function returns
var onDataFetched: ((Result<[String], Error>) -> Void)?
private let activityIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(style: .large)
indicator.translatesAutoresizingMaskIntoConstraints = false
return indicator
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Fetch Data"
view.backgroundColor = .systemBackground
setupUI()
fetchData()
}
private func setupUI() {
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
activityIndicator.startAnimating()
}
private func fetchData() {
// Simulate API call with escaping closure
performAsyncOperation { [weak self] result in
DispatchQueue.main.async {
self?.activityIndicator.stopAnimating()
switch result {
case .success(let data):
self?.onDataFetched?(.success(data))
case .failure(let error):
self?.onDataFetched?(.failure(error))
}
self?.navigationController?.popViewController(animated: true)
}
}
}
// Escaping closure marked with @escaping
private func performAsyncOperation(completion: @escaping (Result<[String], Error>) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
let mockData = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]
completion(.success(mockData))
}
}
}
// MARK: - Parent Using Escaping Closure
class DataListViewController: UIViewController {
private var dataItems: [String] = []
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
private let fetchButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Fetch Data", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Data List"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(tableView)
view.addSubview(fetchButton)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: fetchButton.topAnchor, constant: -16),
fetchButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
fetchButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
fetchButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
fetchButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
fetchButton.addTarget(self, action: #selector(fetchButtonTapped), for: .touchUpInside)
}
@objc private func fetchButtonTapped() {
let fetchVC = DataFetchViewController()
// Assign escaping closure
fetchVC.onDataFetched = { [weak self] result in
switch result {
case .success(let data):
self?.dataItems = data
self?.tableView.reloadData()
self?.showSuccessMessage()
case .failure(let error):
self?.showErrorAlert(error: error)
}
}
navigationController?.pushViewController(fetchVC, animated: true)
}
private func showSuccessMessage() {
let alert = UIAlertController(title: "Success", message: "Data fetched successfully!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func showErrorAlert(error: Error) {
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
extension DataListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = dataItems[indexPath.row]
return cell
}
}
Generic Closures for Reusability
import UIKit
// MARK: - Generic Picker ViewController
class GenericPickerViewController<T>: UIViewController where T: CustomStringConvertible {
// Generic closure
var onItemSelected: ((T) -> Void)?
var items: [T] = []
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Select Item"
view.backgroundColor = .systemBackground
setupUI()
}
private func setupUI() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
extension GenericPickerViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].description
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedItem = items[indexPath.row]
onItemSelected?(selectedItem)
navigationController?.popViewController(animated: true)
}
}
// MARK: - Using Generic Picker
enum Priority: String, CustomStringConvertible {
case low = "Low"
case medium = "Medium"
case high = "High"
case critical = "Critical"
var description: String {
return self.rawValue
}
}
struct Category: CustomStringConvertible {
let id: Int
let name: String
let icon: String
var description: String {
return "\(icon) \(name)"
}
}
class TaskCreatorViewController: UIViewController {
private var selectedPriority: Priority = .medium
private var selectedCategory: Category?
private let priorityButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Priority: Medium", for: .normal)
button.backgroundColor = .systemGray6
button.setTitleColor(.label, for: .normal)
button.layer.cornerRadius = 8
button.contentHorizontalAlignment = .left
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let categoryButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Category: None", for: .normal)
button.backgroundColor = .systemGray6
button.setTitleColor(.label, for: .normal)
button.layer.cornerRadius = 8
button.contentHorizontalAlignment = .left
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Create Task"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(priorityButton)
view.addSubview(categoryButton)
NSLayoutConstraint.activate([
priorityButton.topAnchor# The Ultimate Guide to Data Sharing Between ViewControllers in Swift 5 UIKit
Data sharing between ViewControllers is one of the most fundamental concepts in iOS development. Whether you're building a simple app or a complex enterprise solution, understanding how to effectively pass data between screens is crucial. This comprehensive guide covers every method, pattern, and technique for sharing data between ViewControllers in Swift 5 using UIKit.
## Table of Contents
1. [Introduction to Data Flow](#introduction-to-data-flow)
2. [Method 1: Direct Property Assignment](#method-1-direct-property-assignment)
3. [Method 2: Segue-Based Data Passing](#method-2-segue-based-data-passing)
4. [Method 3: Delegation Pattern](#method-3-delegation-pattern)
5. [Method 4: Closures and Callbacks](#method-4-closures-and-callbacks)
6. [Method 5: NotificationCenter](#method-5-notificationcenter)
7. [Method 6: Singleton Pattern](#method-6-singleton-pattern)
8. [Method 7: UserDefaults](#method-7-userdefaults)
9. [Method 8: Key-Value Observing (KVO)](#method-8-key-value-observing-kvo)
10. [Method 9: Protocol-Oriented Programming](#method-9-protocol-oriented-programming)
11. [Method 10: Dependency Injection](#method-10-dependency-injection)
12. [Method 11: Coordinator Pattern](#method-11-coordinator-pattern)
13. [Method 12: MVVM Architecture](#method-12-mvvm-architecture)
14. [Method 13: Reactive Programming](#method-13-reactive-programming)
15. [Advanced Patterns](#advanced-patterns)
16. [Best Practices](#best-practices)
---
## Introduction to Data Flow
Before diving into specific methods, it's essential to understand the different types of data flow in iOS applications.
### Types of Data Flow
#### Forward Data Flow (Parent → Child)
Data flows from a source ViewController to a destination ViewController. This is the most common pattern when navigating forward in your app.
**Use Cases:**
- Passing user selection to detail screen
- Sending configuration data to new screen
- Providing context to presented controllers
#### Backward Data Flow (Child → Parent)
Data flows back from a presented/pushed ViewController to the presenting/pushing ViewController. This happens when user completes an action and needs to send results back.
**Use Cases:**
- Form submission results
- User selection from picker
- Filter/search criteria
- Photo selection
#### Sibling Data Flow
Data flows between ViewControllers at the same level in the hierarchy, typically managed through a common parent or shared data source.
**Use Cases:**
- Tab bar controllers sharing state
- Split view controllers
- Container view controllers
### Navigation Patterns and Data Flow
#### Push Navigation (UINavigationController)
ViewController A → ViewController B → ViewController C
Data typically flows forward during push, and backward when popping.
#### Modal Presentation
ViewController A presents ViewController B
Can flow in both directions, but requires explicit handling.
#### Tab Bar Navigation
Tab 1 (VC A) ←→ Shared Data ←→ Tab 2 (VC B)
Requires shared data source or communication mechanism.
#### Container ViewControllers
Parent VC ├── Child VC 1 ├── Child VC 2 └── Child VC 3
Parent manages data distribution to children.
---
## Method 1: Direct Property Assignment
Direct property assignment is the simplest and most straightforward method for passing data forward from one ViewController to another. This method is ideal for one-way data flow when presenting or pushing a new ViewController.
### How It Works
When you create or access a destination ViewController, you directly set its properties before presenting or pushing it. This method is type-safe, explicit, and easy to understand.
### Advantages
- ✅ Simple and straightforward
- ✅ Type-safe
- ✅ Explicit and easy to understand
- ✅ No additional dependencies
- ✅ Compile-time checking
### Disadvantages
- ❌ Only works for forward data flow
- ❌ Creates tight coupling between ViewControllers
- ❌ Doesn't work for backward data flow
- ❌ Can lead to massive ViewControllers
### Implementation - Basic Example
```swift
import UIKit
// MARK: - First ViewController (Source)
class FirstViewController: UIViewController {
// UI Elements
private let nameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let ageTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your age"
textField.borderStyle = .roundedRect
textField.keyboardType = .numberPad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your email"
textField.borderStyle = .roundedRect
textField.keyboardType = .emailAddress
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let nextButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Next", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Enter Details"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(nameTextField)
view.addSubview(ageTextField)
view.addSubview(emailTextField)
view.addSubview(nextButton)
NSLayoutConstraint.activate([
nameTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
nameTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nameTextField.heightAnchor.constraint(equalToConstant: 44),
ageTextField.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 20),
ageTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
ageTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
ageTextField.heightAnchor.constraint(equalToConstant: 44),
emailTextField.topAnchor.constraint(equalTo: ageTextField.bottomAnchor, constant: 20),
emailTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
emailTextField.heightAnchor.constraint(equalToConstant: 44),
nextButton.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 40),
nextButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nextButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside)
}
@objc private func nextButtonTapped() {
guard let name = nameTextField.text, !name.isEmpty,
let ageText = ageTextField.text, let age = Int(ageText),
let email = emailTextField.text, !email.isEmpty else {
showAlert(message: "Please fill all fields correctly")
return
}
// Create the destination ViewController
let secondVC = SecondViewController()
// Direct property assignment - passing data forward
secondVC.userName = name
secondVC.userAge = age
secondVC.userEmail = email
// Navigate to second ViewController
navigationController?.pushViewController(secondVC, animated: true)
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
// MARK: - Second ViewController (Destination)
class SecondViewController: UIViewController {
// Properties to receive data
var userName: String?
var userAge: Int?
var userEmail: String?
// UI Elements
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let ageLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let emailLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 20
stack.alignment = .leading
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "User Details"
view.backgroundColor = .systemBackground
setupUI()
displayUserData()
}
private func setupUI() {
stackView.addArrangedSubview(nameLabel)
stackView.addArrangedSubview(ageLabel)
stackView.addArrangedSubview(emailLabel)
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
private func displayUserData() {
nameLabel.text = "Name: \(userName ?? "N/A")"
ageLabel.text = "Age: \(userAge != nil ? String(userAge!) : "N/A")"
emailLabel.text = "Email: \(userEmail ?? "N/A")"
}
}
Advanced Implementation with Custom Models
import UIKit
// MARK: - Data Models
struct User {
let id: UUID
let name: String
let age: Int
let email: String
let phoneNumber: String?
let address: Address?
let profileImage: UIImage?
init(id: UUID = UUID(), name: String, age: Int, email: String,
phoneNumber: String? = nil, address: Address? = nil, profileImage: UIImage? = nil) {
self.id = id
self.name = name
self.age = age
self.email = email
self.phoneNumber = phoneNumber
self.address = address
self.profileImage = profileImage
}
}
struct Address {
let street: String
let city: String
let state: String
let zipCode: String
let country: String
}
enum UserRole {
case admin
case moderator
case user
case guest
var displayName: String {
switch self {
case .admin: return "Administrator"
case .moderator: return "Moderator"
case .user: return "User"
case .guest: return "Guest"
}
}
}
// MARK: - User List ViewController
class UserListViewController: UIViewController {
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "UserCell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
private var users: [User] = []
override func viewDidLoad() {
super.viewDidLoad()
title = "Users"
view.backgroundColor = .systemBackground
setupUI()
loadUsers()
}
private func setupUI() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func loadUsers() {
// Sample data
users = [
User(name: "John Doe", age: 28, email: "john@example.com",
phoneNumber: "+1234567890",
address: Address(street: "123 Main St", city: "New York",
state: "NY", zipCode: "10001", country: "USA")),
User(name: "Jane Smith", age: 32, email: "jane@example.com",
phoneNumber: "+1987654321",
address: Address(street: "456 Oak Ave", city: "Los Angeles",
state: "CA", zipCode: "90001", country: "USA")),
User(name: "Bob Johnson", age: 45, email: "bob@example.com")
]
tableView.reloadData()
}
private func navigateToUserDetail(user: User) {
let detailVC = UserDetailViewController()
// Direct property assignment with complex model
detailVC.user = user
detailVC.userRole = .user
detailVC.isEditingEnabled = true
navigationController?.pushViewController(detailVC, animated: true)
}
}
extension UserListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath)
let user = users[indexPath.row]
cell.textLabel?.text = user.name
cell.accessoryType = .disclosureIndicator
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
navigateToUserDetail(user: users[indexPath.row])
}
}
// MARK: - User Detail ViewController
class UserDetailViewController: UIViewController {
// Properties to receive data
var user: User?
var userRole: UserRole = .user
var isEditingEnabled: Bool = false
// UI Elements
private let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
private let contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 50
imageView.backgroundColor = .systemGray5
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let roleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = .systemBlue
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let infoStackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 16
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
private let editButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Edit", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "User Details"
view.backgroundColor = .systemBackground
setupUI()
displayUserData()
}
private func setupUI() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(profileImageView)
contentView.addSubview(nameLabel)
contentView.addSubview(roleLabel)
contentView.addSubview(infoStackView)
if isEditingEnabled {
contentView.addSubview(editButton)
editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside)
}
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
profileImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
profileImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
profileImageView.widthAnchor.constraint(equalToConstant: 100),
profileImageView.heightAnchor.constraint(equalToConstant: 100),
nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 16),
nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
roleLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
roleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
roleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
infoStackView.topAnchor.constraint(equalTo: roleLabel.bottomAnchor, constant: 30),
infoStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
infoStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20)
])
if isEditingEnabled {
NSLayoutConstraint.activate([
editButton.topAnchor.constraint(equalTo: infoStackView.bottomAnchor, constant: 30),
editButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
editButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
editButton.heightAnchor.constraint(equalToConstant: 50),
editButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
])
} else {
infoStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20).isActive = true
}
}
private func displayUserData() {
guard let user = user else { return }
nameLabel.text = user.name
roleLabel.text = userRole.displayName
profileImageView.image = user.profileImage ?? UIImage(systemName: "person.circle.fill")
// Add user information
addInfoRow(title: "Email", value: user.email)
addInfoRow(title: "Age", value: "\(user.age) years old")
if let phone = user.phoneNumber {
addInfoRow(title: "Phone", value: phone)
}
if let address = user.address {
let fullAddress = "\(address.street)\n\(address.city), \(address.state) \(address.zipCode)\n\(address.country)"
addInfoRow(title: "Address", value: fullAddress)
}
}
private func addInfoRow(title: String, value: String) {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 14, weight: .semibold)
titleLabel.textColor = .secondaryLabel
titleLabel.translatesAutoresizingMaskIntoConstraints = false
let valueLabel = UILabel()
valueLabel.text = value
valueLabel.font = .systemFont(ofSize: 16)
valueLabel.numberOfLines = 0
valueLabel.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(titleLabel)
container.addSubview(valueLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: container.topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
valueLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
valueLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
valueLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
valueLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])
infoStackView.addArrangedSubview(container)
}
@objc private func editButtonTapped() {
guard let user = user else { return }
let editVC = UserEditViewController()
editVC.user = user
navigationController?.pushViewController(editVC, animated: true)
}
}
// MARK: - User Edit ViewController
class UserEditViewController: UIViewController {
var user: User?
// UI implementation similar to above
override func viewDidLoad() {
super.viewDidLoad()
title = "Edit User"
view.backgroundColor = .systemBackground
// Setup UI for editing
}
}
Modal Presentation with Direct Assignment
import UIKit
// MARK: - Presenting ViewController
class PresentingViewController: UIViewController {
private let presentButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Present Modal", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupUI()
}
private func setupUI() {
view.addSubview(presentButton)
presentButton.addTarget(self, action: #selector(presentButtonTapped), for: .touchUpInside)
NSLayoutConstraint.activate([
presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
presentButton.widthAnchor.constraint(equalToConstant: 200),
presentButton.heightAnchor.constraint(equalToConstant: 50)
])
}
@objc private func presentButtonTapped() {
let modalVC = ModalViewController()
// Direct property assignment
modalVC.titleText = "Welcome"
modalVC.messageText = "This is a modal presentation"
modalVC.buttonTitle = "Got it!"
modalVC.presentationStyle = .fullScreen
// Configure presentation style
if modalVC.presentationStyle == .fullScreen {
modalVC.modalPresentationStyle = .fullScreen
} else {
modalVC.modalPresentationStyle = .pageSheet
}
present(modalVC, animated: true)
}
}
// MARK: - Modal ViewController
class ModalViewController: UIViewController {
enum PresentationStyle {
case fullScreen
case pageSheet
}
// Properties to receive data
var titleText: String?
var messageText: String?
var buttonTitle: String?
var presentationStyle: PresentationStyle = .pageSheet
// UI Elements
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 28, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let messageLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18)
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let dismissButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupUI()
configureWithData()
}
private func setupUI() {
view.addSubview(titleLabel)
view.addSubview(messageLabel)
view.addSubview(dismissButton)
dismissButton.addTarget(self, action: #selector(dismissButtonTapped), for: .touchUpInside)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 60),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
messageLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
messageLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
dismissButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
dismissButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
dismissButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
dismissButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func configureWithData() {
titleLabel.text = titleText ?? "Title"
messageLabel.text = messageText ?? "Message"
dismissButton.setTitle(buttonTitle ?? "Dismiss", for: .normal)
}
@objc private func dismissButtonTapped() {
dismiss(animated: true)
}
}
Method 2: Segue-Based Data Passing
Segues are a storyboard-based way to define transitions between ViewControllers. The prepare(for:sender:)
method is called before a segue is performed, allowing you to configure the destination ViewController.
How It Works
When a segue is triggered (either programmatically or through storyboard connections), the prepare(for:sender:)
method is called. You can identify the segue by its identifier and cast the destination ViewController to set its properties.
Advantages
- ✅ Visual representation in storyboard
- ✅ Easy to set up for simple scenarios
- ✅ Centralized data passing logic
- ✅ Works well with Interface Builder
Disadvantages
- ❌ Requires storyboards (not programmatic)
- ❌ String-based identifiers (not type-safe)
- ❌ Can become messy with many segues
- ❌ Harder to test
- ❌ Only for forward data flow
Implementation - Basic Segue
import UIKit
// MARK: - Source ViewController with Segue
class SegueSourceViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var phoneTextField: UITextField!
@IBOutlet weak var nextButton: UIButton!
// Segue identifiers
private enum SegueIdentifier {
static let showDetail = "ShowDetailSegue"
static let showSettings = "ShowSettingsSegue"
}
override func viewDidLoad() {
super.viewDidLoad()
title = "User Input"
}
// This method is called before any segue is performed
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Identify which segue is being performed
if segue.identifier == SegueIdentifier.showDetail {
// Cast destination to the correct type
if let destinationVC = segue.destination as? SegueDestinationViewController {
// Pass data through properties
destinationVC.userName = nameTextField.text
destinationVC.userEmail = emailTextField.text
destinationVC.userPhone = phoneTextField.text
destinationVC.timestamp = Date()
}
} else if segue.identifier == SegueIdentifier.showSettings {
if let settingsVC = segue.destination as? SettingsViewController {
settingsVC.currentUser = createUserObject()
}
}
}
// Validate and trigger segue programmatically
@IBAction func nextButtonTapped(_ sender: UIButton) {
guard validateInputs() else {
showValidationError()
return
}
// Perform segue with identifier
performSegue(withIdentifier: SegueIdentifier.showDetail, sender: self)
}
private func validateInputs() -> Bool {
guard let name = nameTextField.text, !name.isEmpty,
let email = emailTextField.text, !email.isEmpty else {
return false
}
return true
}
private func showValidationError() {
let alert = UIAlertController(
title: "Error",
message: "Please fill in all required fields",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func createUserObject() -> User {
return User(
name: nameTextField.text ?? "",
age: 0,
email: emailTextField.text ?? ""
)
}
}
// MARK: - Destination ViewController
class SegueDestinationViewController: UIViewController {
// Properties to receive data from segue
var userName: String?
var userEmail: String?
var userPhone: String?
var timestamp: Date?
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
@IBOutlet weak var phoneLabel: UILabel!
@IBOutlet weak var timestampLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
title = "User Details"
displayUserData()
}
private func displayUserData() {
nameLabel.text = "Name: \(userName ?? "N/A")"
emailLabel.text = "Email: \(userEmail ?? "N/A")"
phoneLabel.text = "Phone: \(userPhone ?? "N/A")"
if let timestamp = timestamp {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
timestampLabel.text = "Created: \(formatter.string(from: timestamp))"
}
}
}
Advanced Segue Implementation with Multiple Destinations
import UIKit
// MARK: - Master View Controller with Multiple Segues
class MasterViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private var products: [Product] = []
private var selectedProduct: Product?
// Segue identifiers as enum for type safety
private enum SegueID: String {
case showProductDetail = "ShowProductDetailSegue"
case showProductEdit = "ShowProductEditSegue"
case showProductReviews = "ShowProductReviewsSegue"
case showCart = "ShowCartSegue"
case showCheckout = "ShowCheckoutSegue"
}
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
loadProducts()
}
private func setupTableView() {
tableView.delegate = self
tableView.dataSource = self
tableView.register(ProductTableViewCell.self, forCellReuseIdentifier: "ProductCell")
}
private func loadProducts() {
products = [
Product(id: "1", name: "iPhone 14", price: 999.99, category: .electronics),
Product(id: "2", name: "MacBook Pro", price: 2499.99, category: .electronics),
Product(id: "3", name: "AirPods Pro", price: 249.99, category: .electronics)
]
tableView.reloadData()
}
// Centralized segue preparation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier,
let segueID = SegueID(rawValue: identifier) else {
return
}
switch segueID {
case .showProductDetail:
prepareProductDetail(segue: segue, sender: sender)
case .showProductEdit:
prepareProductEdit(segue: segue, sender: sender)
case .showProductReviews:
prepareProductReviews(segue: segue, sender: sender)
case .showCart:
prepareCart(segue: segue, sender: sender)
case .showCheckout:
prepareCheckout(segue: segue, sender: sender)
}
}
private func prepareProductDetail(segue: UIStoryboardSegue, sender: Any?) {
if let detailVC = segue.destination as? ProductDetailViewController {
detailVC.product = selectedProduct
detailVC.viewMode = .readOnly
detailVC.showReviewsButton = true
detailVC.allowPurchase = true
}
}
private func prepareProductEdit(segue: UIStoryboardSegue, sender: Any?) {
if let editVC = segue.destination as? ProductEditViewController {
editVC.product = selectedProduct
editVC.editMode = .update
}
}
private func prepareProductReviews(segue: UIStoryboardSegue, sender: Any?) {
if let reviewsVC = segue.destination as? ProductReviewsViewController {
reviewsVC.productID = selectedProduct?.id
reviewsVC.productName = selectedProduct?.name
}
}
private func prepareCart(segue: UIStoryboardSegue, sender: Any?) {
if let cartVC = segue.destination as? CartViewController {
cartVC.cartItems = CartManager.shared.items
cartVC.totalAmount = CartManager.shared.totalAmount
}
}
private func prepareCheckout(segue: UIStoryboardSegue, sender: Any?) {
if let checkoutVC = segue.destination as? CheckoutViewController {
checkoutVC.cartItems = CartManager.shared.items
checkoutVC.subtotal = CartManager.shared.subtotal
checkoutVC.tax = CartManager.shared.tax
checkoutVC.shippingCost = CartManager.shared.shippingCost
checkoutVC.total = CartManager.shared.totalAmount
}
}
@IBAction func cartButtonTapped(_ sender: UIBarButtonItem) {
performSegue(withIdentifier: SegueID.showCart.rawValue, sender: self)
}
}
extension MasterViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return products.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath)
let product = products[indexPath.row]
cell.textLabel?.text = product.name
cell.detailTextLabel?.text = "$\(product.price)"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedProduct = products[indexPath.row]
performSegue(withIdentifier: SegueID.showProductDetail.rawValue, sender: self)
}
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
selectedProduct = products[indexPath.row]
performSegue(withIdentifier: SegueID.showProductEdit.rawValue, sender: self)
}
}
// MARK: - Supporting Models
struct Product {
let id: String
let name: String
let price: Double
let category: ProductCategory
var description: String?
var imageURL: URL?
var rating: Double?
var reviewCount: Int?
}
enum ProductCategory {
case electronics
case clothing
case books
case home
}
class CartManager {
static let shared = CartManager()
var items: [CartItem] = []
var subtotal: Double {
return items.reduce(0) { $0 + ($1.product.price * Double($1.quantity)) }
}
var tax: Double {
return subtotal * 0.08
}
var shippingCost: Double {
return subtotal > 50 ? 0 : 5.99
}
var totalAmount: Double {
return subtotal + tax + shippingCost
}
}
struct CartItem {
let product: Product
var quantity: Int
}
Unwind Segues for Backward Data Flow
import UIKit
// MARK: - Detail ViewController
class DetailViewController: UIViewController {
var selectedColor: UIColor?
var selectedSize: String?
var quantity: Int = 1
@IBOutlet weak var colorPicker: UISegmentedControl!
@IBOutlet weak var sizePicker: UISegmentedControl!
@IBOutlet weak var quantityStepper: UIStepper!
@IBOutlet weak var quantityLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func quantityChanged(_ sender: UIStepper) {
quantity = Int(sender.value)
quantityLabel.text = "\(quantity)"
}
@IBAction func colorChanged(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0: selectedColor = .red
case 1: selectedColor = .blue
case 2: selectedColor = .green
default: selectedColor = nil
}
}
@IBAction func sizeChanged(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0: selectedSize = "S"
case 1: selectedSize = "M"
case 2: selectedSize = "L"
case 3: selectedSize = "XL"
default: selectedSize = nil
}
}
// This prepares data for the unwind segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "UnwindToMain" {
// Data is already in properties, will be accessed by destination
}
}
}
// MARK: - Main ViewController (Destination of Unwind)
class MainViewController: UIViewController {
@IBOutlet weak var selectedColorView: UIView!
@IBOutlet weak var selectedSizeLabel: UILabel!
@IBOutlet weak var selectedQuantityLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
// Unwind segue action - receives data from source
@IBAction func unwindToMain(_ segue: UIStoryboardSegue) {
if let sourceVC = segue.source as? DetailViewController {
// Receive data from the unwinding ViewController
if let color = sourceVC.selectedColor {
selectedColorView.backgroundColor = color
}
if let size = sourceVC.selectedSize {
selectedSizeLabel.text = "Size: \(size)"
}
selectedQuantityLabel.text = "Quantity: \(sourceVC.quantity)"
// Perform additional actions with received data
saveSelection(color: sourceVC.selectedColor,
size: sourceVC.selectedSize,
quantity: sourceVC.quantity)
}
}
private func saveSelection(color: UIColor?, size: String?, quantity: Int) {
// Save or process the received data
print("Received selection: Color=\(String(describing: color)), Size=\(size ?? "N/A"), Quantity=\(quantity)")
}
// Forward segue to detail
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowDetail" {
if let detailVC = segue.destination as? DetailViewController {
// Can pre-populate with existing data
detailVC.selectedColor = selectedColorView.backgroundColor
detailVC.selectedSize = extractSize(from: selectedSizeLabel.text)
detailVC.quantity = extractQuantity(from: selectedQuantityLabel.text)
}
}
}
private func extractSize(from text: String?) -> String? {
return text?.replacingOccurrences(of: "Size: ", with: "")
}
private func extractQuantity(from text: String?) -> Int {
guard let text = text else { return 1 }
let quantity = text.replacingOccurrences(of: "Quantity: ", with: "")
return Int(quantity) ?? 1
}
}
Conditional Segues with Validation
import UIKit
// MARK: - Form ViewController with Validation
class FormViewController: UIViewController {
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var confirmPasswordTextField: UITextField!
@IBOutlet weak var agreeSwitch: UISwitch!
@IBOutlet weak var submitButton: UIButton!
private var validationErrors: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
setupValidation()
}
private func setupValidation() {
emailTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
passwordTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
confirmPasswordTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
@objc private func textFieldDidChange() {
validateForm()
}
private func validateForm() {
validationErrors.removeAll()
// Email validation
if let email = emailTextField.text, !email.isEmpty {
if !isValidEmail(email) {
validationErrors.append("Invalid email format")
}
} else {
validationErrors.append("Email is required")
}
// Password validation
if let password = passwordTextField.text, !password.isEmpty {
if password.count < 8 {
validationErrors.append("Password must be at least 8 characters")
}
} else {
validationErrors.append("Password is required")
}
// Confirm password validation
if passwordTextField.text != confirmPasswordTextField.text {
validationErrors.append("Passwords do not match")
}
// Terms agreement
if !agreeSwitch.isOn {
validationErrors.append("You must agree to terms and conditions")
}
submitButton.isEnabled = validationErrors.isEmpty
}
private func isValidEmail(_ email: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
// Override shouldPerformSegue to add validation
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if identifier == "ShowSuccessSegue" {
validateForm()
if !validationErrors.isEmpty {
showValidationErrors()
return false
}
return true
}
return true
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowSuccessSegue" {
if let successVC = segue.destination as? SuccessViewController {
successVC.userEmail = emailTextField.text
successVC.registrationDate = Date()
}
}
}
private func showValidationErrors() {
let errorMessage = validationErrors.joined(separator: "\n")
let alert = UIAlertController(
title: "Validation Error",
message: errorMessage,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
@IBAction func submitButtonTapped(_ sender: UIButton) {
performSegue(withIdentifier: "ShowSuccessSegue", sender: self)
}
}
// MARK: - Success ViewController
class SuccessViewController: UIViewController {
var userEmail: String?
var registrationDate: Date?
@IBOutlet weak var messageLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
displaySuccess()
}
private func displaySuccess() {
messageLabel.text = "Registration Successful!"
emailLabel.text = "Email: \(userEmail ?? "N/A")"
if let date = registrationDate {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .medium
dateLabel.text = "Date: \(formatter.string(from: date))"
}
}
}
Method 3: Delegation Pattern
The Delegation pattern is one of the most important design patterns in iOS development. It allows one object to send messages to another object when specific events occur. This is the primary method for backward data flow.
How It Works
- Define a protocol that declares methods for communication
- The delegating class has a weak delegate property
- The delegate class conforms to the protocol
- The delegating class calls delegate methods when needed
- The delegate implements the methods to handle events
Advantages
- ✅ Perfect for backward data flow
- ✅ Loose coupling through protocols
- ✅ Type-safe communication
- ✅ Clear contract between objects
- ✅ Apple's preferred pattern
Disadvantages
- ❌ Requires protocol definition
- ❌ More boilerplate code
- ❌ One-to-one relationship only
- ❌ Must manage weak references
Basic Delegation Implementation
import UIKit
// MARK: - Step 1: Define the Protocol
protocol ColorPickerDelegate: AnyObject {
func colorPicker(_ picker: ColorPickerViewController, didSelectColor color: UIColor)
func colorPickerDidCancel(_ picker: ColorPickerViewController)
}
// MARK: - Step 2: Delegating ViewController (Child)
class ColorPickerViewController: UIViewController {
// Weak delegate property to avoid retain cycles
weak var delegate: ColorPickerDelegate?
private var selectedColor: UIColor = .white
// UI Elements
private let colorPickerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let redSlider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 1
slider.translatesAutoresizingMaskIntoConstraints = false
return slider
}()
private let greenSlider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 1
slider.translatesAutoresizingMaskIntoConstraints = false
return slider
}()
private let blueSlider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 1
slider.translatesAutoresizingMaskIntoConstraints = false
return slider
}()
private let previewView: UIView = {
let view = UIView()
view.layer.cornerRadius = 50
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.systemGray.cgColor
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let selectButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Select Color", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let cancelButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Cancel", for: .normal)
button.backgroundColor = .systemGray5
button.setTitleColor(.systemBlue, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Choose Color"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
updatePreview()
}
private func setupUI() {
view.addSubview(previewView)
view.addSubview(redSlider)
view.addSubview(greenSlider)
view.addSubview(blueSlider)
view.addSubview(selectButton)
view.addSubview(cancelButton)
NSLayoutConstraint.activate([
previewView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
previewView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
previewView.widthAnchor.constraint(equalToConstant: 100),
previewView.heightAnchor.constraint(equalToConstant: 100),
redSlider.topAnchor.constraint(equalTo: previewView.bottomAnchor, constant: 40),
redSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
redSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
greenSlider.topAnchor.constraint(equalTo: redSlider.bottomAnchor, constant: 20),
greenSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
greenSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
blueSlider.topAnchor.constraint(equalTo: greenSlider.bottomAnchor, constant: 20),
blueSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
blueSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
selectButton.bottomAnchor.constraint(equalTo: cancelButton.topAnchor, constant: -16),
selectButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
selectButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
selectButton.heightAnchor.constraint(equalToConstant: 50),
cancelButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
cancelButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
cancelButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
cancelButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
redSlider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
greenSlider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
blueSlider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
selectButton.addTarget(self, action: #selector(selectButtonTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside)
}
@objc private func sliderValueChanged() {
updatePreview()
}
private func updatePreview() {
selectedColor = UIColor(
red: CGFloat(redSlider.value),
green: CGFloat(greenSlider.value),
blue: CGFloat(blueSlider.value),
alpha: 1.0
)
previewView.backgroundColor = selectedColor
}
@objc private func selectButtonTapped() {
// Call delegate method to pass data back
delegate?.colorPicker(self, didSelectColor: selectedColor)
navigationController?.popViewController(animated: true)
}
@objc private func cancelButtonTapped() {
// Call delegate method for cancellation
delegate?.colorPickerDidCancel(self)
navigationController?.popViewController(animated: true)
}
}
// MARK: - Step 3: Delegate ViewController (Parent)
class MainColorViewController: UIViewController {
private let colorDisplayView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 20
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.systemGray.cgColor
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let colorLabel: UILabel = {
let label = UILabel()
label.text = "No color selected"
label.textAlignment = .center
label.font = .systemFont(ofSize: 18, weight: .medium)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let chooseColorButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Choose Color", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Color Manager"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(colorDisplayView)
view.addSubview(colorLabel)
view.addSubview(chooseColorButton)
NSLayoutConstraint.activate([
colorDisplayView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
colorDisplayView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
colorDisplayView.widthAnchor.constraint(equalToConstant: 200),
colorDisplayView.heightAnchor.constraint(equalToConstant: 200),
colorLabel.topAnchor.constraint(equalTo: colorDisplayView.bottomAnchor, constant: 20),
colorLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
colorLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
chooseColorButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
chooseColorButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
chooseColorButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
chooseColorButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
chooseColorButton.addTarget(self, action: #selector(chooseColorButtonTapped), for: .touchUpInside)
}
@objc private func chooseColorButtonTapped() {
let colorPickerVC = ColorPickerViewController()
// Step 4: Set self as the delegate
colorPickerVC.delegate = self
navigationController?.pushViewController(colorPickerVC, animated: true)
}
}
// MARK: - Step 5: Conform to Protocol and Implement Methods
extension MainColorViewController: ColorPickerDelegate {
func colorPicker(_ picker: ColorPickerViewController, didSelectColor color: UIColor) {
// Receive data from child ViewController
colorDisplayView.backgroundColor = color
colorLabel.text = "Color selected successfully!"
// Save color to UserDefaults
saveColor(color)
// Perform additional actions
animateColorChange()
}
func colorPickerDidCancel(_ picker: ColorPickerViewController) {
colorLabel.text = "Color selection cancelled"
// Show cancellation feedback
showCancellationAlert()
}
private func saveColor(_ color: UIColor) {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
UserDefaults.standard.set(Double(red), forKey: "savedColorRed")
UserDefaults.standard.set(Double(green), forKey: "savedColorGreen")
UserDefaults.standard.set(Double(blue), forKey: "savedColorBlue")
}
private func animateColorChange() {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.3) {
self.colorDisplayView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
} completion: { _ in
UIView.animate(withDuration: 0.3) {
self.colorDisplayView.transform = .identity
}
}
}
private func showCancellationAlert() {
let alert = UIAlertController(
title: "Cancelled",
message: "Color selection was cancelled",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
Advanced Delegation with Multiple Data Types
import UIKit
// MARK: - Complex Protocol with Multiple Methods
protocol UserProfileEditorDelegate: AnyObject {
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateProfile profile: UserProfile)
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateAvatar image: UIImage)
func userProfileEditor(_ editor: UserProfileEditorViewController, didChangePrivacySettings settings: PrivacySettings)
func userProfileEditorDidRequestDelete(_ editor: UserProfileEditorViewController)
func userProfileEditorDidCancel(_ editor: UserProfileEditorViewController)
}
// MARK: - Data Models
struct UserProfile {
var firstName: String
var lastName: String
var email: String
var phoneNumber: String?
var bio: String?
var dateOfBirth: Date?
var location: String?
var website: URL?
}
struct PrivacySettings {
var profileVisibility: ProfileVisibility
var showEmail: Bool
var showPhoneNumber: Bool
var allowMessages: Bool
var allowFriendRequests: Bool
}
enum ProfileVisibility {
case publicProfile
case friendsOnly
case privateProfile
}
// MARK: - Editor ViewController
class UserProfileEditorViewController: UIViewController {
weak var delegate: UserProfileEditorDelegate?
var currentProfile: UserProfile?
var currentPrivacySettings: PrivacySettings?
// UI Elements
private let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
private let contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let avatarImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 60
imageView.backgroundColor = .systemGray5
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let firstNameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "First Name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let lastNameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Last Name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Email"
textField.borderStyle = .roundedRect
textField.keyboardType = .emailAddress
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let phoneTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Phone Number"
textField.borderStyle = .roundedRect
textField.keyboardType = .phonePad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let bioTextView: UITextView = {
let textView = UITextView()
textView.font = .systemFont(ofSize: 16)
textView.layer.borderColor = UIColor.systemGray4.cgColor
textView.layer.borderWidth = 1
textView.layer.cornerRadius = 8
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
private let profileVisibilitySegment: UISegmentedControl = {
let segment = UISegmentedControl(items: ["Public", "Friends", "Private"])
segment.selectedSegmentIndex = 0
segment.translatesAutoresizingMaskIntoConstraints = false
return segment
}()
private let showEmailSwitch: UISwitch = {
let toggle = UISwitch()
toggle.translatesAutoresizingMaskIntoConstraints = false
return toggle
}()
private let showPhoneSwitch: UISwitch = {
let toggle = UISwitch()
toggle.translatesAutoresizingMaskIntoConstraints = false
return toggle
}()
private let saveButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Save Changes", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let cancelButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Cancel", for: .normal)
button.backgroundColor = .systemGray5
button.setTitleColor(.systemBlue, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let deleteButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Delete Profile", for: .normal)
button.backgroundColor = .systemRed
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Edit Profile"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
populateFields()
}
private func setupUI() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(avatarImageView)
contentView.addSubview(firstNameTextField)
contentView.addSubview(lastNameTextField)
contentView.addSubview(emailTextField)
contentView.addSubview(phoneTextField)
contentView.addSubview(bioTextView)
contentView.addSubview(profileVisibilitySegment)
contentView.addSubview(showEmailSwitch)
contentView.addSubview(showPhoneSwitch)
contentView.addSubview(saveButton)
contentView.addSubview(cancelButton)
contentView.addSubview(deleteButton)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
avatarImageView.widthAnchor.constraint(equalToConstant: 120),
avatarImageView.heightAnchor.constraint(equalToConstant: 120),
firstNameTextField.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 30),
firstNameTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
firstNameTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
firstNameTextField.heightAnchor.constraint(equalToConstant: 44),
lastNameTextField.topAnchor.constraint(equalTo: firstNameTextField.bottomAnchor, constant: 16),
lastNameTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
lastNameTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
lastNameTextField.heightAnchor.constraint(equalToConstant: 44),
emailTextField.topAnchor.constraint(equalTo: lastNameTextField.bottomAnchor, constant: 16),
emailTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
emailTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
emailTextField.heightAnchor.constraint(equalToConstant: 44),
phoneTextField.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 16),
phoneTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
phoneTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
phoneTextField.heightAnchor.constraint(equalToConstant: 44),
bioTextView.topAnchor.constraint(equalTo: phoneTextField.bottomAnchor, constant: 16),
bioTextView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
bioTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
bioTextView.heightAnchor.constraint(equalToConstant: 100),
profileVisibilitySegment.topAnchor.constraint(equalTo: bioTextView.bottomAnchor, constant: 30),
profileVisibilitySegment.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
profileVisibilitySegment.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
saveButton.topAnchor.constraint(equalTo: profileVisibilitySegment.bottomAnchor, constant: 40),
saveButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
saveButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
saveButton.heightAnchor.constraint(equalToConstant: 50),
cancelButton.topAnchor.constraint(equalTo: saveButton.bottomAnchor, constant: 12),
cancelButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
cancelButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
cancelButton.heightAnchor.constraint(equalToConstant: 50),
deleteButton.topAnchor.constraint(equalTo: cancelButton.bottomAnchor, constant: 30),
deleteButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
deleteButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
deleteButton.heightAnchor.constraint(equalToConstant: 50),
deleteButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30)
])
}
private func setupActions() {
let avatarTapGesture = UITapGestureRecognizer(target: self, action: #selector(avatarTapped))
avatarImageView.addGestureRecognizer(avatarTapGesture)
saveButton.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside)
deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
profileVisibilitySegment.addTarget(self, action: #selector(privacySettingsChanged), for: .valueChanged)
showEmailSwitch.addTarget(self, action: #selector(privacySettingsChanged), for: .valueChanged)
showPhoneSwitch.addTarget(self, action: #selector(privacySettingsChanged), for: .valueChanged)
}
private func populateFields() {
guard let profile = currentProfile else { return }
firstNameTextField.text = profile.firstName
lastNameTextField.text = profile.lastName
emailTextField.text = profile.email
phoneTextField.text = profile.phoneNumber
bioTextView.text = profile.bio
if let settings = currentPrivacySettings {
switch settings.profileVisibility {
case .publicProfile: profileVisibilitySegment.selectedSegmentIndex = 0
case .friendsOnly: profileVisibilitySegment.selectedSegmentIndex = 1
case .privateProfile: profileVisibilitySegment.selectedSegmentIndex = 2
}
showEmailSwitch.isOn = settings.showEmail
showPhoneSwitch.isOn = settings.showPhoneNumber
}
}
@objc private func avatarTapped() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .photoLibrary
imagePicker.allowsEditing = true
present(imagePicker, animated: true)
}
@objc private func saveButtonTapped() {
guard validateInputs() else { return }
// Create updated profile
let updatedProfile = UserProfile(
firstName: firstNameTextField.text ?? "",
lastName: lastNameTextField.text ?? "",
email: emailTextField.text ?? "",
phoneNumber: phoneTextField.text,
bio: bioTextView.text,
dateOfBirth: currentProfile?.dateOfBirth,
location: currentProfile?.location,
website: currentProfile?.website
)
// Notify delegate about profile update
delegate?.userProfileEditor(self, didUpdateProfile: updatedProfile)
navigationController?.popViewController(animated: true)
}
@objc private func cancelButtonTapped() {
// Notify delegate about cancellation
delegate?.userProfileEditorDidCancel(self)
navigationController?.popViewController(animated: true)
}
@objc private func deleteButtonTapped() {
let alert = UIAlertController(
title: "Delete Profile",
message: "Are you sure you want to delete your profile? This action cannot be undone.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
guard let self = self else { return }
self.delegate?.userProfileEditorDidRequestDelete(self)
self.navigationController?.popViewController(animated: true)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
@objc private func privacySettingsChanged() {
let visibility: ProfileVisibility
switch profileVisibilitySegment.selectedSegmentIndex {
case 0: visibility = .publicProfile
case 1: visibility = .friendsOnly
case 2: visibility = .privateProfile
default: visibility = .publicProfile
}
let updatedSettings = PrivacySettings(
profileVisibility: visibility,
showEmail: showEmailSwitch.isOn,
showPhoneNumber: showPhoneSwitch.isOn,
allowMessages: currentPrivacySettings?.allowMessages ?? true,
allowFriendRequests: currentPrivacySettings?.allowFriendRequests ?? true
)
// Notify delegate about privacy settings change
delegate?.userProfileEditor(self, didChangePrivacySettings: updatedSettings)
}
private func validateInputs() -> Bool {
guard let firstName = firstNameTextField.text, !firstName.isEmpty,
let lastName = lastNameTextField.text, !lastName.isEmpty,
let email = emailTextField.text, !email.isEmpty else {
showAlert(message: "Please fill in all required fields")
return false
}
return true
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
// MARK: - UIImagePickerControllerDelegate
extension UserProfileEditorViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let editedImage = info[.editedImage] as? UIImage {
avatarImageView.image = editedImage
// Notify delegate about avatar change
delegate?.userProfileEditor(self, didUpdateAvatar: editedImage)
} else if let originalImage = info[.originalImage] as? UIImage {
avatarImageView.image = originalImage
delegate?.userProfileEditor(self, didUpdateAvatar: originalImage)
}
dismiss(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true)
}
}
// MARK: - Parent ViewController Implementing Delegate
class UserProfileViewController: UIViewController {
private var userProfile: UserProfile?
private var privacySettings: PrivacySettings?
private let profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 60
imageView.backgroundColor = .systemGray5
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let emailLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16)
label.textColor = .secondaryLabel
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let editButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Edit Profile", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "My Profile"
view.backgroundColor = .systemBackground
setupUI()
loadUserProfile()
}
private func setupUI() {
view.addSubview(profileImageView)
view.addSubview(nameLabel)
view.addSubview(emailLabel)
view.addSubview(editButton)
editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside)
NSLayoutConstraint.activate([
profileImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
profileImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
profileImageView.widthAnchor.constraint(equalToConstant: 120),
profileImageView.heightAnchor.constraint(equalToConstant: 120),
nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 20),
nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
emailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
emailLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
editButton.topAnchor.constraint(equalTo: emailLabel.bottomAnchor, constant: 40),
editButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
editButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
editButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func loadUserProfile() {
userProfile = UserProfile(
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
phoneNumber: "+1234567890",
bio: "iOS Developer"
)
privacySettings = PrivacySettings(
profileVisibility: .publicProfile,
showEmail: true,
showPhoneNumber: false,
allowMessages: true,
allowFriendRequests: true
)
updateUI()
}
private func updateUI() {
guard let profile = userProfile else { return }
nameLabel.text = "\(profile.firstName) \(profile.lastName)"
emailLabel.text = profile.email
}
@objc private func editButtonTapped() {
let editorVC = UserProfileEditorViewController()
editorVC.currentProfile = userProfile
editorVC.currentPrivacySettings = privacySettings
// Set self as delegate
editorVC.delegate = self
navigationController?.pushViewController(editorVC, animated: true)
}
}
// MARK: - Conform to Delegate Protocol
extension UserProfileViewController: UserProfileEditorDelegate {
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateProfile profile: UserProfile) {
// Receive updated profile
self.userProfile = profile
updateUI()
// Save to database or API
saveProfileToServer(profile)
// Show success message
showSuccessAlert(message: "Profile updated successfully!")
}
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateAvatar image: UIImage) {
// Receive new avatar
profileImageView.image = image
// Upload to server
uploadAvatarToServer(image)
}
func userProfileEditor(_ editor: UserProfileEditorViewController, didChangePrivacySettings settings: PrivacySettings) {
// Receive privacy settings changes
self.privacySettings = settings
// Save settings
savePrivacySettings(settings)
print("Privacy settings updated: \(settings)")
}
func userProfileEditorDidRequestDelete(_ editor: UserProfileEditorViewController) {
// Handle profile deletion request
let alert = UIAlertController(
title: "Confirm Deletion",
message: "Your profile will be permanently deleted. Continue?",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
self?.deleteProfile()
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
func userProfileEditorDidCancel(_ editor: UserProfileEditorViewController) {
print("Profile editing cancelled")
}
// Helper methods
private func saveProfileToServer(_ profile: UserProfile) {
// API call to save profile
print("Saving profile to server: \(profile)")
}
private func uploadAvatarToServer(_ image: UIImage) {
// API call to upload avatar
print("Uploading avatar to server")
}
private func savePrivacySettings(_ settings: PrivacySettings) {
// Save privacy settings
print("Saving privacy settings")
}
private func deleteProfile() {
// API call to delete profile
print("Deleting profile")
// Navigate back or logout
navigationController?.popToRootViewController(animated: true)
}
private func showSuccessAlert(message: String) {
let alert = UIAlertController(title: "Success", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
Optional Protocol Methods
import UIKit
// MARK: - Protocol with Optional Methods
@objc protocol FormDelegate: AnyObject {
func formDidSubmit(_ form: FormViewController, withData data: [String: Any])
// Optional methods
@objc optional func formDidStartEditing(_ form: FormViewController)
@objc optional func form(_ form: FormViewController, didUpdateField fieldName: String, value: Any)
@objc optional func formDidCancel(_ form: FormViewController)
@objc optional func form(_ form: FormViewController, didEncounterError error: Error)
}
// MARK: - Form View Controller
class FormViewController: UIViewController {
weak var delegate: FormDelegate?
private var formData: [String: Any] = [:]
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
// UI setup code
}
private func notifyEditingStarted() {
// Call optional delegate method
delegate?.formDidStartEditing?(self)
}
private func notifyFieldUpdate(fieldName: String, value: Any) {
formData[fieldName] = value
// Call optional delegate method
delegate?.form?(self, didUpdateField: fieldName, value: value)
}
@objc private func submitButtonTapped() {
// Call required delegate method
delegate?.formDidSubmit(self, withData: formData)
}
@objc private func cancelButtonTapped() {
// Call optional delegate method
delegate?.formDidCancel?(self)
}
private func handleError(_ error: Error) {
// Call optional delegate method
delegate?.form?(self, didEncounterError: error)
}
}
// MARK: - Implementing Optional Methods
class ParentViewController: UIViewController, FormDelegate {
// Required method
func formDidSubmit(_ form: FormViewController, withData data: [String : Any]) {
print("Form submitted with data: \(data)")
}
// Optional methods - implement only what you need
func form(_ form: FormViewController, didUpdateField fieldName: String, value: Any) {
print("Field \(fieldName) updated to: \(value)")
}
func formDidCancel(_ form: FormViewController) {
print("Form cancelled")
}
// formDidStartEditing and didEncounterError are not implemented
// No error because they're marked as @objc optional
}
Method 4: Closures and Callbacks
Closures (also known as callbacks or completion handlers) provide a modern, Swift-native way to pass data between ViewControllers. They're especially useful for asynchronous operations and simple backward data flow.
How It Works
- Define a closure property in the source ViewController
- The destination ViewController assigns a closure to this property
- When ready, the source ViewController executes the closure with data
- The destination ViewController receives and processes the data
Advantages
- ✅ Swift-native and modern
- ✅ Less boilerplate than delegates
- ✅ Perfect for simple callbacks
- ✅ Type-safe with generics
- ✅ Can capture context easily
- ✅ Inline code execution
Disadvantages
- ❌ Can lead to retain cycles if not careful
- ❌ Less discoverable than protocols
- ❌ Can become complex with multiple callbacks
- ❌ Harder to test than delegates
Basic Closure Implementation
import UIKit
// MARK: - Child ViewController with Closure
class ImagePickerViewController: UIViewController {
// Define closure type alias for readability
typealias ImageSelectionHandler = (UIImage) -> Void
// Closure property
var onImageSelected: ImageSelectionHandler?
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .systemGray6
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let selectButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Select Image", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let chooseButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Choose from Library", for: .normal)
button.backgroundColor = .systemGreen
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Choose Image"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(imageView)
view.addSubview(selectButton)
view.addSubview(chooseButton)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
imageView.heightAnchor.constraint(equalToConstant: 300),
chooseButton.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 20),
chooseButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
chooseButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
chooseButton.heightAnchor.constraint(equalToConstant: 50),
selectButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
selectButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
selectButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
selectButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
selectButton.addTarget(self, action: #selector(selectButtonTapped), for: .touchUpInside)
chooseButton.addTarget(self, action: #selector(chooseButtonTapped), for: .touchUpInside)
}
@objc private func selectButtonTapped() {
guard let image = imageView.image else {
showAlert(message: "Please choose an image first")
return
}
// Execute the closure to pass data back
onImageSelected?(image)
navigationController?.popViewController(animated: true)
}
@objc private func chooseButtonTapped() {
let picker = UIImagePickerController()
picker.delegate = self
picker.sourceType = .photoLibrary
present(picker, animated: true)
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
extension ImagePickerViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let selectedImage = info[.originalImage] as? UIImage {
imageView.image = selectedImage
}
dismiss(animated: true)
}
}
// MARK: - Parent ViewController Using Closure
class MainImageViewController: UIViewController {
private let displayImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .systemGray6
imageView.layer.cornerRadius = 12
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let changeImageButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Change Image", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Image Display"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(displayImageView)
view.addSubview(changeImageButton)
NSLayoutConstraint.activate([
displayImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
displayImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
displayImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
displayImageView.heightAnchor.constraint(equalToConstant: 300),
changeImageButton.topAnchor.constraint(equalTo: displayImageView.bottomAnchor, constant: 40),
changeImageButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
changeImageButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
changeImageButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
changeImageButton.addTarget(self, action: #selector(changeImageButtonTapped), for: .touchUpInside)
}
@objc private func changeImageButtonTapped() {
let pickerVC = ImagePickerViewController()
// Assign closure to receive data - capturing self weakly to avoid retain cycle
pickerVC.onImageSelected = { [weak self] selectedImage in
self?.displayImageView.image = selectedImage
self?.saveImage(selectedImage)
self?.showSuccessAnimation()
}
navigationController?.pushViewController(pickerVC, animated: true)
}
private func saveImage(_ image: UIImage) {
// Save image logic
print("Image saved successfully")
}
private func showSuccessAnimation() {
UIView.animate(withDuration: 0.3) {
self.displayImageView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
} completion: { _ in
UIView.animate(withDuration: 0.3) {
self.displayImageView.transform = .identity
}
}
}
}
import UIKit
// MARK: - Child ViewController with Closure
class ImagePickerViewController: UIViewController {
// Define closure type alias for readability
typealias ImageSelectionHandler = (UIImage) -> Void
// Closure property
var onImageSelected: ImageSelectionHandler?
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .systemGray6
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let selectButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Select Image", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let chooseButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Choose from Library", for: .normal)
button.backgroundColor = .systemGreen
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Choose Image"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(imageView)
view.addSubview(selectButton)
view.addSubview(chooseButton)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
imageView.heightAnchor.constraint(equalToConstant: 300),
chooseButton.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 20),
chooseButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
chooseButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
chooseButton.heightAnchor.constraint(equalToConstant: 50),
selectButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
selectButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
selectButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
selectButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
selectButton.addTarget(self, action: #selector(selectButtonTapped), for: .touchUpInside)
chooseButton.addTarget(self, action: #selector(chooseButtonTapped), for: .touchUpInside)
}
@objc private func selectButtonTapped() {
guard let image = imageView.image else {
showAlert(message: "Please choose an image first")
return
}
// Execute the closure to pass data back
onImageSelected?(image)
navigationController?.popViewController(animated: true)
}
@objc private func chooseButtonTapped() {
let picker = UIImagePickerController()
picker.delegate = self
picker.sourceType = .photoLibrary
present(picker, animated: true)
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
extension ImagePickerViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let selectedImage = info[.originalImage] as? UIImage {
imageView.image = selectedImage
}
dismiss(animated: true)
}
}
// MARK: - Parent ViewController Using Closure
class MainImageViewController: UIViewController {
private let displayImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .systemGray6
imageView.layer.cornerRadius = 12
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let changeImageButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Change Image", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Image Display"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(displayImageView)
view.addSubview(changeImageButton)
NSLayoutConstraint.activate([
displayImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
displayImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
displayImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
displayImageView.heightAnchor.constraint(equalToConstant: 300),
changeImageButton.topAnchor.constraint(equalTo: displayImageView.bottomAnchor, constant: 40),
changeImageButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
changeImageButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
changeImageButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
changeImageButton.addTarget(self, action: #selector(changeImageButtonTapped), for: .touchUpInside)
}
@objc private func changeImageButtonTapped() {
let pickerVC = ImagePickerViewController()
// Assign closure to receive data - capturing self weakly to avoid retain cycle
pickerVC.onImageSelected = { [weak self] selectedImage in
self?.displayImageView.image = selectedImage
self?.saveImage(selectedImage)
self?.showSuccessAnimation()
}
navigationController?.pushViewController(pickerVC, animated: true)
}
private func saveImage(_ image: UIImage) {
// Save image logic
print("Image saved successfully")
}
private func showSuccessAnimation() {
UIView.animate(withDuration: 0.3) {
self.displayImageView.transform = CGAffineTransform(scaleX: 1.05, y: 1.05)
} completion: { _ in
UIView.animate(withDuration: 0.3) {
self.displayImageView.transform = .identity
}
}
}
}
Multiple Closures for Different Events
import UIKit
// MARK: - Form ViewController with Multiple Closures
class FormViewController: UIViewController {
// Multiple closure properties for different events
var onSubmit: ((FormData) -> Void)?
var onCancel: (() -> Void)?
var onValidationError: ((String) -> Void)?
var onFieldChange: ((String, Any) -> Void)?
private var formData = FormData()
private let nameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Email"
textField.borderStyle = .roundedRect
textField.keyboardType = .emailAddress
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let ageTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Age"
textField.borderStyle = .roundedRect
textField.keyboardType = .numberPad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let submitButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Submit", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let cancelButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Cancel", for: .normal)
button.backgroundColor = .systemGray5
button.setTitleColor(.systemBlue, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "User Form"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(nameTextField)
view.addSubview(emailTextField)
view.addSubview(ageTextField)
view.addSubview(submitButton)
view.addSubview(cancelButton)
NSLayoutConstraint.activate([
nameTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
nameTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nameTextField.heightAnchor.constraint(equalToConstant: 44),
emailTextField.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 16),
emailTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
emailTextField.heightAnchor.constraint(equalToConstant: 44),
ageTextField.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 16),
ageTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
ageTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
ageTextField.heightAnchor.constraint(equalToConstant: 44),
submitButton.topAnchor.constraint(equalTo: ageTextField.bottomAnchor, constant: 40),
submitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
submitButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
submitButton.heightAnchor.constraint(equalToConstant: 50),
cancelButton.topAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: 12),
cancelButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
cancelButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
cancelButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
nameTextField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
emailTextField.addTarget(self, action: #selector(emailChanged), for: .editingChanged)
ageTextField.addTarget(self, action: #selector(ageChanged), for: .editingChanged)
submitButton.addTarget(self, action: #selector(submitButtonTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside)
}
@objc private func nameChanged() {
formData.name = nameTextField.text ?? ""
onFieldChange?("name", formData.name)
}
@objc private func emailChanged() {
formData.email = emailTextField.text ?? ""
onFieldChange?("email", formData.email)
}
@objc private func ageChanged() {
if let ageText = ageTextField.text, let age = Int(ageText) {
formData.age = age
onFieldChange?("age", age)
}
}
@objc private func submitButtonTapped() {
// Validate form
if !validateForm() {
return
}
// Execute submit closure
onSubmit?(formData)
navigationController?.popViewController(animated: true)
}
@objc private func cancelButtonTapped() {
// Execute cancel closure
onCancel?()
navigationController?.popViewController(animated: true)
}
private func validateForm() -> Bool {
if formData.name.isEmpty {
onValidationError?("Name is required")
return false
}
if formData.email.isEmpty {
onValidationError?("Email is required")
return false
}
if !isValidEmail(formData.email) {
onValidationError?("Invalid email format")
return false
}
if formData.age <= 0 {
onValidationError?("Please enter a valid age")
return false
}
return true
}
private func isValidEmail(_ email: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
}
// MARK: - Form Data Model
struct FormData {
var name: String = ""
var email: String = ""
var age: Int = 0
}
// MARK: - Parent ViewController Using Multiple Closures
class FormContainerViewController: UIViewController {
private let resultLabel: UILabel = {
let label = UILabel()
label.text = "No form submitted yet"
label.textAlignment = .center
label.numberOfLines = 0
label.font = .systemFont(ofSize: 16)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let showFormButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Show Form", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Form Manager"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(resultLabel)
view.addSubview(showFormButton)
NSLayoutConstraint.activate([
resultLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
resultLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
resultLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
showFormButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
showFormButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
showFormButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
showFormButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
showFormButton.addTarget(self, action: #selector(showFormButtonTapped), for: .touchUpInside)
}
@objc private func showFormButtonTapped() {
let formVC = FormViewController()
// Setup all closures
formVC.onSubmit = { [weak self] formData in
self?.handleFormSubmission(formData)
}
formVC.onCancel = { [weak self] in
self?.handleFormCancellation()
}
formVC.onValidationError = { [weak self] errorMessage in
self?.handleValidationError(errorMessage)
}
formVC.onFieldChange = { [weak self] fieldName, value in
self?.handleFieldChange(fieldName: fieldName, value: value)
}
navigationController?.pushViewController(formVC, animated: true)
}
private func handleFormSubmission(_ formData: FormData) {
resultLabel.text = """
Form Submitted!
Name: \(formData.name)
Email: \(formData.email)
Age: \(formData.age)
"""
// Save data to database
saveFormData(formData)
// Show success alert
showSuccessAlert()
}
private func handleFormCancellation() {
resultLabel.text = "Form submission cancelled"
print("User cancelled the form")
}
private func handleValidationError(_ errorMessage: String) {
// Show error alert
let alert = UIAlertController(title: "Validation Error", message: errorMessage, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func handleFieldChange(fieldName: String, value: Any) {
print("Field '\(fieldName)' changed to: \(value)")
// Could update UI or perform real-time validation
}
private func saveFormData(_ formData: FormData) {
// Save to UserDefaults, CoreData, or send to API
print("Saving form data: \(formData)")
}
private func showSuccessAlert() {
let alert = UIAlertController(title: "Success", message: "Form submitted successfully!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
import UIKit
// MARK: - Form ViewController with Multiple Closures
class FormViewController: UIViewController {
// Multiple closure properties for different events
var onSubmit: ((FormData) -> Void)?
var onCancel: (() -> Void)?
var onValidationError: ((String) -> Void)?
var onFieldChange: ((String, Any) -> Void)?
private var formData = FormData()
private let nameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Email"
textField.borderStyle = .roundedRect
textField.keyboardType = .emailAddress
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let ageTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Age"
textField.borderStyle = .roundedRect
textField.keyboardType = .numberPad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let submitButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Submit", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let cancelButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Cancel", for: .normal)
button.backgroundColor = .systemGray5
button.setTitleColor(.systemBlue, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "User Form"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(nameTextField)
view.addSubview(emailTextField)
view.addSubview(ageTextField)
view.addSubview(submitButton)
view.addSubview(cancelButton)
NSLayoutConstraint.activate([
nameTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
nameTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nameTextField.heightAnchor.constraint(equalToConstant: 44),
emailTextField.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 16),
emailTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
emailTextField.heightAnchor.constraint(equalToConstant: 44),
ageTextField.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 16),
ageTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
ageTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
ageTextField.heightAnchor.constraint(equalToConstant: 44),
submitButton.topAnchor.constraint(equalTo: ageTextField.bottomAnchor, constant: 40),
submitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
submitButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
submitButton.heightAnchor.constraint(equalToConstant: 50),
cancelButton.topAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: 12),
cancelButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
cancelButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
cancelButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
nameTextField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
emailTextField.addTarget(self, action: #selector(emailChanged), for: .editingChanged)
ageTextField.addTarget(self, action: #selector(ageChanged), for: .editingChanged)
submitButton.addTarget(self, action: #selector(submitButtonTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside)
}
@objc private func nameChanged() {
formData.name = nameTextField.text ?? ""
onFieldChange?("name", formData.name)
}
@objc private func emailChanged() {
formData.email = emailTextField.text ?? ""
onFieldChange?("email", formData.email)
}
@objc private func ageChanged() {
if let ageText = ageTextField.text, let age = Int(ageText) {
formData.age = age
onFieldChange?("age", age)
}
}
@objc private func submitButtonTapped() {
// Validate form
if !validateForm() {
return
}
// Execute submit closure
onSubmit?(formData)
navigationController?.popViewController(animated: true)
}
@objc private func cancelButtonTapped() {
// Execute cancel closure
onCancel?()
navigationController?.popViewController(animated: true)
}
private func validateForm() -> Bool {
if formData.name.isEmpty {
onValidationError?("Name is required")
return false
}
if formData.email.isEmpty {
onValidationError?("Email is required")
return false
}
if !isValidEmail(formData.email) {
onValidationError?("Invalid email format")
return false
}
if formData.age <= 0 {
onValidationError?("Please enter a valid age")
return false
}
return true
}
private func isValidEmail(_ email: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
}
// MARK: - Form Data Model
struct FormData {
var name: String = ""
var email: String = ""
var age: Int = 0
}
// MARK: - Parent ViewController Using Multiple Closures
class FormContainerViewController: UIViewController {
private let resultLabel: UILabel = {
let label = UILabel()
label.text = "No form submitted yet"
label.textAlignment = .center
label.numberOfLines = 0
label.font = .systemFont(ofSize: 16)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let showFormButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Show Form", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Form Manager"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(resultLabel)
view.addSubview(showFormButton)
NSLayoutConstraint.activate([
resultLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
resultLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
resultLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
resultLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
showFormButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
showFormButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
showFormButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
showFormButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
showFormButton.addTarget(self, action: #selector(showFormButtonTapped), for: .touchUpInside)
}
@objc private func showFormButtonTapped() {
let formVC = FormViewController()
// Setup all closures
formVC.onSubmit = { [weak self] formData in
self?.handleFormSubmission(formData)
}
formVC.onCancel = { [weak self] in
self?.handleFormCancellation()
}
formVC.onValidationError = { [weak self] errorMessage in
self?.handleValidationError(errorMessage)
}
formVC.onFieldChange = { [weak self] fieldName, value in
self?.handleFieldChange(fieldName: fieldName, value: value)
}
navigationController?.pushViewController(formVC, animated: true)
}
private func handleFormSubmission(_ formData: FormData) {
resultLabel.text = """
Form Submitted!
Name: \(formData.name)
Email: \(formData.email)
Age: \(formData.age)
"""
// Save data to database
saveFormData(formData)
// Show success alert
showSuccessAlert()
}
private func handleFormCancellation() {
resultLabel.text = "Form submission cancelled"
print("User cancelled the form")
}
private func handleValidationError(_ errorMessage: String) {
// Show error alert
let alert = UIAlertController(title: "Validation Error", message: errorMessage, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func handleFieldChange(fieldName: String, value: Any) {
print("Field '\(fieldName)' changed to: \(value)")
// Could update UI or perform real-time validation
}
private func saveFormData(_ formData: FormData) {
// Save to UserDefaults, CoreData, or send to API
print("Saving form data: \(formData)")
}
private func showSuccessAlert() {
let alert = UIAlertController(title: "Success", message: "Form submitted successfully!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
Escaping and Non-Escaping Closures
import UIKit
// MARK: - Understanding Escaping Closures
class DataFetchViewController: UIViewController {
// Escaping closure - executes after function returns
var onDataFetched: ((Result<[String], Error>) -> Void)?
private let activityIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(style: .large)
indicator.translatesAutoresizingMaskIntoConstraints = false
return indicator
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Fetch Data"
view.backgroundColor = .systemBackground
setupUI()
fetchData()
}
private func setupUI() {
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
activityIndicator.startAnimating()
}
private func fetchData() {
// Simulate API call with escaping closure
performAsyncOperation { [weak self] result in
DispatchQueue.main.async {
self?.activityIndicator.stopAnimating()
switch result {
case .success(let data):
self?.onDataFetched?(.success(data))
case .failure(let error):
self?.onDataFetched?(.failure(error))
}
self?.navigationController?.popViewController(animated: true)
}
}
}
// Escaping closure marked with @escaping
private func performAsyncOperation(completion: @escaping (Result<[String], Error>) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
let mockData = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]
completion(.success(mockData))
}
}
}
// MARK: - Parent Using Escaping Closure
class DataListViewController: UIViewController {
private var dataItems: [String] = []
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
private let fetchButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Fetch Data", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Data List"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(tableView)
view.addSubview(fetchButton)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: fetchButton.topAnchor, constant: -16),
fetchButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
fetchButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
fetchButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
fetchButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
fetchButton.addTarget(self, action: #selector(fetchButtonTapped), for: .touchUpInside)
}
@objc private func fetchButtonTapped() {
let fetchVC = DataFetchViewController()
// Assign escaping closure
fetchVC.onDataFetched = { [weak self] result in
switch result {
case .success(let data):
self?.dataItems = data
self?.tableView.reloadData()
self?.showSuccessMessage()
case .failure(let error):
self?.showErrorAlert(error: error)
}
}
navigationController?.pushViewController(fetchVC, animated: true)
}
private func showSuccessMessage() {
let alert = UIAlertController(title: "Success", message: "Data fetched successfully!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func showErrorAlert(error: Error) {
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
extension DataListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = dataItems[indexPath.row]
return cell
}
}
import UIKit
// MARK: - Understanding Escaping Closures
class DataFetchViewController: UIViewController {
// Escaping closure - executes after function returns
var onDataFetched: ((Result<[String], Error>) -> Void)?
private let activityIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(style: .large)
indicator.translatesAutoresizingMaskIntoConstraints = false
return indicator
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Fetch Data"
view.backgroundColor = .systemBackground
setupUI()
fetchData()
}
private func setupUI() {
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
activityIndicator.startAnimating()
}
private func fetchData() {
// Simulate API call with escaping closure
performAsyncOperation { [weak self] result in
DispatchQueue.main.async {
self?.activityIndicator.stopAnimating()
switch result {
case .success(let data):
self?.onDataFetched?(.success(data))
case .failure(let error):
self?.onDataFetched?(.failure(error))
}
self?.navigationController?.popViewController(animated: true)
}
}
}
// Escaping closure marked with @escaping
private func performAsyncOperation(completion: @escaping (Result<[String], Error>) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
let mockData = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]
completion(.success(mockData))
}
}
}
// MARK: - Parent Using Escaping Closure
class DataListViewController: UIViewController {
private var dataItems: [String] = []
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
private let fetchButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Fetch Data", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Data List"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(tableView)
view.addSubview(fetchButton)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: fetchButton.topAnchor, constant: -16),
fetchButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
fetchButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
fetchButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
fetchButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
fetchButton.addTarget(self, action: #selector(fetchButtonTapped), for: .touchUpInside)
}
@objc private func fetchButtonTapped() {
let fetchVC = DataFetchViewController()
// Assign escaping closure
fetchVC.onDataFetched = { [weak self] result in
switch result {
case .success(let data):
self?.dataItems = data
self?.tableView.reloadData()
self?.showSuccessMessage()
case .failure(let error):
self?.showErrorAlert(error: error)
}
}
navigationController?.pushViewController(fetchVC, animated: true)
}
private func showSuccessMessage() {
let alert = UIAlertController(title: "Success", message: "Data fetched successfully!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func showErrorAlert(error: Error) {
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
extension DataListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = dataItems[indexPath.row]
return cell
}
}
Generic Closures for Reusability
import UIKit
// MARK: - Generic Picker ViewController
class GenericPickerViewController<T>: UIViewController where T: CustomStringConvertible {
// Generic closure
var onItemSelected: ((T) -> Void)?
var items: [T] = []
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Select Item"
view.backgroundColor = .systemBackground
setupUI()
}
private func setupUI() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
extension GenericPickerViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].description
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedItem = items[indexPath.row]
onItemSelected?(selectedItem)
navigationController?.popViewController(animated: true)
}
}
// MARK: - Using Generic Picker
enum Priority: String, CustomStringConvertible {
case low = "Low"
case medium = "Medium"
case high = "High"
case critical = "Critical"
var description: String {
return self.rawValue
}
}
struct Category: CustomStringConvertible {
let id: Int
let name: String
let icon: String
var description: String {
return "\(icon) \(name)"
}
}
class TaskCreatorViewController: UIViewController {
private var selectedPriority: Priority = .medium
private var selectedCategory: Category?
private let priorityButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Priority: Medium", for: .normal)
button.backgroundColor = .systemGray6
button.setTitleColor(.label, for: .normal)
button.layer.cornerRadius = 8
button.contentHorizontalAlignment = .left
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let categoryButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Category: None", for: .normal)
button.backgroundColor = .systemGray6
button.setTitleColor(.label, for: .normal)
button.layer.cornerRadius = 8
button.contentHorizontalAlignment = .left
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Create Task"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(priorityButton)
view.addSubview(categoryButton)
NSLayoutConstraint.activate([
priorityButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
priorityButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
priorityButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
priorityButton.heightAnchor.constraint(equalToConstant: 50),
categoryButton.topAnchor.constraint(equalTo: priorityButton.bottomAnchor, constant: 16),
categoryButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
categoryButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
categoryButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
priorityButton.addTarget(self, action: #selector(priorityButtonTapped), for: .touchUpInside)
categoryButton.addTarget(self, action: #selector(categoryButtonTapped), for: .touchUpInside)
}
@objc private func priorityButtonTapped() {
let pickerVC = GenericPickerViewController<Priority>()
pickerVC.items = [.low, .medium, .high, .critical]
// Use generic closure for Priority type
pickerVC.onItemSelected = { [weak self] priority in
self?.selectedPriority = priority
self?.priorityButton.setTitle("Priority: \(priority.rawValue)", for: .normal)
}
navigationController?.pushViewController(pickerVC, animated: true)
}
@objc private func categoryButtonTapped() {
let pickerVC = GenericPickerViewController<Category>()
pickerVC.items = [
Category(id: 1, name: "Work", icon: "💼"),
Category(id: 2, name: "Personal", icon: "🏠"),
Category(id: 3, name: "Shopping", icon: "🛒"),
Category(id: 4, name: "Health", icon: "⚕️")
]
// Use generic closure for Category type
pickerVC.onItemSelected = { [weak self] category in
self?.selectedCategory = category
self?.categoryButton.setTitle("Category: \(category.name)", for: .normal)
}
navigationController?.pushViewController(pickerVC, animated: true)
}
}
import UIKit
// MARK: - Generic Picker ViewController
class GenericPickerViewController<T>: UIViewController where T: CustomStringConvertible {
// Generic closure
var onItemSelected: ((T) -> Void)?
var items: [T] = []
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Select Item"
view.backgroundColor = .systemBackground
setupUI()
}
private func setupUI() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
extension GenericPickerViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].description
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedItem = items[indexPath.row]
onItemSelected?(selectedItem)
navigationController?.popViewController(animated: true)
}
}
// MARK: - Using Generic Picker
enum Priority: String, CustomStringConvertible {
case low = "Low"
case medium = "Medium"
case high = "High"
case critical = "Critical"
var description: String {
return self.rawValue
}
}
struct Category: CustomStringConvertible {
let id: Int
let name: String
let icon: String
var description: String {
return "\(icon) \(name)"
}
}
class TaskCreatorViewController: UIViewController {
private var selectedPriority: Priority = .medium
private var selectedCategory: Category?
private let priorityButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Priority: Medium", for: .normal)
button.backgroundColor = .systemGray6
button.setTitleColor(.label, for: .normal)
button.layer.cornerRadius = 8
button.contentHorizontalAlignment = .left
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let categoryButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Category: None", for: .normal)
button.backgroundColor = .systemGray6
button.setTitleColor(.label, for: .normal)
button.layer.cornerRadius = 8
button.contentHorizontalAlignment = .left
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Create Task"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(priorityButton)
view.addSubview(categoryButton)
NSLayoutConstraint.activate([
priorityButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
priorityButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
priorityButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
priorityButton.heightAnchor.constraint(equalToConstant: 50),
categoryButton.topAnchor.constraint(equalTo: priorityButton.bottomAnchor, constant: 16),
categoryButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
categoryButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
categoryButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
priorityButton.addTarget(self, action: #selector(priorityButtonTapped), for: .touchUpInside)
categoryButton.addTarget(self, action: #selector(categoryButtonTapped), for: .touchUpInside)
}
@objc private func priorityButtonTapped() {
let pickerVC = GenericPickerViewController<Priority>()
pickerVC.items = [.low, .medium, .high, .critical]
// Use generic closure for Priority type
pickerVC.onItemSelected = { [weak self] priority in
self?.selectedPriority = priority
self?.priorityButton.setTitle("Priority: \(priority.rawValue)", for: .normal)
}
navigationController?.pushViewController(pickerVC, animated: true)
}
@objc private func categoryButtonTapped() {
let pickerVC = GenericPickerViewController<Category>()
pickerVC.items = [
Category(id: 1, name: "Work", icon: "💼"),
Category(id: 2, name: "Personal", icon: "🏠"),
Category(id: 3, name: "Shopping", icon: "🛒"),
Category(id: 4, name: "Health", icon: "⚕️")
]
// Use generic closure for Category type
pickerVC.onItemSelected = { [weak self] category in
self?.selectedCategory = category
self?.categoryButton.setTitle("Category: \(category.name)", for: .normal)
}
navigationController?.pushViewController(pickerVC, animated: true)
}
}
Method 5: NotificationCenter
NotificationCenter provides a broadcast mechanism for sending information between objects in your application. It's particularly useful for one-to-many communication where multiple observers need to react to the same event.
How It Works
- Define a notification name
- Observers register to receive notifications
- Senders post notifications with optional data
- All registered observers receive the notification
- Observers process the notification data
Advantages
- ✅ One-to-many communication
- ✅ Decoupled components
- ✅ System-wide broadcasting
- ✅ Can pass data with userInfo
- ✅ Built into Foundation
Disadvantages
- ❌ No compile-time checking
- ❌ String-based notification names
- ❌ Hard to track data flow
- ❌ Must remember to remove observers
- ❌ Can lead to difficult debugging
Basic NotificationCenter Implementation
import UIKit
// MARK: - Notification Name Extension
extension Notification.Name {
static let userDidLogin = Notification.Name("userDidLogin")
static let userDidLogout = Notification.Name("userDidLogout")
static let themeDidChange = Notification.Name("themeDidChange")
static let dataDidRefresh = Notification.Name("dataDidRefresh")
}
// MARK: - Login ViewController (Sender)
class LoginViewController: UIViewController {
private let usernameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Username"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let passwordTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Password"
textField.borderStyle = .roundedRect
textField.isSecureTextEntry = true
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let loginButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Login", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Login"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(usernameTextField)
view.addSubview(passwordTextField)
view.addSubview(loginButton)
NSLayoutConstraint.activate([
usernameTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
usernameTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
usernameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
usernameTextField.heightAnchor.constraint(equalToConstant: 44),
passwordTextField.topAnchor.constraint(equalTo: usernameTextField.bottomAnchor, constant: 16),
passwordTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
passwordTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
passwordTextField.heightAnchor.constraint(equalToConstant: 44),
loginButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 40),
loginButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
loginButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
loginButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
}
@objc private func loginButtonTapped() {
guard let username = usernameTextField.text, !username.isEmpty,
let password = passwordTextField.text, !password.isEmpty else {
showAlert(message: "Please fill in all fields")
return
}
// Simulate login
performLogin(username: username, password: password)
}
private func performLogin(username: String, password: String) {
// Simulate API call
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
let userInfo: [String: Any] = [
"username": username,
"userId": UUID().uuidString,
"loginTime": Date(),
"accessToken": "sample_token_\(Int.random(in: 1000...9999))"
]
// Post notification with user data
NotificationCenter.default.post(
name: .userDidLogin,
object: self,
userInfo: userInfo
)
self?.dismiss(animated: true)
}
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
// MARK: - Dashboard ViewController (Observer)
class DashboardViewController: UIViewController {
private var currentUser: String = "Not logged in"
private let welcomeLabel: UILabel = {
let label = UILabel()
label.text = "Welcome!"
label.font = .systemFont(ofSize: 28, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let userStatusLabel: UILabel = {
let label = UILabel()
label.text = "Please log in"
label.font = .systemFont(ofSize: 16)
label.textColor = .secondaryLabel
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let loginButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Login", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let logoutButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Logout", for: .normal)
button.backgroundColor = .systemRed
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.isHidden = true
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Dashboard"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
registerForNotifications()
}
private func setupUI() {
view.addSubview(welcomeLabel)
view.addSubview(userStatusLabel)
view.addSubview(loginButton)
view.addSubview(logoutButton)
NSLayoutConstraint.activate([
welcomeLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 80),
welcomeLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
welcomeLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
userStatusLabel.topAnchor.constraint(equalTo: welcomeLabel.bottomAnchor, constant: 16),
userStatusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
userStatusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
loginButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
loginButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
loginButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
loginButton.heightAnchor.constraint(equalToConstant: 50),
logoutButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
logoutButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
logoutButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
logoutButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
logoutButton.addTarget(self, action: #selector(logoutButtonTapped), for: .touchUpInside)
}
private func registerForNotifications() {
// Register for login notification
NotificationCenter.default.addObserver(
self,
selector: #selector(handleUserDidLogin(_:)),
name: .userDidLogin,
object: nil
)
// Register for logout notification
NotificationCenter.default.addObserver(
self,
selector: #selector(handleUserDidLogout(_:)),
name: .userDidLogout,
object: nil
)
}
@objc private func handleUserDidLogin(_ notification: Notification) {
// Extract data from notification
guard let userInfo = notification.userInfo,
let username = userInfo["username"] as? String,
let userId = userInfo["userId"] as? String,
let loginTime = userInfo["loginTime"] as? Date else {
return
}
currentUser = username
// Update UI
welcomeLabel.text = "Welcome, \(username)!"
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
userStatusLabel.text = "Logged in at \(formatter.string(from: loginTime))\nUser ID: \(userId)"
loginButton.isHidden = true
logoutButton.isHidden = false
// Animate transition
animateLoginSuccess()
}
@objc private func handleUserDidLogout(_ notification: Notification) {
currentUser = "Not logged in"
welcomeLabel.text = "Welcome!"
userStatusLabel.text = "Please log in"
loginButton.isHidden = false
logoutButton.isHidden = true
}
@objc private func loginButtonTapped() {
let loginVC = LoginViewController()
let navController = UINavigationController(rootViewController: loginVC)
navController.modalPresentationStyle = .fullScreen
present(navController, animated: true)
}
@objc private func logoutButtonTapped() {
// Post logout notification
NotificationCenter.default.post(name: .userDidLogout, object: self)
}
private func animateLoginSuccess() {
welcomeLabel.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
welcomeLabel.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.3) {
self.welcomeLabel.transform = .identity
self.welcomeLabel.alpha = 1
}
}
deinit {
// Remove observers when view controller is deallocated
NotificationCenter.default.removeObserver(self)
}
}
// MARK: - Profile ViewController (Another Observer)
class ProfileViewController: UIViewController {
private let userInfoLabel: UILabel = {
let label = UILabel()
label.text = "No user logged in"
label.font = .systemFont(ofSize: 18)
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Profile"
view.backgroundColor = .systemBackground
setupUI()
registerForNotifications()
}
private func setupUI() {
view.addSubview(userInfoLabel)
NSLayoutConstraint.activate([
userInfoLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
userInfoLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
userInfoLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
userInfoLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
private func registerForNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleUserDidLogin(_:)),
name: .userDidLogin,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleUserDidLogout(_:)),
name: .userDidLogout,
object: nil
)
}
@objc private func handleUserDidLogin(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let username = userInfo["username"] as? String,
let userId = userInfo["userId"] as? String else {
return
}
userInfoLabel.text = """
User: \(username)
ID: \(userId)
Status: Active
"""
}
@objc private func handleUserDidLogout(_ notification: Notification) {
userInfoLabel.text = "No user logged in"
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
import UIKit
// MARK: - Notification Name Extension
extension Notification.Name {
static let userDidLogin = Notification.Name("userDidLogin")
static let userDidLogout = Notification.Name("userDidLogout")
static let themeDidChange = Notification.Name("themeDidChange")
static let dataDidRefresh = Notification.Name("dataDidRefresh")
}
// MARK: - Login ViewController (Sender)
class LoginViewController: UIViewController {
private let usernameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Username"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let passwordTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Password"
textField.borderStyle = .roundedRect
textField.isSecureTextEntry = true
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let loginButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Login", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Login"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(usernameTextField)
view.addSubview(passwordTextField)
view.addSubview(loginButton)
NSLayoutConstraint.activate([
usernameTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
usernameTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
usernameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
usernameTextField.heightAnchor.constraint(equalToConstant: 44),
passwordTextField.topAnchor.constraint(equalTo: usernameTextField.bottomAnchor, constant: 16),
passwordTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
passwordTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
passwordTextField.heightAnchor.constraint(equalToConstant: 44),
loginButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 40),
loginButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
loginButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
loginButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
}
@objc private func loginButtonTapped() {
guard let username = usernameTextField.text, !username.isEmpty,
let password = passwordTextField.text, !password.isEmpty else {
showAlert(message: "Please fill in all fields")
return
}
// Simulate login
performLogin(username: username, password: password)
}
private func performLogin(username: String, password: String) {
// Simulate API call
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
let userInfo: [String: Any] = [
"username": username,
"userId": UUID().uuidString,
"loginTime": Date(),
"accessToken": "sample_token_\(Int.random(in: 1000...9999))"
]
// Post notification with user data
NotificationCenter.default.post(
name: .userDidLogin,
object: self,
userInfo: userInfo
)
self?.dismiss(animated: true)
}
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
// MARK: - Dashboard ViewController (Observer)
class DashboardViewController: UIViewController {
private var currentUser: String = "Not logged in"
private let welcomeLabel: UILabel = {
let label = UILabel()
label.text = "Welcome!"
label.font = .systemFont(ofSize: 28, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let userStatusLabel: UILabel = {
let label = UILabel()
label.text = "Please log in"
label.font = .systemFont(ofSize: 16)
label.textColor = .secondaryLabel
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let loginButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Login", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let logoutButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Logout", for: .normal)
button.backgroundColor = .systemRed
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.isHidden = true
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Dashboard"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
registerForNotifications()
}
private func setupUI() {
view.addSubview(welcomeLabel)
view.addSubview(userStatusLabel)
view.addSubview(loginButton)
view.addSubview(logoutButton)
NSLayoutConstraint.activate([
welcomeLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 80),
welcomeLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
welcomeLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
userStatusLabel.topAnchor.constraint(equalTo: welcomeLabel.bottomAnchor, constant: 16),
userStatusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
userStatusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
loginButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
loginButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
loginButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
loginButton.heightAnchor.constraint(equalToConstant: 50),
logoutButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
logoutButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
logoutButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
logoutButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
logoutButton.addTarget(self, action: #selector(logoutButtonTapped), for: .touchUpInside)
}
private func registerForNotifications() {
// Register for login notification
NotificationCenter.default.addObserver(
self,
selector: #selector(handleUserDidLogin(_:)),
name: .userDidLogin,
object: nil
)
// Register for logout notification
NotificationCenter.default.addObserver(
self,
selector: #selector(handleUserDidLogout(_:)),
name: .userDidLogout,
object: nil
)
}
@objc private func handleUserDidLogin(_ notification: Notification) {
// Extract data from notification
guard let userInfo = notification.userInfo,
let username = userInfo["username"] as? String,
let userId = userInfo["userId"] as? String,
let loginTime = userInfo["loginTime"] as? Date else {
return
}
currentUser = username
// Update UI
welcomeLabel.text = "Welcome, \(username)!"
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
userStatusLabel.text = "Logged in at \(formatter.string(from: loginTime))\nUser ID: \(userId)"
loginButton.isHidden = true
logoutButton.isHidden = false
// Animate transition
animateLoginSuccess()
}
@objc private func handleUserDidLogout(_ notification: Notification) {
currentUser = "Not logged in"
welcomeLabel.text = "Welcome!"
userStatusLabel.text = "Please log in"
loginButton.isHidden = false
logoutButton.isHidden = true
}
@objc private func loginButtonTapped() {
let loginVC = LoginViewController()
let navController = UINavigationController(rootViewController: loginVC)
navController.modalPresentationStyle = .fullScreen
present(navController, animated: true)
}
@objc private func logoutButtonTapped() {
// Post logout notification
NotificationCenter.default.post(name: .userDidLogout, object: self)
}
private func animateLoginSuccess() {
welcomeLabel.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
welcomeLabel.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.3) {
self.welcomeLabel.transform = .identity
self.welcomeLabel.alpha = 1
}
}
deinit {
// Remove observers when view controller is deallocated
NotificationCenter.default.removeObserver(self)
}
}
// MARK: - Profile ViewController (Another Observer)
class ProfileViewController: UIViewController {
private let userInfoLabel: UILabel = {
let label = UILabel()
label.text = "No user logged in"
label.font = .systemFont(ofSize: 18)
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Profile"
view.backgroundColor = .systemBackground
setupUI()
registerForNotifications()
}
private func setupUI() {
view.addSubview(userInfoLabel)
NSLayoutConstraint.activate([
userInfoLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
userInfoLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
userInfoLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
userInfoLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
private func registerForNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleUserDidLogin(_:)),
name: .userDidLogin,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleUserDidLogout(_:)),
name: .userDidLogout,
object: nil
)
}
@objc private func handleUserDidLogin(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let username = userInfo["username"] as? String,
let userId = userInfo["userId"] as? String else {
return
}
userInfoLabel.text = """
User: \(username)
ID: \(userId)
Status: Active
"""
}
@objc private func handleUserDidLogout(_ notification: Notification) {
userInfoLabel.text = "No user logged in"
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
Advanced NotificationCenter with Custom Objects
import UIKit
// MARK: - Custom Notification Object
class DataUpdateNotification {
let updateType: UpdateType
let affectedItems: [String]
let timestamp: Date
let source: String
enum UpdateType {
case created
case updated
case deleted
case synced
}
init(updateType: UpdateType, affectedItems: [String], source: String) {
self.updateType = updateType
self.affectedItems = affectedItems
self.timestamp = Date()
self.source = source
}
}
// MARK: - Notification Names
extension Notification.Name {
static let dataDidUpdate = Notification.Name("dataDidUpdate")
static let syncDidComplete = Notification.Name("syncDidComplete")
static let connectionStatusChanged = Notification.Name("connectionStatusChanged")
}
// MARK: - Data Manager (Notification Sender)
class DataManager {
static let shared = DataManager()
private init() {}
func createItem(name: String) {
// Simulate item creation
print("Creating item: \(name)")
let updateNotification = DataUpdateNotification(
updateType: .created,
affectedItems: [name],
source: "DataManager"
)
// Post notification with custom object
NotificationCenter.default.post(
name: .dataDidUpdate,
object: updateNotification
)
}
func updateItems(names: [String]) {
print("Updating items: \(names)")
let updateNotification = DataUpdateNotification(
updateType: .updated,
affectedItems: names,
source: "DataManager"
)
NotificationCenter.default.post(
name: .dataDidUpdate,
object: updateNotification
)
}
func deleteItem(name: String) {
print("Deleting item: \(name)")
let updateNotification = DataUpdateNotification(
updateType: .deleted,
affectedItems: [name],
source: "DataManager"
)
NotificationCenter.default.post(
name: .dataDidUpdate,
object: updateNotification
)
}
func syncData() {
print("Syncing data...")
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
let syncInfo: [String: Any] = [
"itemCount": 150,
"syncDuration": 2.0,
"success": true,
"timestamp": Date()
]
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .syncDidComplete,
object: nil,
userInfo: syncInfo
)
}
}
}
}
// MARK: - List ViewController (Observer)
class ListViewController: UIViewController {
private var items: [String] = []
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
private let statusLabel: UILabel = {
let label = UILabel()
label.text = "Ready"
label.font = .systemFont(ofSize: 14)
label.textColor = .secondaryLabel
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let addButton: UIBarButtonItem = {
return UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil)
}()
private let syncButton: UIBarButtonItem = {
return UIBarButtonItem(barButtonSystemItem: .refresh, target: nil, action: nil)
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Items"
view.backgroundColor = .systemBackground
setupUI()
setupNavigationBar()
registerForNotifications()
loadInitialData()
}
private func setupUI() {
view.addSubview(tableView)
view.addSubview(statusLabel)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: statusLabel.topAnchor),
statusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
statusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
statusLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -8),
statusLabel.heightAnchor.constraint(equalToConstant: 30)
])
}
private func setupNavigationBar() {
addButton.target = self
addButton.action = #selector(addButtonTapped)
syncButton.target = self
syncButton.action = #selector(syncButtonTapped)
navigationItem.rightBarButtonItems = [addButton, syncButton]
}
private func registerForNotifications() {
// Register for data updates
NotificationCenter.default.addObserver(
self,
selector: #selector(handleDataUpdate(_:)),
name: .dataDidUpdate,
object: nil
)
// Register for sync completion
NotificationCenter.default.addObserver(
self,
selector: #selector(handleSyncComplete(_:)),
name: .syncDidComplete,
object: nil
)
}
@objc private func handleDataUpdate(_ notification: Notification) {
guard let updateInfo = notification.object as? DataUpdateNotification else {
return
}
switch updateInfo.updateType {
case .created:
items.append(contentsOf: updateInfo.affectedItems)
statusLabel.text = "Added \(updateInfo.affectedItems.count) item(s)"
case .updated:
statusLabel.text = "Updated \(updateInfo.affectedItems.count) item(s)"
case .deleted:
items.removeAll { updateInfo.affectedItems.contains($0) }
statusLabel.text = "Deleted \(updateInfo.affectedItems.count) item(s)"
case .synced:
statusLabel.text = "Data synced successfully"
}
tableView.reloadData()
// Reset status after delay
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [weak self] in
self?.statusLabel.text = "Ready"
}
}
@objc private func handleSyncComplete(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let itemCount = userInfo["itemCount"] as? Int,
let duration = userInfo["syncDuration"] as? Double,
let success = userInfo["success"] as? Bool else {
return
}
if success {
statusLabel.text = "Synced \(itemCount) items in \(String(format: "%.1f", duration))s"
} else {
statusLabel.text = "Sync failed"
}
}
@objc private func addButtonTapped() {
let alert = UIAlertController(title: "Add Item", message: nil, preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = "Item name"
}
alert.addAction(UIAlertAction(title: "Add", style: .default) { [weak self] _ in
guard let name = alert.textFields?.first?.text, !name.isEmpty else { return }
DataManager.shared.createItem(name: name)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
@objc private func syncButtonTapped() {
statusLabel.text = "Syncing..."
DataManager.shared.syncData()
}
private func loadInitialData() {
items = ["Item 1", "Item 2", "Item 3"]
tableView.reloadData()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
extension ListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let itemName = items[indexPath.row]
DataManager.shared.deleteItem(name: itemName)
}
}
}
import UIKit
// MARK: - Custom Notification Object
class DataUpdateNotification {
let updateType: UpdateType
let affectedItems: [String]
let timestamp: Date
let source: String
enum UpdateType {
case created
case updated
case deleted
case synced
}
init(updateType: UpdateType, affectedItems: [String], source: String) {
self.updateType = updateType
self.affectedItems = affectedItems
self.timestamp = Date()
self.source = source
}
}
// MARK: - Notification Names
extension Notification.Name {
static let dataDidUpdate = Notification.Name("dataDidUpdate")
static let syncDidComplete = Notification.Name("syncDidComplete")
static let connectionStatusChanged = Notification.Name("connectionStatusChanged")
}
// MARK: - Data Manager (Notification Sender)
class DataManager {
static let shared = DataManager()
private init() {}
func createItem(name: String) {
// Simulate item creation
print("Creating item: \(name)")
let updateNotification = DataUpdateNotification(
updateType: .created,
affectedItems: [name],
source: "DataManager"
)
// Post notification with custom object
NotificationCenter.default.post(
name: .dataDidUpdate,
object: updateNotification
)
}
func updateItems(names: [String]) {
print("Updating items: \(names)")
let updateNotification = DataUpdateNotification(
updateType: .updated,
affectedItems: names,
source: "DataManager"
)
NotificationCenter.default.post(
name: .dataDidUpdate,
object: updateNotification
)
}
func deleteItem(name: String) {
print("Deleting item: \(name)")
let updateNotification = DataUpdateNotification(
updateType: .deleted,
affectedItems: [name],
source: "DataManager"
)
NotificationCenter.default.post(
name: .dataDidUpdate,
object: updateNotification
)
}
func syncData() {
print("Syncing data...")
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
let syncInfo: [String: Any] = [
"itemCount": 150,
"syncDuration": 2.0,
"success": true,
"timestamp": Date()
]
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .syncDidComplete,
object: nil,
userInfo: syncInfo
)
}
}
}
}
// MARK: - List ViewController (Observer)
class ListViewController: UIViewController {
private var items: [String] = []
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
private let statusLabel: UILabel = {
let label = UILabel()
label.text = "Ready"
label.font = .systemFont(ofSize: 14)
label.textColor = .secondaryLabel
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let addButton: UIBarButtonItem = {
return UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil)
}()
private let syncButton: UIBarButtonItem = {
return UIBarButtonItem(barButtonSystemItem: .refresh, target: nil, action: nil)
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Items"
view.backgroundColor = .systemBackground
setupUI()
setupNavigationBar()
registerForNotifications()
loadInitialData()
}
private func setupUI() {
view.addSubview(tableView)
view.addSubview(statusLabel)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: statusLabel.topAnchor),
statusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
statusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
statusLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -8),
statusLabel.heightAnchor.constraint(equalToConstant: 30)
])
}
private func setupNavigationBar() {
addButton.target = self
addButton.action = #selector(addButtonTapped)
syncButton.target = self
syncButton.action = #selector(syncButtonTapped)
navigationItem.rightBarButtonItems = [addButton, syncButton]
}
private func registerForNotifications() {
// Register for data updates
NotificationCenter.default.addObserver(
self,
selector: #selector(handleDataUpdate(_:)),
name: .dataDidUpdate,
object: nil
)
// Register for sync completion
NotificationCenter.default.addObserver(
self,
selector: #selector(handleSyncComplete(_:)),
name: .syncDidComplete,
object: nil
)
}
@objc private func handleDataUpdate(_ notification: Notification) {
guard let updateInfo = notification.object as? DataUpdateNotification else {
return
}
switch updateInfo.updateType {
case .created:
items.append(contentsOf: updateInfo.affectedItems)
statusLabel.text = "Added \(updateInfo.affectedItems.count) item(s)"
case .updated:
statusLabel.text = "Updated \(updateInfo.affectedItems.count) item(s)"
case .deleted:
items.removeAll { updateInfo.affectedItems.contains($0) }
statusLabel.text = "Deleted \(updateInfo.affectedItems.count) item(s)"
case .synced:
statusLabel.text = "Data synced successfully"
}
tableView.reloadData()
// Reset status after delay
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { [weak self] in
self?.statusLabel.text = "Ready"
}
}
@objc private func handleSyncComplete(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let itemCount = userInfo["itemCount"] as? Int,
let duration = userInfo["syncDuration"] as? Double,
let success = userInfo["success"] as? Bool else {
return
}
if success {
statusLabel.text = "Synced \(itemCount) items in \(String(format: "%.1f", duration))s"
} else {
statusLabel.text = "Sync failed"
}
}
@objc private func addButtonTapped() {
let alert = UIAlertController(title: "Add Item", message: nil, preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = "Item name"
}
alert.addAction(UIAlertAction(title: "Add", style: .default) { [weak self] _ in
guard let name = alert.textFields?.first?.text, !name.isEmpty else { return }
DataManager.shared.createItem(name: name)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
@objc private func syncButtonTapped() {
statusLabel.text = "Syncing..."
DataManager.shared.syncData()
}
private func loadInitialData() {
items = ["Item 1", "Item 2", "Item 3"]
tableView.reloadData()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
extension ListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let itemName = items[indexPath.row]
DataManager.shared.deleteItem(name: itemName)
}
}
}
NotificationCenter with Queue and Modern API
import UIKit
// MARK: - Modern NotificationCenter Usage (iOS 13+)
class ModernNotificationViewController: UIViewController {
private var observations = [NSObjectProtocol]()
private let statusLabel: UILabel = {
let label = UILabel()
label.text = "Waiting for events..."
label.numberOfLines = 0
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Modern Notifications"
view.backgroundColor = .systemBackground
setupUI()
setupModernObservers()
}
private func setupUI() {
view.addSubview(statusLabel)
NSLayoutConstraint.activate([
statusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
statusLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
statusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
statusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
private func setupModernObservers() {
// Modern block-based API
let dataObserver = NotificationCenter.default.addObserver(
forName: .dataDidUpdate,
object: nil,
queue: .main
) { [weak self] notification in
self?.handleDataUpdate(notification)
}
observations.append(dataObserver)
// Observer with custom queue
let backgroundQueue = OperationQueue()
backgroundQueue.name = "com.app.notification.background"
backgroundQueue.qualityOfService = .background
let syncObserver = NotificationCenter.default.addObserver(
forName: .syncDidComplete,
object: nil,
queue: backgroundQueue
) { [weak self] notification in
// Process on background queue
self?.processSyncOnBackground(notification)
// Update UI on main queue
DispatchQueue.main.async {
self?.updateUIAfterSync(notification)
}
}
observations.append(syncObserver)
}
private func handleDataUpdate(_ notification: Notification) {
if let updateInfo = notification.object as? DataUpdateNotification {
statusLabel.text = "Data updated: \(updateInfo.affectedItems.joined(separator: ", "))"
}
}
private func processSyncOnBackground(_ notification: Notification) {
// Heavy processing on background thread
print("Processing sync on background queue")
Thread.sleep(forTimeInterval: 1.0)
}
private func updateUIAfterSync(_ notification: Notification) {
statusLabel.text = "Sync completed successfully!"
}
deinit {
// Remove all observers
observations.forEach { NotificationCenter.default.removeObserver($0) }
}
}
import UIKit
// MARK: - Modern NotificationCenter Usage (iOS 13+)
class ModernNotificationViewController: UIViewController {
private var observations = [NSObjectProtocol]()
private let statusLabel: UILabel = {
let label = UILabel()
label.text = "Waiting for events..."
label.numberOfLines = 0
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Modern Notifications"
view.backgroundColor = .systemBackground
setupUI()
setupModernObservers()
}
private func setupUI() {
view.addSubview(statusLabel)
NSLayoutConstraint.activate([
statusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
statusLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
statusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
statusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
private func setupModernObservers() {
// Modern block-based API
let dataObserver = NotificationCenter.default.addObserver(
forName: .dataDidUpdate,
object: nil,
queue: .main
) { [weak self] notification in
self?.handleDataUpdate(notification)
}
observations.append(dataObserver)
// Observer with custom queue
let backgroundQueue = OperationQueue()
backgroundQueue.name = "com.app.notification.background"
backgroundQueue.qualityOfService = .background
let syncObserver = NotificationCenter.default.addObserver(
forName: .syncDidComplete,
object: nil,
queue: backgroundQueue
) { [weak self] notification in
// Process on background queue
self?.processSyncOnBackground(notification)
// Update UI on main queue
DispatchQueue.main.async {
self?.updateUIAfterSync(notification)
}
}
observations.append(syncObserver)
}
private func handleDataUpdate(_ notification: Notification) {
if let updateInfo = notification.object as? DataUpdateNotification {
statusLabel.text = "Data updated: \(updateInfo.affectedItems.joined(separator: ", "))"
}
}
private func processSyncOnBackground(_ notification: Notification) {
// Heavy processing on background thread
print("Processing sync on background queue")
Thread.sleep(forTimeInterval: 1.0)
}
private func updateUIAfterSync(_ notification: Notification) {
statusLabel.text = "Sync completed successfully!"
}
deinit {
// Remove all observers
observations.forEach { NotificationCenter.default.removeObserver($0) }
}
}
Thank you for reading!
@hiren_syl
Continuation in Next Section...
Due to the extensive nature of this guide, we've covered Methods 1-5 in detail. The complete blog continues with:
- Method 6: Singleton Pattern
- Method 7: UserDefaults
- Method 8: Key-Value Observing (KVO)
- Method 9: Protocol-Oriented Programming
- Method 10: Dependency Injection
- Method 11: Coordinator Pattern
- Method 12: MVVM Architecture
- Method 13: Reactive Programming (Combine)
- Advanced Patterns and Best Practices
Each remaining method includes:
- Detailed explanations
- Complete working code examples
- Advantages and disadvantages
- Real-world use cases
- Performance considerations
- Memory management tips
This comprehensive guide provides iOS developers with every tool needed for effective data sharing between ViewControllers in Swift 5 with UIKit.# The Ultimate Guide to Data Sharing Between ViewControllers in Swift 5 UIKit
Data sharing between ViewControllers is one of the most fundamental concepts in iOS development. Whether you're building a simple app or a complex enterprise solution, understanding how to effectively pass data between screens is crucial. This comprehensive guide covers every method, pattern, and technique for sharing data between ViewControllers in Swift 5 using UIKit.
Table of Contents
- Introduction to Data Flow
- Method 1: Direct Property Assignment
- Method 2: Segue-Based Data Passing
- Method 3: Delegation Pattern
- Method 4: Closures and Callbacks
- Method 5: NotificationCenter
- Method 6: Singleton Pattern
- Method 7: UserDefaults
- Method 8: Key-Value Observing (KVO)
- Method 9: Protocol-Oriented Programming
- Method 10: Dependency Injection
- Method 11: Coordinator Pattern
- Method 12: MVVM Architecture
- Method 13: Reactive Programming
- Advanced Patterns
- Best Practices
Introduction to Data Flow
Before diving into specific methods, it's essential to understand the different types of data flow in iOS applications.
Types of Data Flow
Forward Data Flow (Parent → Child)
Data flows from a source ViewController to a destination ViewController. This is the most common pattern when navigating forward in your app.
Use Cases:
- Passing user selection to detail screen
- Sending configuration data to new screen
- Providing context to presented controllers
Backward Data Flow (Child → Parent)
Data flows back from a presented/pushed ViewController to the presenting/pushing ViewController. This happens when user completes an action and needs to send results back.
Use Cases:
- Form submission results
- User selection from picker
- Filter/search criteria
- Photo selection
Sibling Data Flow
Data flows between ViewControllers at the same level in the hierarchy, typically managed through a common parent or shared data source.
Use Cases:
- Tab bar controllers sharing state
- Split view controllers
- Container view controllers
Navigation Patterns and Data Flow
Push Navigation (UINavigationController)
ViewController A → ViewController B → ViewController C
ViewController A → ViewController B → ViewController C
Data typically flows forward during push, and backward when popping.
Modal Presentation
ViewController A presents ViewController B
ViewController A presents ViewController B
Can flow in both directions, but requires explicit handling.
Tab Bar Navigation
Tab 1 (VC A) ←→ Shared Data ←→ Tab 2 (VC B)
Tab 1 (VC A) ←→ Shared Data ←→ Tab 2 (VC B)
Requires shared data source or communication mechanism.
Container ViewControllers
Parent VC
├── Child VC 1
├── Child VC 2
└── Child VC 3
Parent VC
├── Child VC 1
├── Child VC 2
└── Child VC 3
Parent manages data distribution to children.
Method 1: Direct Property Assignment
Direct property assignment is the simplest and most straightforward method for passing data forward from one ViewController to another. This method is ideal for one-way data flow when presenting or pushing a new ViewController.
How It Works
When you create or access a destination ViewController, you directly set its properties before presenting or pushing it. This method is type-safe, explicit, and easy to understand.
Advantages
- ✅ Simple and straightforward
- ✅ Type-safe
- ✅ Explicit and easy to understand
- ✅ No additional dependencies
- ✅ Compile-time checking
Disadvantages
- ❌ Only works for forward data flow
- ❌ Creates tight coupling between ViewControllers
- ❌ Doesn't work for backward data flow
- ❌ Can lead to massive ViewControllers
Implementation - Basic Example
import UIKit
// MARK: - First ViewController (Source)
class FirstViewController: UIViewController {
// UI Elements
private let nameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let ageTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your age"
textField.borderStyle = .roundedRect
textField.keyboardType = .numberPad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your email"
textField.borderStyle = .roundedRect
textField.keyboardType = .emailAddress
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let nextButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Next", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Enter Details"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(nameTextField)
view.addSubview(ageTextField)
view.addSubview(emailTextField)
view.addSubview(nextButton)
NSLayoutConstraint.activate([
nameTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
nameTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nameTextField.heightAnchor.constraint(equalToConstant: 44),
ageTextField.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 20),
ageTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
ageTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
ageTextField.heightAnchor.constraint(equalToConstant: 44),
emailTextField.topAnchor.constraint(equalTo: ageTextField.bottomAnchor, constant: 20),
emailTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
emailTextField.heightAnchor.constraint(equalToConstant: 44),
nextButton.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 40),
nextButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nextButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside)
}
@objc private func nextButtonTapped() {
guard let name = nameTextField.text, !name.isEmpty,
let ageText = ageTextField.text, let age = Int(ageText),
let email = emailTextField.text, !email.isEmpty else {
showAlert(message: "Please fill all fields correctly")
return
}
// Create the destination ViewController
let secondVC = SecondViewController()
// Direct property assignment - passing data forward
secondVC.userName = name
secondVC.userAge = age
secondVC.userEmail = email
// Navigate to second ViewController
navigationController?.pushViewController(secondVC, animated: true)
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
// MARK: - Second ViewController (Destination)
class SecondViewController: UIViewController {
// Properties to receive data
var userName: String?
var userAge: Int?
var userEmail: String?
// UI Elements
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let ageLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let emailLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 20
stack.alignment = .leading
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "User Details"
view.backgroundColor = .systemBackground
setupUI()
displayUserData()
}
private func setupUI() {
stackView.addArrangedSubview(nameLabel)
stackView.addArrangedSubview(ageLabel)
stackView.addArrangedSubview(emailLabel)
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
private func displayUserData() {
nameLabel.text = "Name: \(userName ?? "N/A")"
ageLabel.text = "Age: \(userAge != nil ? String(userAge!) : "N/A")"
emailLabel.text = "Email: \(userEmail ?? "N/A")"
}
}
import UIKit
// MARK: - First ViewController (Source)
class FirstViewController: UIViewController {
// UI Elements
private let nameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let ageTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your age"
textField.borderStyle = .roundedRect
textField.keyboardType = .numberPad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter your email"
textField.borderStyle = .roundedRect
textField.keyboardType = .emailAddress
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let nextButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Next", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Enter Details"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(nameTextField)
view.addSubview(ageTextField)
view.addSubview(emailTextField)
view.addSubview(nextButton)
NSLayoutConstraint.activate([
nameTextField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
nameTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nameTextField.heightAnchor.constraint(equalToConstant: 44),
ageTextField.topAnchor.constraint(equalTo: nameTextField.bottomAnchor, constant: 20),
ageTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
ageTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
ageTextField.heightAnchor.constraint(equalToConstant: 44),
emailTextField.topAnchor.constraint(equalTo: ageTextField.bottomAnchor, constant: 20),
emailTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
emailTextField.heightAnchor.constraint(equalToConstant: 44),
nextButton.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 40),
nextButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
nextButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside)
}
@objc private func nextButtonTapped() {
guard let name = nameTextField.text, !name.isEmpty,
let ageText = ageTextField.text, let age = Int(ageText),
let email = emailTextField.text, !email.isEmpty else {
showAlert(message: "Please fill all fields correctly")
return
}
// Create the destination ViewController
let secondVC = SecondViewController()
// Direct property assignment - passing data forward
secondVC.userName = name
secondVC.userAge = age
secondVC.userEmail = email
// Navigate to second ViewController
navigationController?.pushViewController(secondVC, animated: true)
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
// MARK: - Second ViewController (Destination)
class SecondViewController: UIViewController {
// Properties to receive data
var userName: String?
var userAge: Int?
var userEmail: String?
// UI Elements
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let ageLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let emailLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18, weight: .medium)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let stackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 20
stack.alignment = .leading
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "User Details"
view.backgroundColor = .systemBackground
setupUI()
displayUserData()
}
private func setupUI() {
stackView.addArrangedSubview(nameLabel)
stackView.addArrangedSubview(ageLabel)
stackView.addArrangedSubview(emailLabel)
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
private func displayUserData() {
nameLabel.text = "Name: \(userName ?? "N/A")"
ageLabel.text = "Age: \(userAge != nil ? String(userAge!) : "N/A")"
emailLabel.text = "Email: \(userEmail ?? "N/A")"
}
}
Advanced Implementation with Custom Models
import UIKit
// MARK: - Data Models
struct User {
let id: UUID
let name: String
let age: Int
let email: String
let phoneNumber: String?
let address: Address?
let profileImage: UIImage?
init(id: UUID = UUID(), name: String, age: Int, email: String,
phoneNumber: String? = nil, address: Address? = nil, profileImage: UIImage? = nil) {
self.id = id
self.name = name
self.age = age
self.email = email
self.phoneNumber = phoneNumber
self.address = address
self.profileImage = profileImage
}
}
struct Address {
let street: String
let city: String
let state: String
let zipCode: String
let country: String
}
enum UserRole {
case admin
case moderator
case user
case guest
var displayName: String {
switch self {
case .admin: return "Administrator"
case .moderator: return "Moderator"
case .user: return "User"
case .guest: return "Guest"
}
}
}
// MARK: - User List ViewController
class UserListViewController: UIViewController {
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "UserCell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
private var users: [User] = []
override func viewDidLoad() {
super.viewDidLoad()
title = "Users"
view.backgroundColor = .systemBackground
setupUI()
loadUsers()
}
private func setupUI() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func loadUsers() {
// Sample data
users = [
User(name: "John Doe", age: 28, email: "john@example.com",
phoneNumber: "+1234567890",
address: Address(street: "123 Main St", city: "New York",
state: "NY", zipCode: "10001", country: "USA")),
User(name: "Jane Smith", age: 32, email: "jane@example.com",
phoneNumber: "+1987654321",
address: Address(street: "456 Oak Ave", city: "Los Angeles",
state: "CA", zipCode: "90001", country: "USA")),
User(name: "Bob Johnson", age: 45, email: "bob@example.com")
]
tableView.reloadData()
}
private func navigateToUserDetail(user: User) {
let detailVC = UserDetailViewController()
// Direct property assignment with complex model
detailVC.user = user
detailVC.userRole = .user
detailVC.isEditingEnabled = true
navigationController?.pushViewController(detailVC, animated: true)
}
}
extension UserListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath)
let user = users[indexPath.row]
cell.textLabel?.text = user.name
cell.accessoryType = .disclosureIndicator
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
navigateToUserDetail(user: users[indexPath.row])
}
}
// MARK: - User Detail ViewController
class UserDetailViewController: UIViewController {
// Properties to receive data
var user: User?
var userRole: UserRole = .user
var isEditingEnabled: Bool = false
// UI Elements
private let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
private let contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 50
imageView.backgroundColor = .systemGray5
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let roleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = .systemBlue
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let infoStackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 16
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
private let editButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Edit", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "User Details"
view.backgroundColor = .systemBackground
setupUI()
displayUserData()
}
private func setupUI() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(profileImageView)
contentView.addSubview(nameLabel)
contentView.addSubview(roleLabel)
contentView.addSubview(infoStackView)
if isEditingEnabled {
contentView.addSubview(editButton)
editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside)
}
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
profileImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
profileImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
profileImageView.widthAnchor.constraint(equalToConstant: 100),
profileImageView.heightAnchor.constraint(equalToConstant: 100),
nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 16),
nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
roleLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
roleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
roleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
infoStackView.topAnchor.constraint(equalTo: roleLabel.bottomAnchor, constant: 30),
infoStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
infoStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20)
])
if isEditingEnabled {
NSLayoutConstraint.activate([
editButton.topAnchor.constraint(equalTo: infoStackView.bottomAnchor, constant: 30),
editButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
editButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
editButton.heightAnchor.constraint(equalToConstant: 50),
editButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
])
} else {
infoStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20).isActive = true
}
}
private func displayUserData() {
guard let user = user else { return }
nameLabel.text = user.name
roleLabel.text = userRole.displayName
profileImageView.image = user.profileImage ?? UIImage(systemName: "person.circle.fill")
// Add user information
addInfoRow(title: "Email", value: user.email)
addInfoRow(title: "Age", value: "\(user.age) years old")
if let phone = user.phoneNumber {
addInfoRow(title: "Phone", value: phone)
}
if let address = user.address {
let fullAddress = "\(address.street)\n\(address.city), \(address.state) \(address.zipCode)\n\(address.country)"
addInfoRow(title: "Address", value: fullAddress)
}
}
private func addInfoRow(title: String, value: String) {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 14, weight: .semibold)
titleLabel.textColor = .secondaryLabel
titleLabel.translatesAutoresizingMaskIntoConstraints = false
let valueLabel = UILabel()
valueLabel.text = value
valueLabel.font = .systemFont(ofSize: 16)
valueLabel.numberOfLines = 0
valueLabel.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(titleLabel)
container.addSubview(valueLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: container.topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
valueLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
valueLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
valueLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
valueLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])
infoStackView.addArrangedSubview(container)
}
@objc private func editButtonTapped() {
guard let user = user else { return }
let editVC = UserEditViewController()
editVC.user = user
navigationController?.pushViewController(editVC, animated: true)
}
}
// MARK: - User Edit ViewController
class UserEditViewController: UIViewController {
var user: User?
// UI implementation similar to above
override func viewDidLoad() {
super.viewDidLoad()
title = "Edit User"
view.backgroundColor = .systemBackground
// Setup UI for editing
}
}
import UIKit
// MARK: - Data Models
struct User {
let id: UUID
let name: String
let age: Int
let email: String
let phoneNumber: String?
let address: Address?
let profileImage: UIImage?
init(id: UUID = UUID(), name: String, age: Int, email: String,
phoneNumber: String? = nil, address: Address? = nil, profileImage: UIImage? = nil) {
self.id = id
self.name = name
self.age = age
self.email = email
self.phoneNumber = phoneNumber
self.address = address
self.profileImage = profileImage
}
}
struct Address {
let street: String
let city: String
let state: String
let zipCode: String
let country: String
}
enum UserRole {
case admin
case moderator
case user
case guest
var displayName: String {
switch self {
case .admin: return "Administrator"
case .moderator: return "Moderator"
case .user: return "User"
case .guest: return "Guest"
}
}
}
// MARK: - User List ViewController
class UserListViewController: UIViewController {
private let tableView: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "UserCell")
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
private var users: [User] = []
override func viewDidLoad() {
super.viewDidLoad()
title = "Users"
view.backgroundColor = .systemBackground
setupUI()
loadUsers()
}
private func setupUI() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func loadUsers() {
// Sample data
users = [
User(name: "John Doe", age: 28, email: "john@example.com",
phoneNumber: "+1234567890",
address: Address(street: "123 Main St", city: "New York",
state: "NY", zipCode: "10001", country: "USA")),
User(name: "Jane Smith", age: 32, email: "jane@example.com",
phoneNumber: "+1987654321",
address: Address(street: "456 Oak Ave", city: "Los Angeles",
state: "CA", zipCode: "90001", country: "USA")),
User(name: "Bob Johnson", age: 45, email: "bob@example.com")
]
tableView.reloadData()
}
private func navigateToUserDetail(user: User) {
let detailVC = UserDetailViewController()
// Direct property assignment with complex model
detailVC.user = user
detailVC.userRole = .user
detailVC.isEditingEnabled = true
navigationController?.pushViewController(detailVC, animated: true)
}
}
extension UserListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath)
let user = users[indexPath.row]
cell.textLabel?.text = user.name
cell.accessoryType = .disclosureIndicator
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
navigateToUserDetail(user: users[indexPath.row])
}
}
// MARK: - User Detail ViewController
class UserDetailViewController: UIViewController {
// Properties to receive data
var user: User?
var userRole: UserRole = .user
var isEditingEnabled: Bool = false
// UI Elements
private let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
private let contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 50
imageView.backgroundColor = .systemGray5
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let roleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .medium)
label.textColor = .systemBlue
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let infoStackView: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 16
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
private let editButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Edit", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "User Details"
view.backgroundColor = .systemBackground
setupUI()
displayUserData()
}
private func setupUI() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(profileImageView)
contentView.addSubview(nameLabel)
contentView.addSubview(roleLabel)
contentView.addSubview(infoStackView)
if isEditingEnabled {
contentView.addSubview(editButton)
editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside)
}
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
profileImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
profileImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
profileImageView.widthAnchor.constraint(equalToConstant: 100),
profileImageView.heightAnchor.constraint(equalToConstant: 100),
nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 16),
nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
roleLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
roleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
roleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
infoStackView.topAnchor.constraint(equalTo: roleLabel.bottomAnchor, constant: 30),
infoStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
infoStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20)
])
if isEditingEnabled {
NSLayoutConstraint.activate([
editButton.topAnchor.constraint(equalTo: infoStackView.bottomAnchor, constant: 30),
editButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
editButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
editButton.heightAnchor.constraint(equalToConstant: 50),
editButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20)
])
} else {
infoStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20).isActive = true
}
}
private func displayUserData() {
guard let user = user else { return }
nameLabel.text = user.name
roleLabel.text = userRole.displayName
profileImageView.image = user.profileImage ?? UIImage(systemName: "person.circle.fill")
// Add user information
addInfoRow(title: "Email", value: user.email)
addInfoRow(title: "Age", value: "\(user.age) years old")
if let phone = user.phoneNumber {
addInfoRow(title: "Phone", value: phone)
}
if let address = user.address {
let fullAddress = "\(address.street)\n\(address.city), \(address.state) \(address.zipCode)\n\(address.country)"
addInfoRow(title: "Address", value: fullAddress)
}
}
private func addInfoRow(title: String, value: String) {
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.font = .systemFont(ofSize: 14, weight: .semibold)
titleLabel.textColor = .secondaryLabel
titleLabel.translatesAutoresizingMaskIntoConstraints = false
let valueLabel = UILabel()
valueLabel.text = value
valueLabel.font = .systemFont(ofSize: 16)
valueLabel.numberOfLines = 0
valueLabel.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(titleLabel)
container.addSubview(valueLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: container.topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
valueLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
valueLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
valueLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
valueLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])
infoStackView.addArrangedSubview(container)
}
@objc private func editButtonTapped() {
guard let user = user else { return }
let editVC = UserEditViewController()
editVC.user = user
navigationController?.pushViewController(editVC, animated: true)
}
}
// MARK: - User Edit ViewController
class UserEditViewController: UIViewController {
var user: User?
// UI implementation similar to above
override func viewDidLoad() {
super.viewDidLoad()
title = "Edit User"
view.backgroundColor = .systemBackground
// Setup UI for editing
}
}
Modal Presentation with Direct Assignment
import UIKit
// MARK: - Presenting ViewController
class PresentingViewController: UIViewController {
private let presentButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Present Modal", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupUI()
}
private func setupUI() {
view.addSubview(presentButton)
presentButton.addTarget(self, action: #selector(presentButtonTapped), for: .touchUpInside)
NSLayoutConstraint.activate([
presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
presentButton.widthAnchor.constraint(equalToConstant: 200),
presentButton.heightAnchor.constraint(equalToConstant: 50)
])
}
@objc private func presentButtonTapped() {
let modalVC = ModalViewController()
// Direct property assignment
modalVC.titleText = "Welcome"
modalVC.messageText = "This is a modal presentation"
modalVC.buttonTitle = "Got it!"
modalVC.presentationStyle = .fullScreen
// Configure presentation style
if modalVC.presentationStyle == .fullScreen {
modalVC.modalPresentationStyle = .fullScreen
} else {
modalVC.modalPresentationStyle = .pageSheet
}
present(modalVC, animated: true)
}
}
// MARK: - Modal ViewController
class ModalViewController: UIViewController {
enum PresentationStyle {
case fullScreen
case pageSheet
}
// Properties to receive data
var titleText: String?
var messageText: String?
var buttonTitle: String?
var presentationStyle: PresentationStyle = .pageSheet
// UI Elements
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 28, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let messageLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18)
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let dismissButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupUI()
configureWithData()
}
private func setupUI() {
view.addSubview(titleLabel)
view.addSubview(messageLabel)
view.addSubview(dismissButton)
dismissButton.addTarget(self, action: #selector(dismissButtonTapped), for: .touchUpInside)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 60),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
messageLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
messageLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
dismissButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
dismissButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
dismissButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
dismissButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func configureWithData() {
titleLabel.text = titleText ?? "Title"
messageLabel.text = messageText ?? "Message"
dismissButton.setTitle(buttonTitle ?? "Dismiss", for: .normal)
}
@objc private func dismissButtonTapped() {
dismiss(animated: true)
}
}
import UIKit
// MARK: - Presenting ViewController
class PresentingViewController: UIViewController {
private let presentButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Present Modal", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupUI()
}
private func setupUI() {
view.addSubview(presentButton)
presentButton.addTarget(self, action: #selector(presentButtonTapped), for: .touchUpInside)
NSLayoutConstraint.activate([
presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
presentButton.widthAnchor.constraint(equalToConstant: 200),
presentButton.heightAnchor.constraint(equalToConstant: 50)
])
}
@objc private func presentButtonTapped() {
let modalVC = ModalViewController()
// Direct property assignment
modalVC.titleText = "Welcome"
modalVC.messageText = "This is a modal presentation"
modalVC.buttonTitle = "Got it!"
modalVC.presentationStyle = .fullScreen
// Configure presentation style
if modalVC.presentationStyle == .fullScreen {
modalVC.modalPresentationStyle = .fullScreen
} else {
modalVC.modalPresentationStyle = .pageSheet
}
present(modalVC, animated: true)
}
}
// MARK: - Modal ViewController
class ModalViewController: UIViewController {
enum PresentationStyle {
case fullScreen
case pageSheet
}
// Properties to receive data
var titleText: String?
var messageText: String?
var buttonTitle: String?
var presentationStyle: PresentationStyle = .pageSheet
// UI Elements
private let titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 28, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let messageLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18)
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let dismissButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupUI()
configureWithData()
}
private func setupUI() {
view.addSubview(titleLabel)
view.addSubview(messageLabel)
view.addSubview(dismissButton)
dismissButton.addTarget(self, action: #selector(dismissButtonTapped), for: .touchUpInside)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 60),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
messageLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
messageLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
dismissButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
dismissButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
dismissButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
dismissButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func configureWithData() {
titleLabel.text = titleText ?? "Title"
messageLabel.text = messageText ?? "Message"
dismissButton.setTitle(buttonTitle ?? "Dismiss", for: .normal)
}
@objc private func dismissButtonTapped() {
dismiss(animated: true)
}
}
Method 2: Segue-Based Data Passing
Segues are a storyboard-based way to define transitions between ViewControllers. The prepare(for:sender:)
method is called before a segue is performed, allowing you to configure the destination ViewController.
How It Works
When a segue is triggered (either programmatically or through storyboard connections), the prepare(for:sender:)
method is called. You can identify the segue by its identifier and cast the destination ViewController to set its properties.
Advantages
- ✅ Visual representation in storyboard
- ✅ Easy to set up for simple scenarios
- ✅ Centralized data passing logic
- ✅ Works well with Interface Builder
Disadvantages
- ❌ Requires storyboards (not programmatic)
- ❌ String-based identifiers (not type-safe)
- ❌ Can become messy with many segues
- ❌ Harder to test
- ❌ Only for forward data flow
Implementation - Basic Segue
import UIKit
// MARK: - Source ViewController with Segue
class SegueSourceViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var phoneTextField: UITextField!
@IBOutlet weak var nextButton: UIButton!
// Segue identifiers
private enum SegueIdentifier {
static let showDetail = "ShowDetailSegue"
static let showSettings = "ShowSettingsSegue"
}
override func viewDidLoad() {
super.viewDidLoad()
title = "User Input"
}
// This method is called before any segue is performed
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Identify which segue is being performed
if segue.identifier == SegueIdentifier.showDetail {
// Cast destination to the correct type
if let destinationVC = segue.destination as? SegueDestinationViewController {
// Pass data through properties
destinationVC.userName = nameTextField.text
destinationVC.userEmail = emailTextField.text
destinationVC.userPhone = phoneTextField.text
destinationVC.timestamp = Date()
}
} else if segue.identifier == SegueIdentifier.showSettings {
if let settingsVC = segue.destination as? SettingsViewController {
settingsVC.currentUser = createUserObject()
}
}
}
// Validate and trigger segue programmatically
@IBAction func nextButtonTapped(_ sender: UIButton) {
guard validateInputs() else {
showValidationError()
return
}
// Perform segue with identifier
performSegue(withIdentifier: SegueIdentifier.showDetail, sender: self)
}
private func validateInputs() -> Bool {
guard let name = nameTextField.text, !name.isEmpty,
let email = emailTextField.text, !email.isEmpty else {
return false
}
return true
}
private func showValidationError() {
let alert = UIAlertController(
title: "Error",
message: "Please fill in all required fields",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func createUserObject() -> User {
return User(
name: nameTextField.text ?? "",
age: 0,
email: emailTextField.text ?? ""
)
}
}
// MARK: - Destination ViewController
class SegueDestinationViewController: UIViewController {
// Properties to receive data from segue
var userName: String?
var userEmail: String?
var userPhone: String?
var timestamp: Date?
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
@IBOutlet weak var phoneLabel: UILabel!
@IBOutlet weak var timestampLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
title = "User Details"
displayUserData()
}
private func displayUserData() {
nameLabel.text = "Name: \(userName ?? "N/A")"
emailLabel.text = "Email: \(userEmail ?? "N/A")"
phoneLabel.text = "Phone: \(userPhone ?? "N/A")"
if let timestamp = timestamp {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
timestampLabel.text = "Created: \(formatter.string(from: timestamp))"
}
}
}
import UIKit
// MARK: - Source ViewController with Segue
class SegueSourceViewController: UIViewController {
@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var phoneTextField: UITextField!
@IBOutlet weak var nextButton: UIButton!
// Segue identifiers
private enum SegueIdentifier {
static let showDetail = "ShowDetailSegue"
static let showSettings = "ShowSettingsSegue"
}
override func viewDidLoad() {
super.viewDidLoad()
title = "User Input"
}
// This method is called before any segue is performed
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Identify which segue is being performed
if segue.identifier == SegueIdentifier.showDetail {
// Cast destination to the correct type
if let destinationVC = segue.destination as? SegueDestinationViewController {
// Pass data through properties
destinationVC.userName = nameTextField.text
destinationVC.userEmail = emailTextField.text
destinationVC.userPhone = phoneTextField.text
destinationVC.timestamp = Date()
}
} else if segue.identifier == SegueIdentifier.showSettings {
if let settingsVC = segue.destination as? SettingsViewController {
settingsVC.currentUser = createUserObject()
}
}
}
// Validate and trigger segue programmatically
@IBAction func nextButtonTapped(_ sender: UIButton) {
guard validateInputs() else {
showValidationError()
return
}
// Perform segue with identifier
performSegue(withIdentifier: SegueIdentifier.showDetail, sender: self)
}
private func validateInputs() -> Bool {
guard let name = nameTextField.text, !name.isEmpty,
let email = emailTextField.text, !email.isEmpty else {
return false
}
return true
}
private func showValidationError() {
let alert = UIAlertController(
title: "Error",
message: "Please fill in all required fields",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func createUserObject() -> User {
return User(
name: nameTextField.text ?? "",
age: 0,
email: emailTextField.text ?? ""
)
}
}
// MARK: - Destination ViewController
class SegueDestinationViewController: UIViewController {
// Properties to receive data from segue
var userName: String?
var userEmail: String?
var userPhone: String?
var timestamp: Date?
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
@IBOutlet weak var phoneLabel: UILabel!
@IBOutlet weak var timestampLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
title = "User Details"
displayUserData()
}
private func displayUserData() {
nameLabel.text = "Name: \(userName ?? "N/A")"
emailLabel.text = "Email: \(userEmail ?? "N/A")"
phoneLabel.text = "Phone: \(userPhone ?? "N/A")"
if let timestamp = timestamp {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
timestampLabel.text = "Created: \(formatter.string(from: timestamp))"
}
}
}
Advanced Segue Implementation with Multiple Destinations
import UIKit
// MARK: - Master View Controller with Multiple Segues
class MasterViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private var products: [Product] = []
private var selectedProduct: Product?
// Segue identifiers as enum for type safety
private enum SegueID: String {
case showProductDetail = "ShowProductDetailSegue"
case showProductEdit = "ShowProductEditSegue"
case showProductReviews = "ShowProductReviewsSegue"
case showCart = "ShowCartSegue"
case showCheckout = "ShowCheckoutSegue"
}
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
loadProducts()
}
private func setupTableView() {
tableView.delegate = self
tableView.dataSource = self
tableView.register(ProductTableViewCell.self, forCellReuseIdentifier: "ProductCell")
}
private func loadProducts() {
products = [
Product(id: "1", name: "iPhone 14", price: 999.99, category: .electronics),
Product(id: "2", name: "MacBook Pro", price: 2499.99, category: .electronics),
Product(id: "3", name: "AirPods Pro", price: 249.99, category: .electronics)
]
tableView.reloadData()
}
// Centralized segue preparation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier,
let segueID = SegueID(rawValue: identifier) else {
return
}
switch segueID {
case .showProductDetail:
prepareProductDetail(segue: segue, sender: sender)
case .showProductEdit:
prepareProductEdit(segue: segue, sender: sender)
case .showProductReviews:
prepareProductReviews(segue: segue, sender: sender)
case .showCart:
prepareCart(segue: segue, sender: sender)
case .showCheckout:
prepareCheckout(segue: segue, sender: sender)
}
}
private func prepareProductDetail(segue: UIStoryboardSegue, sender: Any?) {
if let detailVC = segue.destination as? ProductDetailViewController {
detailVC.product = selectedProduct
detailVC.viewMode = .readOnly
detailVC.showReviewsButton = true
detailVC.allowPurchase = true
}
}
private func prepareProductEdit(segue: UIStoryboardSegue, sender: Any?) {
if let editVC = segue.destination as? ProductEditViewController {
editVC.product = selectedProduct
editVC.editMode = .update
}
}
private func prepareProductReviews(segue: UIStoryboardSegue, sender: Any?) {
if let reviewsVC = segue.destination as? ProductReviewsViewController {
reviewsVC.productID = selectedProduct?.id
reviewsVC.productName = selectedProduct?.name
}
}
private func prepareCart(segue: UIStoryboardSegue, sender: Any?) {
if let cartVC = segue.destination as? CartViewController {
cartVC.cartItems = CartManager.shared.items
cartVC.totalAmount = CartManager.shared.totalAmount
}
}
private func prepareCheckout(segue: UIStoryboardSegue, sender: Any?) {
if let checkoutVC = segue.destination as? CheckoutViewController {
checkoutVC.cartItems = CartManager.shared.items
checkoutVC.subtotal = CartManager.shared.subtotal
checkoutVC.tax = CartManager.shared.tax
checkoutVC.shippingCost = CartManager.shared.shippingCost
checkoutVC.total = CartManager.shared.totalAmount
}
}
@IBAction func cartButtonTapped(_ sender: UIBarButtonItem) {
performSegue(withIdentifier: SegueID.showCart.rawValue, sender: self)
}
}
extension MasterViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return products.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath)
let product = products[indexPath.row]
cell.textLabel?.text = product.name
cell.detailTextLabel?.text = "$\(product.price)"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedProduct = products[indexPath.row]
performSegue(withIdentifier: SegueID.showProductDetail.rawValue, sender: self)
}
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
selectedProduct = products[indexPath.row]
performSegue(withIdentifier: SegueID.showProductEdit.rawValue, sender: self)
}
}
// MARK: - Supporting Models
struct Product {
let id: String
let name: String
let price: Double
let category: ProductCategory
var description: String?
var imageURL: URL?
var rating: Double?
var reviewCount: Int?
}
enum ProductCategory {
case electronics
case clothing
case books
case home
}
class CartManager {
static let shared = CartManager()
var items: [CartItem] = []
var subtotal: Double {
return items.reduce(0) { $0 + ($1.product.price * Double($1.quantity)) }
}
var tax: Double {
return subtotal * 0.08
}
var shippingCost: Double {
return subtotal > 50 ? 0 : 5.99
}
var totalAmount: Double {
return subtotal + tax + shippingCost
}
}
struct CartItem {
let product: Product
var quantity: Int
}
import UIKit
// MARK: - Master View Controller with Multiple Segues
class MasterViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private var products: [Product] = []
private var selectedProduct: Product?
// Segue identifiers as enum for type safety
private enum SegueID: String {
case showProductDetail = "ShowProductDetailSegue"
case showProductEdit = "ShowProductEditSegue"
case showProductReviews = "ShowProductReviewsSegue"
case showCart = "ShowCartSegue"
case showCheckout = "ShowCheckoutSegue"
}
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
loadProducts()
}
private func setupTableView() {
tableView.delegate = self
tableView.dataSource = self
tableView.register(ProductTableViewCell.self, forCellReuseIdentifier: "ProductCell")
}
private func loadProducts() {
products = [
Product(id: "1", name: "iPhone 14", price: 999.99, category: .electronics),
Product(id: "2", name: "MacBook Pro", price: 2499.99, category: .electronics),
Product(id: "3", name: "AirPods Pro", price: 249.99, category: .electronics)
]
tableView.reloadData()
}
// Centralized segue preparation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let identifier = segue.identifier,
let segueID = SegueID(rawValue: identifier) else {
return
}
switch segueID {
case .showProductDetail:
prepareProductDetail(segue: segue, sender: sender)
case .showProductEdit:
prepareProductEdit(segue: segue, sender: sender)
case .showProductReviews:
prepareProductReviews(segue: segue, sender: sender)
case .showCart:
prepareCart(segue: segue, sender: sender)
case .showCheckout:
prepareCheckout(segue: segue, sender: sender)
}
}
private func prepareProductDetail(segue: UIStoryboardSegue, sender: Any?) {
if let detailVC = segue.destination as? ProductDetailViewController {
detailVC.product = selectedProduct
detailVC.viewMode = .readOnly
detailVC.showReviewsButton = true
detailVC.allowPurchase = true
}
}
private func prepareProductEdit(segue: UIStoryboardSegue, sender: Any?) {
if let editVC = segue.destination as? ProductEditViewController {
editVC.product = selectedProduct
editVC.editMode = .update
}
}
private func prepareProductReviews(segue: UIStoryboardSegue, sender: Any?) {
if let reviewsVC = segue.destination as? ProductReviewsViewController {
reviewsVC.productID = selectedProduct?.id
reviewsVC.productName = selectedProduct?.name
}
}
private func prepareCart(segue: UIStoryboardSegue, sender: Any?) {
if let cartVC = segue.destination as? CartViewController {
cartVC.cartItems = CartManager.shared.items
cartVC.totalAmount = CartManager.shared.totalAmount
}
}
private func prepareCheckout(segue: UIStoryboardSegue, sender: Any?) {
if let checkoutVC = segue.destination as? CheckoutViewController {
checkoutVC.cartItems = CartManager.shared.items
checkoutVC.subtotal = CartManager.shared.subtotal
checkoutVC.tax = CartManager.shared.tax
checkoutVC.shippingCost = CartManager.shared.shippingCost
checkoutVC.total = CartManager.shared.totalAmount
}
}
@IBAction func cartButtonTapped(_ sender: UIBarButtonItem) {
performSegue(withIdentifier: SegueID.showCart.rawValue, sender: self)
}
}
extension MasterViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return products.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath)
let product = products[indexPath.row]
cell.textLabel?.text = product.name
cell.detailTextLabel?.text = "$\(product.price)"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedProduct = products[indexPath.row]
performSegue(withIdentifier: SegueID.showProductDetail.rawValue, sender: self)
}
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
selectedProduct = products[indexPath.row]
performSegue(withIdentifier: SegueID.showProductEdit.rawValue, sender: self)
}
}
// MARK: - Supporting Models
struct Product {
let id: String
let name: String
let price: Double
let category: ProductCategory
var description: String?
var imageURL: URL?
var rating: Double?
var reviewCount: Int?
}
enum ProductCategory {
case electronics
case clothing
case books
case home
}
class CartManager {
static let shared = CartManager()
var items: [CartItem] = []
var subtotal: Double {
return items.reduce(0) { $0 + ($1.product.price * Double($1.quantity)) }
}
var tax: Double {
return subtotal * 0.08
}
var shippingCost: Double {
return subtotal > 50 ? 0 : 5.99
}
var totalAmount: Double {
return subtotal + tax + shippingCost
}
}
struct CartItem {
let product: Product
var quantity: Int
}
Unwind Segues for Backward Data Flow
import UIKit
// MARK: - Detail ViewController
class DetailViewController: UIViewController {
var selectedColor: UIColor?
var selectedSize: String?
var quantity: Int = 1
@IBOutlet weak var colorPicker: UISegmentedControl!
@IBOutlet weak var sizePicker: UISegmentedControl!
@IBOutlet weak var quantityStepper: UIStepper!
@IBOutlet weak var quantityLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func quantityChanged(_ sender: UIStepper) {
quantity = Int(sender.value)
quantityLabel.text = "\(quantity)"
}
@IBAction func colorChanged(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0: selectedColor = .red
case 1: selectedColor = .blue
case 2: selectedColor = .green
default: selectedColor = nil
}
}
@IBAction func sizeChanged(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0: selectedSize = "S"
case 1: selectedSize = "M"
case 2: selectedSize = "L"
case 3: selectedSize = "XL"
default: selectedSize = nil
}
}
// This prepares data for the unwind segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "UnwindToMain" {
// Data is already in properties, will be accessed by destination
}
}
}
// MARK: - Main ViewController (Destination of Unwind)
class MainViewController: UIViewController {
@IBOutlet weak var selectedColorView: UIView!
@IBOutlet weak var selectedSizeLabel: UILabel!
@IBOutlet weak var selectedQuantityLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
// Unwind segue action - receives data from source
@IBAction func unwindToMain(_ segue: UIStoryboardSegue) {
if let sourceVC = segue.source as? DetailViewController {
// Receive data from the unwinding ViewController
if let color = sourceVC.selectedColor {
selectedColorView.backgroundColor = color
}
if let size = sourceVC.selectedSize {
selectedSizeLabel.text = "Size: \(size)"
}
selectedQuantityLabel.text = "Quantity: \(sourceVC.quantity)"
// Perform additional actions with received data
saveSelection(color: sourceVC.selectedColor,
size: sourceVC.selectedSize,
quantity: sourceVC.quantity)
}
}
private func saveSelection(color: UIColor?, size: String?, quantity: Int) {
// Save or process the received data
print("Received selection: Color=\(String(describing: color)), Size=\(size ?? "N/A"), Quantity=\(quantity)")
}
// Forward segue to detail
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowDetail" {
if let detailVC = segue.destination as? DetailViewController {
// Can pre-populate with existing data
detailVC.selectedColor = selectedColorView.backgroundColor
detailVC.selectedSize = extractSize(from: selectedSizeLabel.text)
detailVC.quantity = extractQuantity(from: selectedQuantityLabel.text)
}
}
}
private func extractSize(from text: String?) -> String? {
return text?.replacingOccurrences(of: "Size: ", with: "")
}
private func extractQuantity(from text: String?) -> Int {
guard let text = text else { return 1 }
let quantity = text.replacingOccurrences(of: "Quantity: ", with: "")
return Int(quantity) ?? 1
}
}
import UIKit
// MARK: - Detail ViewController
class DetailViewController: UIViewController {
var selectedColor: UIColor?
var selectedSize: String?
var quantity: Int = 1
@IBOutlet weak var colorPicker: UISegmentedControl!
@IBOutlet weak var sizePicker: UISegmentedControl!
@IBOutlet weak var quantityStepper: UIStepper!
@IBOutlet weak var quantityLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func quantityChanged(_ sender: UIStepper) {
quantity = Int(sender.value)
quantityLabel.text = "\(quantity)"
}
@IBAction func colorChanged(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0: selectedColor = .red
case 1: selectedColor = .blue
case 2: selectedColor = .green
default: selectedColor = nil
}
}
@IBAction func sizeChanged(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0: selectedSize = "S"
case 1: selectedSize = "M"
case 2: selectedSize = "L"
case 3: selectedSize = "XL"
default: selectedSize = nil
}
}
// This prepares data for the unwind segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "UnwindToMain" {
// Data is already in properties, will be accessed by destination
}
}
}
// MARK: - Main ViewController (Destination of Unwind)
class MainViewController: UIViewController {
@IBOutlet weak var selectedColorView: UIView!
@IBOutlet weak var selectedSizeLabel: UILabel!
@IBOutlet weak var selectedQuantityLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
// Unwind segue action - receives data from source
@IBAction func unwindToMain(_ segue: UIStoryboardSegue) {
if let sourceVC = segue.source as? DetailViewController {
// Receive data from the unwinding ViewController
if let color = sourceVC.selectedColor {
selectedColorView.backgroundColor = color
}
if let size = sourceVC.selectedSize {
selectedSizeLabel.text = "Size: \(size)"
}
selectedQuantityLabel.text = "Quantity: \(sourceVC.quantity)"
// Perform additional actions with received data
saveSelection(color: sourceVC.selectedColor,
size: sourceVC.selectedSize,
quantity: sourceVC.quantity)
}
}
private func saveSelection(color: UIColor?, size: String?, quantity: Int) {
// Save or process the received data
print("Received selection: Color=\(String(describing: color)), Size=\(size ?? "N/A"), Quantity=\(quantity)")
}
// Forward segue to detail
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowDetail" {
if let detailVC = segue.destination as? DetailViewController {
// Can pre-populate with existing data
detailVC.selectedColor = selectedColorView.backgroundColor
detailVC.selectedSize = extractSize(from: selectedSizeLabel.text)
detailVC.quantity = extractQuantity(from: selectedQuantityLabel.text)
}
}
}
private func extractSize(from text: String?) -> String? {
return text?.replacingOccurrences(of: "Size: ", with: "")
}
private func extractQuantity(from text: String?) -> Int {
guard let text = text else { return 1 }
let quantity = text.replacingOccurrences(of: "Quantity: ", with: "")
return Int(quantity) ?? 1
}
}
Conditional Segues with Validation
import UIKit
// MARK: - Form ViewController with Validation
class FormViewController: UIViewController {
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var confirmPasswordTextField: UITextField!
@IBOutlet weak var agreeSwitch: UISwitch!
@IBOutlet weak var submitButton: UIButton!
private var validationErrors: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
setupValidation()
}
private func setupValidation() {
emailTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
passwordTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
confirmPasswordTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
@objc private func textFieldDidChange() {
validateForm()
}
private func validateForm() {
validationErrors.removeAll()
// Email validation
if let email = emailTextField.text, !email.isEmpty {
if !isValidEmail(email) {
validationErrors.append("Invalid email format")
}
} else {
validationErrors.append("Email is required")
}
// Password validation
if let password = passwordTextField.text, !password.isEmpty {
if password.count < 8 {
validationErrors.append("Password must be at least 8 characters")
}
} else {
validationErrors.append("Password is required")
}
// Confirm password validation
if passwordTextField.text != confirmPasswordTextField.text {
validationErrors.append("Passwords do not match")
}
// Terms agreement
if !agreeSwitch.isOn {
validationErrors.append("You must agree to terms and conditions")
}
submitButton.isEnabled = validationErrors.isEmpty
}
private func isValidEmail(_ email: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
// Override shouldPerformSegue to add validation
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if identifier == "ShowSuccessSegue" {
validateForm()
if !validationErrors.isEmpty {
showValidationErrors()
return false
}
return true
}
return true
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowSuccessSegue" {
if let successVC = segue.destination as? SuccessViewController {
successVC.userEmail = emailTextField.text
successVC.registrationDate = Date()
}
}
}
private func showValidationErrors() {
let errorMessage = validationErrors.joined(separator: "\n")
let alert = UIAlertController(
title: "Validation Error",
message: errorMessage,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
@IBAction func submitButtonTapped(_ sender: UIButton) {
performSegue(withIdentifier: "ShowSuccessSegue", sender: self)
}
}
// MARK: - Success ViewController
class SuccessViewController: UIViewController {
var userEmail: String?
var registrationDate: Date?
@IBOutlet weak var messageLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
displaySuccess()
}
private func displaySuccess() {
messageLabel.text = "Registration Successful!"
emailLabel.text = "Email: \(userEmail ?? "N/A")"
if let date = registrationDate {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .medium
dateLabel.text = "Date: \(formatter.string(from: date))"
}
}
}
import UIKit
// MARK: - Form ViewController with Validation
class FormViewController: UIViewController {
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var confirmPasswordTextField: UITextField!
@IBOutlet weak var agreeSwitch: UISwitch!
@IBOutlet weak var submitButton: UIButton!
private var validationErrors: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
setupValidation()
}
private func setupValidation() {
emailTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
passwordTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
confirmPasswordTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
@objc private func textFieldDidChange() {
validateForm()
}
private func validateForm() {
validationErrors.removeAll()
// Email validation
if let email = emailTextField.text, !email.isEmpty {
if !isValidEmail(email) {
validationErrors.append("Invalid email format")
}
} else {
validationErrors.append("Email is required")
}
// Password validation
if let password = passwordTextField.text, !password.isEmpty {
if password.count < 8 {
validationErrors.append("Password must be at least 8 characters")
}
} else {
validationErrors.append("Password is required")
}
// Confirm password validation
if passwordTextField.text != confirmPasswordTextField.text {
validationErrors.append("Passwords do not match")
}
// Terms agreement
if !agreeSwitch.isOn {
validationErrors.append("You must agree to terms and conditions")
}
submitButton.isEnabled = validationErrors.isEmpty
}
private func isValidEmail(_ email: String) -> Bool {
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailRegex)
return emailPredicate.evaluate(with: email)
}
// Override shouldPerformSegue to add validation
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if identifier == "ShowSuccessSegue" {
validateForm()
if !validationErrors.isEmpty {
showValidationErrors()
return false
}
return true
}
return true
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowSuccessSegue" {
if let successVC = segue.destination as? SuccessViewController {
successVC.userEmail = emailTextField.text
successVC.registrationDate = Date()
}
}
}
private func showValidationErrors() {
let errorMessage = validationErrors.joined(separator: "\n")
let alert = UIAlertController(
title: "Validation Error",
message: errorMessage,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
@IBAction func submitButtonTapped(_ sender: UIButton) {
performSegue(withIdentifier: "ShowSuccessSegue", sender: self)
}
}
// MARK: - Success ViewController
class SuccessViewController: UIViewController {
var userEmail: String?
var registrationDate: Date?
@IBOutlet weak var messageLabel: UILabel!
@IBOutlet weak var emailLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
displaySuccess()
}
private func displaySuccess() {
messageLabel.text = "Registration Successful!"
emailLabel.text = "Email: \(userEmail ?? "N/A")"
if let date = registrationDate {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .medium
dateLabel.text = "Date: \(formatter.string(from: date))"
}
}
}
Method 3: Delegation Pattern
The Delegation pattern is one of the most important design patterns in iOS development. It allows one object to send messages to another object when specific events occur. This is the primary method for backward data flow.
How It Works
- Define a protocol that declares methods for communication
- The delegating class has a weak delegate property
- The delegate class conforms to the protocol
- The delegating class calls delegate methods when needed
- The delegate implements the methods to handle events
Advantages
- ✅ Perfect for backward data flow
- ✅ Loose coupling through protocols
- ✅ Type-safe communication
- ✅ Clear contract between objects
- ✅ Apple's preferred pattern
Disadvantages
- ❌ Requires protocol definition
- ❌ More boilerplate code
- ❌ One-to-one relationship only
- ❌ Must manage weak references
Basic Delegation Implementation
import UIKit
// MARK: - Step 1: Define the Protocol
protocol ColorPickerDelegate: AnyObject {
func colorPicker(_ picker: ColorPickerViewController, didSelectColor color: UIColor)
func colorPickerDidCancel(_ picker: ColorPickerViewController)
}
// MARK: - Step 2: Delegating ViewController (Child)
class ColorPickerViewController: UIViewController {
// Weak delegate property to avoid retain cycles
weak var delegate: ColorPickerDelegate?
private var selectedColor: UIColor = .white
// UI Elements
private let colorPickerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let redSlider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 1
slider.translatesAutoresizingMaskIntoConstraints = false
return slider
}()
private let greenSlider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 1
slider.translatesAutoresizingMaskIntoConstraints = false
return slider
}()
private let blueSlider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 1
slider.translatesAutoresizingMaskIntoConstraints = false
return slider
}()
private let previewView: UIView = {
let view = UIView()
view.layer.cornerRadius = 50
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.systemGray.cgColor
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let selectButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Select Color", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let cancelButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Cancel", for: .normal)
button.backgroundColor = .systemGray5
button.setTitleColor(.systemBlue, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Choose Color"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
updatePreview()
}
private func setupUI() {
view.addSubview(previewView)
view.addSubview(redSlider)
view.addSubview(greenSlider)
view.addSubview(blueSlider)
view.addSubview(selectButton)
view.addSubview(cancelButton)
NSLayoutConstraint.activate([
previewView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
previewView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
previewView.widthAnchor.constraint(equalToConstant: 100),
previewView.heightAnchor.constraint(equalToConstant: 100),
redSlider.topAnchor.constraint(equalTo: previewView.bottomAnchor, constant: 40),
redSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
redSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
greenSlider.topAnchor.constraint(equalTo: redSlider.bottomAnchor, constant: 20),
greenSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
greenSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
blueSlider.topAnchor.constraint(equalTo: greenSlider.bottomAnchor, constant: 20),
blueSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
blueSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
selectButton.bottomAnchor.constraint(equalTo: cancelButton.topAnchor, constant: -16),
selectButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
selectButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
selectButton.heightAnchor.constraint(equalToConstant: 50),
cancelButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
cancelButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
cancelButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
cancelButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
redSlider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
greenSlider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
blueSlider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
selectButton.addTarget(self, action: #selector(selectButtonTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside)
}
@objc private func sliderValueChanged() {
updatePreview()
}
private func updatePreview() {
selectedColor = UIColor(
red: CGFloat(redSlider.value),
green: CGFloat(greenSlider.value),
blue: CGFloat(blueSlider.value),
alpha: 1.0
)
previewView.backgroundColor = selectedColor
}
@objc private func selectButtonTapped() {
// Call delegate method to pass data back
delegate?.colorPicker(self, didSelectColor: selectedColor)
navigationController?.popViewController(animated: true)
}
@objc private func cancelButtonTapped() {
// Call delegate method for cancellation
delegate?.colorPickerDidCancel(self)
navigationController?.popViewController(animated: true)
}
}
// MARK: - Step 3: Delegate ViewController (Parent)
class MainColorViewController: UIViewController {
private let colorDisplayView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 20
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.systemGray.cgColor
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let colorLabel: UILabel = {
let label = UILabel()
label.text = "No color selected"
label.textAlignment = .center
label.font = .systemFont(ofSize: 18, weight: .medium)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let chooseColorButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Choose Color", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Color Manager"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(colorDisplayView)
view.addSubview(colorLabel)
view.addSubview(chooseColorButton)
NSLayoutConstraint.activate([
colorDisplayView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
colorDisplayView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
colorDisplayView.widthAnchor.constraint(equalToConstant: 200),
colorDisplayView.heightAnchor.constraint(equalToConstant: 200),
colorLabel.topAnchor.constraint(equalTo: colorDisplayView.bottomAnchor, constant: 20),
colorLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
colorLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
chooseColorButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
chooseColorButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
chooseColorButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
chooseColorButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
chooseColorButton.addTarget(self, action: #selector(chooseColorButtonTapped), for: .touchUpInside)
}
@objc private func chooseColorButtonTapped() {
let colorPickerVC = ColorPickerViewController()
// Step 4: Set self as the delegate
colorPickerVC.delegate = self
navigationController?.pushViewController(colorPickerVC, animated: true)
}
}
// MARK: - Step 5: Conform to Protocol and Implement Methods
extension MainColorViewController: ColorPickerDelegate {
func colorPicker(_ picker: ColorPickerViewController, didSelectColor color: UIColor) {
// Receive data from child ViewController
colorDisplayView.backgroundColor = color
colorLabel.text = "Color selected successfully!"
// Save color to UserDefaults
saveColor(color)
// Perform additional actions
animateColorChange()
}
func colorPickerDidCancel(_ picker: ColorPickerViewController) {
colorLabel.text = "Color selection cancelled"
// Show cancellation feedback
showCancellationAlert()
}
private func saveColor(_ color: UIColor) {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
UserDefaults.standard.set(Double(red), forKey: "savedColorRed")
UserDefaults.standard.set(Double(green), forKey: "savedColorGreen")
UserDefaults.standard.set(Double(blue), forKey: "savedColorBlue")
}
private func animateColorChange() {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.3) {
self.colorDisplayView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
} completion: { _ in
UIView.animate(withDuration: 0.3) {
self.colorDisplayView.transform = .identity
}
}
}
private func showCancellationAlert() {
let alert = UIAlertController(
title: "Cancelled",
message: "Color selection was cancelled",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
import UIKit
// MARK: - Step 1: Define the Protocol
protocol ColorPickerDelegate: AnyObject {
func colorPicker(_ picker: ColorPickerViewController, didSelectColor color: UIColor)
func colorPickerDidCancel(_ picker: ColorPickerViewController)
}
// MARK: - Step 2: Delegating ViewController (Child)
class ColorPickerViewController: UIViewController {
// Weak delegate property to avoid retain cycles
weak var delegate: ColorPickerDelegate?
private var selectedColor: UIColor = .white
// UI Elements
private let colorPickerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let redSlider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 1
slider.translatesAutoresizingMaskIntoConstraints = false
return slider
}()
private let greenSlider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 1
slider.translatesAutoresizingMaskIntoConstraints = false
return slider
}()
private let blueSlider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 1
slider.value = 1
slider.translatesAutoresizingMaskIntoConstraints = false
return slider
}()
private let previewView: UIView = {
let view = UIView()
view.layer.cornerRadius = 50
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.systemGray.cgColor
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let selectButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Select Color", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let cancelButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Cancel", for: .normal)
button.backgroundColor = .systemGray5
button.setTitleColor(.systemBlue, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Choose Color"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
updatePreview()
}
private func setupUI() {
view.addSubview(previewView)
view.addSubview(redSlider)
view.addSubview(greenSlider)
view.addSubview(blueSlider)
view.addSubview(selectButton)
view.addSubview(cancelButton)
NSLayoutConstraint.activate([
previewView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
previewView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
previewView.widthAnchor.constraint(equalToConstant: 100),
previewView.heightAnchor.constraint(equalToConstant: 100),
redSlider.topAnchor.constraint(equalTo: previewView.bottomAnchor, constant: 40),
redSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
redSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
greenSlider.topAnchor.constraint(equalTo: redSlider.bottomAnchor, constant: 20),
greenSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
greenSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
blueSlider.topAnchor.constraint(equalTo: greenSlider.bottomAnchor, constant: 20),
blueSlider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
blueSlider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
selectButton.bottomAnchor.constraint(equalTo: cancelButton.topAnchor, constant: -16),
selectButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
selectButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
selectButton.heightAnchor.constraint(equalToConstant: 50),
cancelButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
cancelButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
cancelButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
cancelButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
redSlider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
greenSlider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
blueSlider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
selectButton.addTarget(self, action: #selector(selectButtonTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside)
}
@objc private func sliderValueChanged() {
updatePreview()
}
private func updatePreview() {
selectedColor = UIColor(
red: CGFloat(redSlider.value),
green: CGFloat(greenSlider.value),
blue: CGFloat(blueSlider.value),
alpha: 1.0
)
previewView.backgroundColor = selectedColor
}
@objc private func selectButtonTapped() {
// Call delegate method to pass data back
delegate?.colorPicker(self, didSelectColor: selectedColor)
navigationController?.popViewController(animated: true)
}
@objc private func cancelButtonTapped() {
// Call delegate method for cancellation
delegate?.colorPickerDidCancel(self)
navigationController?.popViewController(animated: true)
}
}
// MARK: - Step 3: Delegate ViewController (Parent)
class MainColorViewController: UIViewController {
private let colorDisplayView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = 20
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.systemGray.cgColor
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let colorLabel: UILabel = {
let label = UILabel()
label.text = "No color selected"
label.textAlignment = .center
label.font = .systemFont(ofSize: 18, weight: .medium)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let chooseColorButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Choose Color", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Color Manager"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
}
private func setupUI() {
view.addSubview(colorDisplayView)
view.addSubview(colorLabel)
view.addSubview(chooseColorButton)
NSLayoutConstraint.activate([
colorDisplayView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
colorDisplayView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
colorDisplayView.widthAnchor.constraint(equalToConstant: 200),
colorDisplayView.heightAnchor.constraint(equalToConstant: 200),
colorLabel.topAnchor.constraint(equalTo: colorDisplayView.bottomAnchor, constant: 20),
colorLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
colorLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
chooseColorButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
chooseColorButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
chooseColorButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
chooseColorButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func setupActions() {
chooseColorButton.addTarget(self, action: #selector(chooseColorButtonTapped), for: .touchUpInside)
}
@objc private func chooseColorButtonTapped() {
let colorPickerVC = ColorPickerViewController()
// Step 4: Set self as the delegate
colorPickerVC.delegate = self
navigationController?.pushViewController(colorPickerVC, animated: true)
}
}
// MARK: - Step 5: Conform to Protocol and Implement Methods
extension MainColorViewController: ColorPickerDelegate {
func colorPicker(_ picker: ColorPickerViewController, didSelectColor color: UIColor) {
// Receive data from child ViewController
colorDisplayView.backgroundColor = color
colorLabel.text = "Color selected successfully!"
// Save color to UserDefaults
saveColor(color)
// Perform additional actions
animateColorChange()
}
func colorPickerDidCancel(_ picker: ColorPickerViewController) {
colorLabel.text = "Color selection cancelled"
// Show cancellation feedback
showCancellationAlert()
}
private func saveColor(_ color: UIColor) {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
UserDefaults.standard.set(Double(red), forKey: "savedColorRed")
UserDefaults.standard.set(Double(green), forKey: "savedColorGreen")
UserDefaults.standard.set(Double(blue), forKey: "savedColorBlue")
}
private func animateColorChange() {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.3) {
self.colorDisplayView.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
} completion: { _ in
UIView.animate(withDuration: 0.3) {
self.colorDisplayView.transform = .identity
}
}
}
private func showCancellationAlert() {
let alert = UIAlertController(
title: "Cancelled",
message: "Color selection was cancelled",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
Advanced Delegation with Multiple Data Types
import UIKit
// MARK: - Complex Protocol with Multiple Methods
protocol UserProfileEditorDelegate: AnyObject {
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateProfile profile: UserProfile)
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateAvatar image: UIImage)
func userProfileEditor(_ editor: UserProfileEditorViewController, didChangePrivacySettings settings: PrivacySettings)
func userProfileEditorDidRequestDelete(_ editor: UserProfileEditorViewController)
func userProfileEditorDidCancel(_ editor: UserProfileEditorViewController)
}
// MARK: - Data Models
struct UserProfile {
var firstName: String
var lastName: String
var email: String
var phoneNumber: String?
var bio: String?
var dateOfBirth: Date?
var location: String?
var website: URL?
}
struct PrivacySettings {
var profileVisibility: ProfileVisibility
var showEmail: Bool
var showPhoneNumber: Bool
var allowMessages: Bool
var allowFriendRequests: Bool
}
enum ProfileVisibility {
case publicProfile
case friendsOnly
case privateProfile
}
// MARK: - Editor ViewController
class UserProfileEditorViewController: UIViewController {
weak var delegate: UserProfileEditorDelegate?
var currentProfile: UserProfile?
var currentPrivacySettings: PrivacySettings?
// UI Elements
private let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
private let contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let avatarImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 60
imageView.backgroundColor = .systemGray5
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let firstNameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "First Name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let lastNameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Last Name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Email"
textField.borderStyle = .roundedRect
textField.keyboardType = .emailAddress
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let phoneTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Phone Number"
textField.borderStyle = .roundedRect
textField.keyboardType = .phonePad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let bioTextView: UITextView = {
let textView = UITextView()
textView.font = .systemFont(ofSize: 16)
textView.layer.borderColor = UIColor.systemGray4.cgColor
textView.layer.borderWidth = 1
textView.layer.cornerRadius = 8
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
private let profileVisibilitySegment: UISegmentedControl = {
let segment = UISegmentedControl(items: ["Public", "Friends", "Private"])
segment.selectedSegmentIndex = 0
segment.translatesAutoresizingMaskIntoConstraints = false
return segment
}()
private let showEmailSwitch: UISwitch = {
let toggle = UISwitch()
toggle.translatesAutoresizingMaskIntoConstraints = false
return toggle
}()
private let showPhoneSwitch: UISwitch = {
let toggle = UISwitch()
toggle.translatesAutoresizingMaskIntoConstraints = false
return toggle
}()
private let saveButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Save Changes", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let cancelButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Cancel", for: .normal)
button.backgroundColor = .systemGray5
button.setTitleColor(.systemBlue, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let deleteButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Delete Profile", for: .normal)
button.backgroundColor = .systemRed
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Edit Profile"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
populateFields()
}
private func setupUI() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(avatarImageView)
contentView.addSubview(firstNameTextField)
contentView.addSubview(lastNameTextField)
contentView.addSubview(emailTextField)
contentView.addSubview(phoneTextField)
contentView.addSubview(bioTextView)
contentView.addSubview(profileVisibilitySegment)
contentView.addSubview(showEmailSwitch)
contentView.addSubview(showPhoneSwitch)
contentView.addSubview(saveButton)
contentView.addSubview(cancelButton)
contentView.addSubview(deleteButton)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
avatarImageView.widthAnchor.constraint(equalToConstant: 120),
avatarImageView.heightAnchor.constraint(equalToConstant: 120),
firstNameTextField.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 30),
firstNameTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
firstNameTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
firstNameTextField.heightAnchor.constraint(equalToConstant: 44),
lastNameTextField.topAnchor.constraint(equalTo: firstNameTextField.bottomAnchor, constant: 16),
lastNameTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
lastNameTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
lastNameTextField.heightAnchor.constraint(equalToConstant: 44),
emailTextField.topAnchor.constraint(equalTo: lastNameTextField.bottomAnchor, constant: 16),
emailTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
emailTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
emailTextField.heightAnchor.constraint(equalToConstant: 44),
phoneTextField.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 16),
phoneTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
phoneTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
phoneTextField.heightAnchor.constraint(equalToConstant: 44),
bioTextView.topAnchor.constraint(equalTo: phoneTextField.bottomAnchor, constant: 16),
bioTextView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
bioTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
bioTextView.heightAnchor.constraint(equalToConstant: 100),
profileVisibilitySegment.topAnchor.constraint(equalTo: bioTextView.bottomAnchor, constant: 30),
profileVisibilitySegment.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
profileVisibilitySegment.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
saveButton.topAnchor.constraint(equalTo: profileVisibilitySegment.bottomAnchor, constant: 40),
saveButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
saveButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
saveButton.heightAnchor.constraint(equalToConstant: 50),
cancelButton.topAnchor.constraint(equalTo: saveButton.bottomAnchor, constant: 12),
cancelButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
cancelButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
cancelButton.heightAnchor.constraint(equalToConstant: 50),
deleteButton.topAnchor.constraint(equalTo: cancelButton.bottomAnchor, constant: 30),
deleteButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
deleteButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
deleteButton.heightAnchor.constraint(equalToConstant: 50),
deleteButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30)
])
}
private func setupActions() {
let avatarTapGesture = UITapGestureRecognizer(target: self, action: #selector(avatarTapped))
avatarImageView.addGestureRecognizer(avatarTapGesture)
saveButton.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside)
deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
profileVisibilitySegment.addTarget(self, action: #selector(privacySettingsChanged), for: .valueChanged)
showEmailSwitch.addTarget(self, action: #selector(privacySettingsChanged), for: .valueChanged)
showPhoneSwitch.addTarget(self, action: #selector(privacySettingsChanged), for: .valueChanged)
}
private func populateFields() {
guard let profile = currentProfile else { return }
firstNameTextField.text = profile.firstName
lastNameTextField.text = profile.lastName
emailTextField.text = profile.email
phoneTextField.text = profile.phoneNumber
bioTextView.text = profile.bio
if let settings = currentPrivacySettings {
switch settings.profileVisibility {
case .publicProfile: profileVisibilitySegment.selectedSegmentIndex = 0
case .friendsOnly: profileVisibilitySegment.selectedSegmentIndex = 1
case .privateProfile: profileVisibilitySegment.selectedSegmentIndex = 2
}
showEmailSwitch.isOn = settings.showEmail
showPhoneSwitch.isOn = settings.showPhoneNumber
}
}
@objc private func avatarTapped() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .photoLibrary
imagePicker.allowsEditing = true
present(imagePicker, animated: true)
}
@objc private func saveButtonTapped() {
guard validateInputs() else { return }
// Create updated profile
let updatedProfile = UserProfile(
firstName: firstNameTextField.text ?? "",
lastName: lastNameTextField.text ?? "",
email: emailTextField.text ?? "",
phoneNumber: phoneTextField.text,
bio: bioTextView.text,
dateOfBirth: currentProfile?.dateOfBirth,
location: currentProfile?.location,
website: currentProfile?.website
)
// Notify delegate about profile update
delegate?.userProfileEditor(self, didUpdateProfile: updatedProfile)
navigationController?.popViewController(animated: true)
}
@objc private func cancelButtonTapped() {
// Notify delegate about cancellation
delegate?.userProfileEditorDidCancel(self)
navigationController?.popViewController(animated: true)
}
@objc private func deleteButtonTapped() {
let alert = UIAlertController(
title: "Delete Profile",
message: "Are you sure you want to delete your profile? This action cannot be undone.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
guard let self = self else { return }
self.delegate?.userProfileEditorDidRequestDelete(self)
self.navigationController?.popViewController(animated: true)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
@objc private func privacySettingsChanged() {
let visibility: ProfileVisibility
switch profileVisibilitySegment.selectedSegmentIndex {
case 0: visibility = .publicProfile
case 1: visibility = .friendsOnly
case 2: visibility = .privateProfile
default: visibility = .publicProfile
}
let updatedSettings = PrivacySettings(
profileVisibility: visibility,
showEmail: showEmailSwitch.isOn,
showPhoneNumber: showPhoneSwitch.isOn,
allowMessages: currentPrivacySettings?.allowMessages ?? true,
allowFriendRequests: currentPrivacySettings?.allowFriendRequests ?? true
)
// Notify delegate about privacy settings change
delegate?.userProfileEditor(self, didChangePrivacySettings: updatedSettings)
}
private func validateInputs() -> Bool {
guard let firstName = firstNameTextField.text, !firstName.isEmpty,
let lastName = lastNameTextField.text, !lastName.isEmpty,
let email = emailTextField.text, !email.isEmpty else {
showAlert(message: "Please fill in all required fields")
return false
}
return true
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
// MARK: - UIImagePickerControllerDelegate
extension UserProfileEditorViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let editedImage = info[.editedImage] as? UIImage {
avatarImageView.image = editedImage
// Notify delegate about avatar change
delegate?.userProfileEditor(self, didUpdateAvatar: editedImage)
} else if let originalImage = info[.originalImage] as? UIImage {
avatarImageView.image = originalImage
delegate?.userProfileEditor(self, didUpdateAvatar: originalImage)
}
dismiss(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true)
}
}
// MARK: - Parent ViewController Implementing Delegate
class UserProfileViewController: UIViewController {
private var userProfile: UserProfile?
private var privacySettings: PrivacySettings?
private let profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 60
imageView.backgroundColor = .systemGray5
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let emailLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16)
label.textColor = .secondaryLabel
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let editButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Edit Profile", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "My Profile"
view.backgroundColor = .systemBackground
setupUI()
loadUserProfile()
}
private func setupUI() {
view.addSubview(profileImageView)
view.addSubview(nameLabel)
view.addSubview(emailLabel)
view.addSubview(editButton)
editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside)
NSLayoutConstraint.activate([
profileImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
profileImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
profileImageView.widthAnchor.constraint(equalToConstant: 120),
profileImageView.heightAnchor.constraint(equalToConstant: 120),
nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 20),
nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
emailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
emailLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
editButton.topAnchor.constraint(equalTo: emailLabel.bottomAnchor, constant: 40),
editButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
editButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
editButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func loadUserProfile() {
userProfile = UserProfile(
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
phoneNumber: "+1234567890",
bio: "iOS Developer"
)
privacySettings = PrivacySettings(
profileVisibility: .publicProfile,
showEmail: true,
showPhoneNumber: false,
allowMessages: true,
allowFriendRequests: true
)
updateUI()
}
private func updateUI() {
guard let profile = userProfile else { return }
nameLabel.text = "\(profile.firstName) \(profile.lastName)"
emailLabel.text = profile.email
}
@objc private func editButtonTapped() {
let editorVC = UserProfileEditorViewController()
editorVC.currentProfile = userProfile
editorVC.currentPrivacySettings = privacySettings
// Set self as delegate
editorVC.delegate = self
navigationController?.pushViewController(editorVC, animated: true)
}
}
// MARK: - Conform to Delegate Protocol
extension UserProfileViewController: UserProfileEditorDelegate {
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateProfile profile: UserProfile) {
// Receive updated profile
self.userProfile = profile
updateUI()
// Save to database or API
saveProfileToServer(profile)
// Show success message
showSuccessAlert(message: "Profile updated successfully!")
}
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateAvatar image: UIImage) {
// Receive new avatar
profileImageView.image = image
// Upload to server
uploadAvatarToServer(image)
}
func userProfileEditor(_ editor: UserProfileEditorViewController, didChangePrivacySettings settings: PrivacySettings) {
// Receive privacy settings changes
self.privacySettings = settings
// Save settings
savePrivacySettings(settings)
print("Privacy settings updated: \(settings)")
}
func userProfileEditorDidRequestDelete(_ editor: UserProfileEditorViewController) {
// Handle profile deletion request
let alert = UIAlertController(
title: "Confirm Deletion",
message: "Your profile will be permanently deleted. Continue?",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
self?.deleteProfile()
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
func userProfileEditorDidCancel(_ editor: UserProfileEditorViewController) {
print("Profile editing cancelled")
}
// Helper methods
private func saveProfileToServer(_ profile: UserProfile) {
// API call to save profile
print("Saving profile to server: \(profile)")
}
private func uploadAvatarToServer(_ image: UIImage) {
// API call to upload avatar
print("Uploading avatar to server")
}
private func savePrivacySettings(_ settings: PrivacySettings) {
// Save privacy settings
print("Saving privacy settings")
}
private func deleteProfile() {
// API call to delete profile
print("Deleting profile")
// Navigate back or logout
navigationController?.popToRootViewController(animated: true)
}
private func showSuccessAlert(message: String) {
let alert = UIAlertController(title: "Success", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
import UIKit
// MARK: - Complex Protocol with Multiple Methods
protocol UserProfileEditorDelegate: AnyObject {
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateProfile profile: UserProfile)
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateAvatar image: UIImage)
func userProfileEditor(_ editor: UserProfileEditorViewController, didChangePrivacySettings settings: PrivacySettings)
func userProfileEditorDidRequestDelete(_ editor: UserProfileEditorViewController)
func userProfileEditorDidCancel(_ editor: UserProfileEditorViewController)
}
// MARK: - Data Models
struct UserProfile {
var firstName: String
var lastName: String
var email: String
var phoneNumber: String?
var bio: String?
var dateOfBirth: Date?
var location: String?
var website: URL?
}
struct PrivacySettings {
var profileVisibility: ProfileVisibility
var showEmail: Bool
var showPhoneNumber: Bool
var allowMessages: Bool
var allowFriendRequests: Bool
}
enum ProfileVisibility {
case publicProfile
case friendsOnly
case privateProfile
}
// MARK: - Editor ViewController
class UserProfileEditorViewController: UIViewController {
weak var delegate: UserProfileEditorDelegate?
var currentProfile: UserProfile?
var currentPrivacySettings: PrivacySettings?
// UI Elements
private let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
private let contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let avatarImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 60
imageView.backgroundColor = .systemGray5
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let firstNameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "First Name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let lastNameTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Last Name"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let emailTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Email"
textField.borderStyle = .roundedRect
textField.keyboardType = .emailAddress
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let phoneTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Phone Number"
textField.borderStyle = .roundedRect
textField.keyboardType = .phonePad
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private let bioTextView: UITextView = {
let textView = UITextView()
textView.font = .systemFont(ofSize: 16)
textView.layer.borderColor = UIColor.systemGray4.cgColor
textView.layer.borderWidth = 1
textView.layer.cornerRadius = 8
textView.translatesAutoresizingMaskIntoConstraints = false
return textView
}()
private let profileVisibilitySegment: UISegmentedControl = {
let segment = UISegmentedControl(items: ["Public", "Friends", "Private"])
segment.selectedSegmentIndex = 0
segment.translatesAutoresizingMaskIntoConstraints = false
return segment
}()
private let showEmailSwitch: UISwitch = {
let toggle = UISwitch()
toggle.translatesAutoresizingMaskIntoConstraints = false
return toggle
}()
private let showPhoneSwitch: UISwitch = {
let toggle = UISwitch()
toggle.translatesAutoresizingMaskIntoConstraints = false
return toggle
}()
private let saveButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Save Changes", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let cancelButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Cancel", for: .normal)
button.backgroundColor = .systemGray5
button.setTitleColor(.systemBlue, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let deleteButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Delete Profile", for: .normal)
button.backgroundColor = .systemRed
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "Edit Profile"
view.backgroundColor = .systemBackground
setupUI()
setupActions()
populateFields()
}
private func setupUI() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(avatarImageView)
contentView.addSubview(firstNameTextField)
contentView.addSubview(lastNameTextField)
contentView.addSubview(emailTextField)
contentView.addSubview(phoneTextField)
contentView.addSubview(bioTextView)
contentView.addSubview(profileVisibilitySegment)
contentView.addSubview(showEmailSwitch)
contentView.addSubview(showPhoneSwitch)
contentView.addSubview(saveButton)
contentView.addSubview(cancelButton)
contentView.addSubview(deleteButton)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
avatarImageView.widthAnchor.constraint(equalToConstant: 120),
avatarImageView.heightAnchor.constraint(equalToConstant: 120),
firstNameTextField.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 30),
firstNameTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
firstNameTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
firstNameTextField.heightAnchor.constraint(equalToConstant: 44),
lastNameTextField.topAnchor.constraint(equalTo: firstNameTextField.bottomAnchor, constant: 16),
lastNameTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
lastNameTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
lastNameTextField.heightAnchor.constraint(equalToConstant: 44),
emailTextField.topAnchor.constraint(equalTo: lastNameTextField.bottomAnchor, constant: 16),
emailTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
emailTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
emailTextField.heightAnchor.constraint(equalToConstant: 44),
phoneTextField.topAnchor.constraint(equalTo: emailTextField.bottomAnchor, constant: 16),
phoneTextField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
phoneTextField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
phoneTextField.heightAnchor.constraint(equalToConstant: 44),
bioTextView.topAnchor.constraint(equalTo: phoneTextField.bottomAnchor, constant: 16),
bioTextView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
bioTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
bioTextView.heightAnchor.constraint(equalToConstant: 100),
profileVisibilitySegment.topAnchor.constraint(equalTo: bioTextView.bottomAnchor, constant: 30),
profileVisibilitySegment.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
profileVisibilitySegment.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
saveButton.topAnchor.constraint(equalTo: profileVisibilitySegment.bottomAnchor, constant: 40),
saveButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
saveButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
saveButton.heightAnchor.constraint(equalToConstant: 50),
cancelButton.topAnchor.constraint(equalTo: saveButton.bottomAnchor, constant: 12),
cancelButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
cancelButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
cancelButton.heightAnchor.constraint(equalToConstant: 50),
deleteButton.topAnchor.constraint(equalTo: cancelButton.bottomAnchor, constant: 30),
deleteButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
deleteButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
deleteButton.heightAnchor.constraint(equalToConstant: 50),
deleteButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -30)
])
}
private func setupActions() {
let avatarTapGesture = UITapGestureRecognizer(target: self, action: #selector(avatarTapped))
avatarImageView.addGestureRecognizer(avatarTapGesture)
saveButton.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(cancelButtonTapped), for: .touchUpInside)
deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
profileVisibilitySegment.addTarget(self, action: #selector(privacySettingsChanged), for: .valueChanged)
showEmailSwitch.addTarget(self, action: #selector(privacySettingsChanged), for: .valueChanged)
showPhoneSwitch.addTarget(self, action: #selector(privacySettingsChanged), for: .valueChanged)
}
private func populateFields() {
guard let profile = currentProfile else { return }
firstNameTextField.text = profile.firstName
lastNameTextField.text = profile.lastName
emailTextField.text = profile.email
phoneTextField.text = profile.phoneNumber
bioTextView.text = profile.bio
if let settings = currentPrivacySettings {
switch settings.profileVisibility {
case .publicProfile: profileVisibilitySegment.selectedSegmentIndex = 0
case .friendsOnly: profileVisibilitySegment.selectedSegmentIndex = 1
case .privateProfile: profileVisibilitySegment.selectedSegmentIndex = 2
}
showEmailSwitch.isOn = settings.showEmail
showPhoneSwitch.isOn = settings.showPhoneNumber
}
}
@objc private func avatarTapped() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .photoLibrary
imagePicker.allowsEditing = true
present(imagePicker, animated: true)
}
@objc private func saveButtonTapped() {
guard validateInputs() else { return }
// Create updated profile
let updatedProfile = UserProfile(
firstName: firstNameTextField.text ?? "",
lastName: lastNameTextField.text ?? "",
email: emailTextField.text ?? "",
phoneNumber: phoneTextField.text,
bio: bioTextView.text,
dateOfBirth: currentProfile?.dateOfBirth,
location: currentProfile?.location,
website: currentProfile?.website
)
// Notify delegate about profile update
delegate?.userProfileEditor(self, didUpdateProfile: updatedProfile)
navigationController?.popViewController(animated: true)
}
@objc private func cancelButtonTapped() {
// Notify delegate about cancellation
delegate?.userProfileEditorDidCancel(self)
navigationController?.popViewController(animated: true)
}
@objc private func deleteButtonTapped() {
let alert = UIAlertController(
title: "Delete Profile",
message: "Are you sure you want to delete your profile? This action cannot be undone.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
guard let self = self else { return }
self.delegate?.userProfileEditorDidRequestDelete(self)
self.navigationController?.popViewController(animated: true)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
@objc private func privacySettingsChanged() {
let visibility: ProfileVisibility
switch profileVisibilitySegment.selectedSegmentIndex {
case 0: visibility = .publicProfile
case 1: visibility = .friendsOnly
case 2: visibility = .privateProfile
default: visibility = .publicProfile
}
let updatedSettings = PrivacySettings(
profileVisibility: visibility,
showEmail: showEmailSwitch.isOn,
showPhoneNumber: showPhoneSwitch.isOn,
allowMessages: currentPrivacySettings?.allowMessages ?? true,
allowFriendRequests: currentPrivacySettings?.allowFriendRequests ?? true
)
// Notify delegate about privacy settings change
delegate?.userProfileEditor(self, didChangePrivacySettings: updatedSettings)
}
private func validateInputs() -> Bool {
guard let firstName = firstNameTextField.text, !firstName.isEmpty,
let lastName = lastNameTextField.text, !lastName.isEmpty,
let email = emailTextField.text, !email.isEmpty else {
showAlert(message: "Please fill in all required fields")
return false
}
return true
}
private func showAlert(message: String) {
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
// MARK: - UIImagePickerControllerDelegate
extension UserProfileEditorViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let editedImage = info[.editedImage] as? UIImage {
avatarImageView.image = editedImage
// Notify delegate about avatar change
delegate?.userProfileEditor(self, didUpdateAvatar: editedImage)
} else if let originalImage = info[.originalImage] as? UIImage {
avatarImageView.image = originalImage
delegate?.userProfileEditor(self, didUpdateAvatar: originalImage)
}
dismiss(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true)
}
}
// MARK: - Parent ViewController Implementing Delegate
class UserProfileViewController: UIViewController {
private var userProfile: UserProfile?
private var privacySettings: PrivacySettings?
private let profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 60
imageView.backgroundColor = .systemGray5
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let emailLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16)
label.textColor = .secondaryLabel
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let editButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Edit Profile", for: .normal)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "My Profile"
view.backgroundColor = .systemBackground
setupUI()
loadUserProfile()
}
private func setupUI() {
view.addSubview(profileImageView)
view.addSubview(nameLabel)
view.addSubview(emailLabel)
view.addSubview(editButton)
editButton.addTarget(self, action: #selector(editButtonTapped), for: .touchUpInside)
NSLayoutConstraint.activate([
profileImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
profileImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
profileImageView.widthAnchor.constraint(equalToConstant: 120),
profileImageView.heightAnchor.constraint(equalToConstant: 120),
nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 20),
nameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
emailLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
emailLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
emailLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
editButton.topAnchor.constraint(equalTo: emailLabel.bottomAnchor, constant: 40),
editButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
editButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
editButton.heightAnchor.constraint(equalToConstant: 50)
])
}
private func loadUserProfile() {
userProfile = UserProfile(
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
phoneNumber: "+1234567890",
bio: "iOS Developer"
)
privacySettings = PrivacySettings(
profileVisibility: .publicProfile,
showEmail: true,
showPhoneNumber: false,
allowMessages: true,
allowFriendRequests: true
)
updateUI()
}
private func updateUI() {
guard let profile = userProfile else { return }
nameLabel.text = "\(profile.firstName) \(profile.lastName)"
emailLabel.text = profile.email
}
@objc private func editButtonTapped() {
let editorVC = UserProfileEditorViewController()
editorVC.currentProfile = userProfile
editorVC.currentPrivacySettings = privacySettings
// Set self as delegate
editorVC.delegate = self
navigationController?.pushViewController(editorVC, animated: true)
}
}
// MARK: - Conform to Delegate Protocol
extension UserProfileViewController: UserProfileEditorDelegate {
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateProfile profile: UserProfile) {
// Receive updated profile
self.userProfile = profile
updateUI()
// Save to database or API
saveProfileToServer(profile)
// Show success message
showSuccessAlert(message: "Profile updated successfully!")
}
func userProfileEditor(_ editor: UserProfileEditorViewController, didUpdateAvatar image: UIImage) {
// Receive new avatar
profileImageView.image = image
// Upload to server
uploadAvatarToServer(image)
}
func userProfileEditor(_ editor: UserProfileEditorViewController, didChangePrivacySettings settings: PrivacySettings) {
// Receive privacy settings changes
self.privacySettings = settings
// Save settings
savePrivacySettings(settings)
print("Privacy settings updated: \(settings)")
}
func userProfileEditorDidRequestDelete(_ editor: UserProfileEditorViewController) {
// Handle profile deletion request
let alert = UIAlertController(
title: "Confirm Deletion",
message: "Your profile will be permanently deleted. Continue?",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
self?.deleteProfile()
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
func userProfileEditorDidCancel(_ editor: UserProfileEditorViewController) {
print("Profile editing cancelled")
}
// Helper methods
private func saveProfileToServer(_ profile: UserProfile) {
// API call to save profile
print("Saving profile to server: \(profile)")
}
private func uploadAvatarToServer(_ image: UIImage) {
// API call to upload avatar
print("Uploading avatar to server")
}
private func savePrivacySettings(_ settings: PrivacySettings) {
// Save privacy settings
print("Saving privacy settings")
}
private func deleteProfile() {
// API call to delete profile
print("Deleting profile")
// Navigate back or logout
navigationController?.popToRootViewController(animated: true)
}
private func showSuccessAlert(message: String) {
let alert = UIAlertController(title: "Success", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
Optional Protocol Methods
import UIKit
// MARK: - Protocol with Optional Methods
@objc protocol FormDelegate: AnyObject {
func formDidSubmit(_ form: FormViewController, withData data: [String: Any])
// Optional methods
@objc optional func formDidStartEditing(_ form: FormViewController)
@objc optional func form(_ form: FormViewController, didUpdateField fieldName: String, value: Any)
@objc optional func formDidCancel(_ form: FormViewController)
@objc optional func form(_ form: FormViewController, didEncounterError error: Error)
}
// MARK: - Form View Controller
class FormViewController: UIViewController {
weak var delegate: FormDelegate?
private var formData: [String: Any] = [:]
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
// UI setup code
}
private func notifyEditingStarted() {
// Call optional delegate method
delegate?.formDidStartEditing?(self)
}
private func notifyFieldUpdate(fieldName: String, value: Any) {
formData[fieldName] = value
// Call optional delegate method
delegate?.form?(self, didUpdateField: fieldName, value: value)
}
@objc private func submitButtonTapped() {
// Call required delegate method
delegate?.formDidSubmit(self, withData: formData)
}
@objc private func cancelButtonTapped() {
// Call optional delegate method
delegate?.formDidCancel?(self)
}
private func handleError(_ error: Error) {
// Call optional delegate method
delegate?.form?(self, didEncounterError: error)
}
}
// MARK: - Implementing Optional Methods
class ParentViewController: UIViewController, FormDelegate {
// Required method
func formDidSubmit(_ form: FormViewController, withData data: [String : Any]) {
print("Form submitted with data: \(data)")
}
// Optional methods - implement only what you need
func form(_ form: FormViewController, didUpdateField fieldName: String, value: Any) {
print("Field \(fieldName) updated to: \(value)")
}
func formDidCancel(_ form: FormViewController) {
print("Form cancelled")
}
// formDidStartEditing and didEncounterError are not implemented
// No error because they're marked as @objc optional
}
import UIKit
// MARK: - Protocol with Optional Methods
@objc protocol FormDelegate: AnyObject {
func formDidSubmit(_ form: FormViewController, withData data: [String: Any])
// Optional methods
@objc optional func formDidStartEditing(_ form: FormViewController)
@objc optional func form(_ form: FormViewController, didUpdateField fieldName: String, value: Any)
@objc optional func formDidCancel(_ form: FormViewController)
@objc optional func form(_ form: FormViewController, didEncounterError error: Error)
}
// MARK: - Form View Controller
class FormViewController: UIViewController {
weak var delegate: FormDelegate?
private var formData: [String: Any] = [:]
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
// UI setup code
}
private func notifyEditingStarted() {
// Call optional delegate method
delegate?.formDidStartEditing?(self)
}
private func notifyFieldUpdate(fieldName: String, value: Any) {
formData[fieldName] = value
// Call optional delegate method
delegate?.form?(self, didUpdateField: fieldName, value: value)
}
@objc private func submitButtonTapped() {
// Call required delegate method
delegate?.formDidSubmit(self, withData: formData)
}
@objc private func cancelButtonTapped() {
// Call optional delegate method
delegate?.formDidCancel?(self)
}
private func handleError(_ error: Error) {
// Call optional delegate method
delegate?.form?(self, didEncounterError: error)
}
}
// MARK: - Implementing Optional Methods
class ParentViewController: UIViewController, FormDelegate {
// Required method
func formDidSubmit(_ form: FormViewController, withData data: [String : Any]) {
print("Form submitted with data: \(data)")
}
// Optional methods - implement only what you need
func form(_ form: FormViewController, didUpdateField fieldName: String, value: Any) {
print("Field \(fieldName) updated to: \(value)")
}
func formDidCancel(_ form: FormViewController) {
print("Form cancelled")
}
// formDidStartEditing and didEncounterError are not implemented
// No error because they're marked as @objc optional
}
Method 4: Closures and Callbacks
Closures (also known as callbacks or completion handlers) provide a modern, Swift-native way to pass data between ViewControllers. They're especially useful for asynchronous operations and simple backward data flow.
Comments
Post a Comment