Skip to content

iOS Development - Advanced iOS

iOS Development with Swift - Advanced iOS Topics

Section titled “iOS Development with Swift - Advanced iOS Topics”

This chapter delves into more advanced iOS features crucial for building feature-rich applications. We’ll cover creating multi-screen apps, navigation, displaying lists of data with Table Views, data persistence, and basic web content display.

Carefully review each section, as these concepts form the backbone of many iOS applications.

Most apps consist of multiple screens. Each screen is typically managed by a UIViewController. We’ll learn how to add multiple view controllers and navigate between them.

Start with a new project or an existing one. By default, a new project (using Storyboard) comes with one UIViewController scene in Main.storyboard and its corresponding ViewController.swift file. The arrow pointing to this scene in the storyboard indicates it’s the initial view controller (entry point).

  1. Open Main.storyboard.
  2. Open the Object Library (Shift-Command-L).
  3. Search for ‘View Controller’ and drag a new View Controller object onto the storyboard canvas, placing it beside the existing one.
  4. Create a new Swift file for this view controller: Right-click on your project’s folder in the Project Navigator > New File… > Choose ‘Cocoa Touch Class’ > Next. Name it (e.g., SecondViewController), make it a subclass of UIViewController, ensure ‘Language’ is Swift, and ‘Also create XIB file’ is unchecked. Click Next, then Create.
  5. Link the storyboard scene to this new class: Select the new View Controller scene in the storyboard. Go to the Identity Inspector (third icon from the right in the Inspector pane). In the ‘Class’ field, type and select SecondViewController.

You now have a second view controller scene and its Swift class, but no way to reach it yet.

Transitioning from one view controller to another is typically done using ‘Segues’ in Storyboard, often managed by a UINavigationController.

  1. Embed the first View Controller in a Navigation Controller: Select your initial View Controller scene in Main.storyboard. Go to the Xcode menu: Editor > Embed In > Navigation Controller. This will add a Navigation Controller scene before your first VC and add a navigation bar to your first VC.
  2. Add a button to the first View Controller: Drag a UIButton onto your first View Controller’s view. Title it ‘Go to Second View’.
  3. Create a Segue: Control-drag from this button to the SecondViewController scene. A popup will appear. Under ‘Action Segue’, choose ‘Show’. This creates a segue that will push the second view controller onto the navigation stack when the button is tapped.

Run the app. You’ll see the first view controller with a navigation bar. Tapping the button will transition to the second view controller, and the navigation bar will automatically show a ‘Back’ button to return to the first view.

Select the Navigation Item of a view controller (it appears inside the navigation bar in the storyboard document outline or by clicking the bar area within the VC scene). In the Attributes Inspector, you can set:

  • Title: The title displayed in the center of the navigation bar for that view controller.
  • Prompt: A smaller line of text above the title.
  • Back Button: Customize the text of the back button that appears on the next screen when this VC is pushed.

You can also add UIBarButtonItems to the navigation bar (e.g., for ‘Edit’, ‘Add’, ‘Done’ actions). Drag a ‘Bar Button Item’ from the Object Library onto the navigation bar in your storyboard. To make it trigger a segue, control-drag from the bar button item to another view controller and choose ‘Show’.

UITableView is used to display data in a single-column list of rows, which can be grouped into sections. It’s highly efficient for displaying large datasets.

Let’s create a simple table view. Create a new project or use an existing View Controller.

  1. Drag a ‘Table View’ from the Object Library onto your View Controller’s view. Use Auto Layout to make it fill the view.
  2. Prototype Cells: Select the Table View. In the Attributes Inspector, set ‘Prototype Cells’ to 1. This adds a template cell to the table view in the storyboard.
  3. Cell Identifier: Select the ‘Table View Cell’ (either directly or via the Document Outline). In its Attributes Inspector, set the ‘Identifier’ to a unique string, e.g., ‘MyCell’. This identifier is used to dequeue cells in code.
  4. Style (Optional): You can change the cell’s ‘Style’ (e.g., Basic, Right Detail, Subtitle) in the Attributes Inspector to get pre-configured label layouts.

A table view needs a data source (to provide data) and a delegate (to handle interactions and appearance).

  1. In Storyboard: Control-drag from the Table View to the View Controller icon (yellow circle) at the top of its scene. Select ‘dataSource’. Repeat and select ‘delegate’.
  2. In ViewController.swift: Conform to the protocols: class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { ... }
  3. Add an outlet for the table view: Control-drag from the Table View in Storyboard to your ViewController.swift file and create an outlet named tableView.

Implement the required UITableViewDataSource methods:

// Sample data array
let dataArray = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count // Number of rows
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Dequeue a reusable cell
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath)
// Configure the cell
let item = dataArray[indexPath.row]
cell.textLabel?.text = item // Works if cell style is Basic, Left Detail, etc.
return cell
}

In viewDidLoad(), ensure the delegate and dataSource are set if not done in storyboard, or if you prefer to set them in code: tableView.dataSource = self tableView.delegate = self Also, you might want to add tableView.reloadData() if your data changes after viewDidLoad.

Run the app. You should see a list of fruits. For more complex cells, you’d create a custom UITableViewCell subclass.

Example: Egg Timer Application (Using Timer)

Section titled “Example: Egg Timer Application (Using Timer)”

This application demonstrates using Timer to create a countdown. The UI will have a label to display time, and buttons for Start, Pause, Reset, and adjusting time.

  1. Navigation Bar: Title ‘Egg Timer’. Left Bar Button Item ‘Pause’ (e.g., system pause icon). Right Bar Button Item ‘Start’ (e.g., system play icon).
  2. Center Label: Large label to display remaining time (e.g., timeLeftLabel). Initially ‘210’.
  3. Toolbar (at bottom):
    • Bar Button Item ‘-10s’.
    • Flexible Space Bar Button Item.
    • Bar Button Item ‘Reset’.
    • Flexible Space Bar Button Item.
    • Bar Button Item ‘+10s’. Connect these UI elements (label as outlet, buttons as actions) to your ViewController.swift.
import UIKit
class EggTimerViewController: UIViewController {
@IBOutlet weak var timeLeftLabel: UILabel!
// Add outlets for buttons if their state needs to change (e.g., enable/disable)
var eggTimer: Timer? // Use optional Timer
var timerActive = false
var timeLeft = 210 { // Initial time in seconds
didSet {
timeLeftLabel.text = String(timeLeft)
}
}
override func viewDidLoad() {
super.viewDidLoad()
timeLeftLabel.text = String(timeLeft)
// Setup initial button states if needed
}
func processTimer() {
if timeLeft <= 0 {
stopTimer(reset: false) // Stop but don't reset automatically
timeLeftLabel.text = "Done!"
// Optionally play a sound or show an alert
return
}
timeLeft -= 1
}
func startTimer() {
if !timerActive {
timerActive = true
// Invalidate existing timer before starting a new one
eggTimer?.invalidate()
eggTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.processTimer()
}
// Update button states (e.g., disable Start, enable Pause)
}
}
func stopTimer(reset: Bool) {
timerActive = false
eggTimer?.invalidate()
eggTimer = nil
if reset {
timeLeft = 210
}
// Update button states (e.g., enable Start, disable Pause)
}
// MARK: - IBActions
@IBAction func startTimerButtonTapped(_ sender: Any) {
startTimer()
}
@IBAction func pauseTimerButtonTapped(_ sender: Any) {
stopTimer(reset: false)
}
@IBAction func subtractTimeTapped(_ sender: Any) {
if timeLeft > 10 {
timeLeft -= 10
} else {
timeLeft = 0 // Prevent going below zero
}
if timeLeft == 0 && timerActive { // If timer was active and reached 0
processTimer() // To trigger "Done!" state
}
}
@IBAction func resetTimerTapped(_ sender: Any) {
stopTimer(reset: true)
}
@IBAction func addTimeTapped(_ sender: Any) {
timeLeft += 10
}
}

The Timer.scheduledTimer method now often uses a block-based approach for its action, which can capture self weakly to avoid retain cycles. The #selector approach is also still valid but requires an @objc method.

Apps often need to save data locally on the device. Two common ways are UserDefaults (for small pieces of data like settings) and Core Data (for larger, structured datasets).

UserDefaults is a simple key-value store, ideal for user preferences or simple app state.

// Get the standard UserDefaults instance
let defaults = UserDefaults.standard
// Set values
defaults.set("John Doe", forKey: "userName")
defaults.set(25, forKey: "userAge")
defaults.set(true, forKey: "isUserLoggedIn")
// Get values (provide default if key doesn't exist or type mismatch)
let userName = defaults.string(forKey: "userName") ?? "Guest"
let userAge = defaults.integer(forKey: "userAge") // Defaults to 0 if not found
let isLoggedIn = defaults.bool(forKey: "isUserLoggedIn") // Defaults to false
// To save immediately (optional, system does it periodically)
// defaults.synchronize() // Less needed in modern iOS, but can be used.

Core Data is a powerful object graph and persistence framework. It allows you to model, save, and retrieve structured data.

  1. Project Setup: When creating a new project, check the ‘Use Core Data’ box. This adds a .xcdatamodeld file and boilerplate code in AppDelegate.swift for the Core Data stack (NSPersistentContainer).
  2. Data Model: Open the .xcdatamodeld file. Click ‘Add Entity’ to create a table (e.g., ‘User’). Select the entity and add ‘Attributes’ (columns, e.g., name of type String, age of type Integer 16/32/64).
  3. Accessing the Context: The NSPersistentContainer in AppDelegate provides the viewContext, which is your scratchpad for working with managed objects.
import UIKit
import CoreData
// Assuming 'User' entity with 'name' (String) and 'creationDate' (Date) attributes
// Get the AppDelegate and NSManagedObjectContext
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
// Handle error appropriately
return
}
let context = appDelegate.persistentContainer.viewContext
// Saving Data
func saveNewUser(name: String) {
// Create a new User managed object
// If you've generated NSManagedObject subclasses (Editor > Create NSManagedObject Subclass...):
// let newUser = User(context: context)
// newUser.name = name
// newUser.creationDate = Date()
// Or using NSEntityDescription if no subclass generated:
guard let userEntity = NSEntityDescription.entity(forEntityName: "User", in: context) else {
print("Failed to find User entity")
return
}
let newUser = NSManagedObject(entity: userEntity, insertInto: context)
newUser.setValue(name, forKey: "name")
newUser.setValue(Date(), forKey: "creationDate")
do {
try context.save() // Save changes to persistent store
print("User '\(name)' saved.")
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
// Fetching Data
func fetchUsers() -> [NSManagedObject] { // Or [User] if using subclasses
// Create a fetch request
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "User")
// For subclasses: let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
// Optional: Add sort descriptors or predicates (filters)
// let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
// fetchRequest.sortDescriptors = [sortDescriptor]
// let predicate = NSPredicate(format: "name CONTAINS[c] %@", "John")
// fetchRequest.predicate = predicate
do {
let users = try context.fetch(fetchRequest)
for user in users {
if let userName = user.value(forKey: "name") as? String {
print("Fetched user: \(userName)")
}
}
return users
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
return []
}
}
// Example Usage (e.g., in viewDidLoad or an IBAction)
// saveNewUser(name: "Alice")
// let allUsers = fetchUsers()

Core Data involves more setup but is very powerful for complex data models and large datasets. The do-catch blocks are essential for error handling.

Managing the on-screen keyboard is important for a good user experience.

Dismiss Keyboard on Tap Outside Text Field

Section titled “Dismiss Keyboard on Tap Outside Text Field”

Add this to your UIViewController subclass:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true) // Dismisses keyboard for any active text field in the view
}
  1. Make your View Controller conform to UITextFieldDelegate: class ViewController: UIViewController, UITextFieldDelegate { ... }
  2. In viewDidLoad(), set the delegate for your text field(s): myTextField.delegate = self (where myTextField is an outlet to your UITextField)
  3. Implement the delegate method:
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder() // Dismisses the keyboard for this specific text field
return true // Indicates the return key press was handled
}

To display web content within your app, use WKWebView (from the WebKit framework). UIWebView is deprecated.

  1. Add WKWebView to your View: In Storyboard, search for ‘Web Kit View’ in the Object Library and drag it onto your view. Constrain it as needed. Create an outlet for it (e.g., webView).
  2. Import WebKit: import WebKit in your ViewController.swift.
  3. Load a URL:
import UIKit
import WebKit
class WebViewController: UIViewController, WKNavigationDelegate {
@IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView.navigationDelegate = self // Optional, for handling navigation events
guard let url = URL(string: "https://www.apple.com") else { return }
let request = URLRequest(url: url)
webView.load(request)
}
// Example WKNavigationDelegate methods (optional)
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
print("Started to load")
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("Finished loading")
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print("Failed to load: \(error.localizedDescription)")
}
}

Remember App Transport Security (ATS): If loading non-HTTPS URLs, you’ll need to configure exceptions in your Info.plist (e.g., NSAllowsArbitraryLoads or domain-specific exceptions), though HTTPS is strongly preferred.

These advanced topics provide a strong foundation for building complex and interactive iOS applications. Continue exploring Apple’s documentation and experimenting with these features.