Chapters

Hide chapters

SwiftUI Apprentice

Third Edition · iOS 18 · Swift 5.9 · Xcode 16.2

Section I: Your First App: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your Second App: Cards

Section 2: 9 chapters
Show chapters Hide chapters

17. Adding Photos to Your App
Written by Caroline Begbie

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the previous chapter, you learned how to add stickers to your card. These stickers were images provided to the app by you and your designers. Your users will want to add their own images to their cards, so in this chapter, you’ll learn how to add the user’s photos to your card and how to drag images from other apps, such as Safari.

The PhotosUI Framework

With the stickers, you load the sticker images lazily, and when the user selects one, you use that one image. This selected image is already loaded at the time of selection, so you just add it to the card.

Loading photos is not as simple as loading stickers, because the user’s media library might number in the tens of thousand of assets, and the user might select multiple photos. The full selected images might be located in the cloud, and you have no control over the quality of the user’s internet connection.

Whenever a task takes an indeterminate amount of time, you should perform it asynchronously, so you don’t hold up the main thread. You’ll learn more about asynchronous operations in Section 3, but you’ll have a brief encounter with them here when you load photos.

The PhotosUI framework provides a PhotosPicker view that will display the user’s media assets. The user then selects photos, and each selected item goes into an array. As the item is added to the array, the picker downloads the full photo file on a background thread. When the photo is fully downloaded, your app will then add the photo to the card on the main thread.

The PhotosPicker View

Skills you’ll learn in this section: PhotosPicker

import PhotosUI
struct PhotosModal: View {
  @Binding var card: Card
  // 1
  @State private var selectedItems: [PhotosPickerItem] = []

  var body: some View {
    // 2
    PhotosPicker(
      // 3
      selection: $selectedItems,
      // 4
      matching: .images) {
      // 5
        ToolbarButton(modal: .photoModal)
    }
  }
}
#Preview {
  PhotosModal(card: .constant(Card()))
}
PhotosPicker view button
YredacSiklaj foub lalfad

The system photos picker
Kye pngsir qzedew riqliv

Adding the Photos Picker to Your App

➤ Open CardToolbar.swift, and locate .sheet(item: $currentModal).

case .photoModal:
  PhotosModal(card: $card)
Two Photos buttons
Mwe Zdeceb zepmijn

switch selection {
default:
case .photoModal:
  Button {
  } label: {
    PhotosModal(card: $card)
  }
@Binding var card: Card
BottomToolbar(
  card: .constant(Card()),
  modal: .constant(.stickerModal))
case .photoModal:
  PhotosModal(card: $card)
BottomToolbar(
  card: $card,
  modal: $currentModal)
The system photos picker
Zya txqjiz vpifug wognus

The Transferable Protocol

Skills you’ll learn in this section: Transferable; Uniform Type Identifiers; add photos to Simulator

.onChange(of: selectedItems) { _, items in
  for item in items {
    print(item)
  }
  selectedItems = []
}
Console output
Xavtoli uupkaf

Uniform Type Identifiers

Uniform Type Identifiers, or UTIs, identify file types. For example, JPG is a standard UTI, with the identifier public.jpeg. It’s a subtype of the base image data type public.image.

.png app list
.rhd awz woxk

extension UTType {
  static var myType: UTType =
    { UTType(exportedAs: "com.kodeco.myType") }
}

Adding Photos to Your App

➤ Still in PhotosModal.swift, in the for loop, replace print(item) with:

item.loadTransferable(type: Data.self) { result in
  Task {
  // create a UIImage
  }
}
switch result {
case .success(let data):
  if let data,
    let uiImage = UIImage(data: data) {
    card.addElement(uiImage: uiImage)
  }
case .failure(let failure):
  fatalError("Image transfer failed: \(failure)")
}
Swift 6 compile warning
Xginc 8 wotjelo jelzuvw

await MainActor.run {
  card.addElement(uiImage: uiImage)
}
Photos added to the card
Sbetin opxuw ze jka zixd

Adding Photos to Simulator

You can test your app fully in Simulator or on your device. If you want more photos in Simulator than the ones Apple supplies, you can simply drag and drop your photos from Finder into Simulator. Simulator will place these into the Photos library and you can then access them in the photos picker.

Drag and Drop From Other Apps

Skills you’ll learn in this section: Split view; drag and drop; data representation

Split View
Bhhoy Kioy

Cards and Safari in Split View
Xamkh uqt Ciguru ab Htnaf Tiur

Drag a giraffe
Lwef i gemaqwu

Adding the Dropped Item to Your App

➤ Open CardDetailView.swift and, in body, add this modifier to ZStack:

.dropDestination(for: Data.self) { receivedData, location in
  print(location)
  for data in receivedData {
    if let image = UIImage(data: data) {
      card.addElement(uiImage: image)
    }
  }
  return !receivedData.isEmpty
}
Drop is active
Lloc id uvxuru

Selecting multiple images
Geniyjifj gumguwhu ugiyow

A tower of giraffes
O wecol if fugezkat

Conforming Types to Transferable

When you conform a type to Transferable, you describe the representation of the data. You have full control over what you can transfer and how you transfer it. You can describe types that already exist or types that you have created.

import SwiftUI

// 1
struct CustomTransfer: Transferable {
// 2
  var image: UIImage?
  var text: String?

// 3
  public static var transferRepresentation: 
    some TransferRepresentation {
// 4
    DataRepresentation(importedContentType: .image) { data in
      let image = UIImage(data: data) ?? UIImage.error
      return CustomTransfer(image: image)
    }
 // 5
    DataRepresentation(importedContentType: .text) { data in
      return CustomTransfer(text: "Dragged Text")
    }
  }
}

Updating the Drag and Drop

You can now import both dropped photos and text. Once CustomTransfer has created either an image or the text from the transferred data, you’ll add an element to the card.

mutating func addElement(text: TextElement) {
  elements.append(text)
}
mutating func addElements(from transfer: [CustomTransfer]) {
  for element in transfer {
    if let text = element.text {
      addElement(text: TextElement(text: text))
    } else if let image = element.image {
      addElement(uiImage: image)
    }
  }
}
.dropDestination(for: CustomTransfer.self) { items, location in
  print(location)
  Task {
    await MainActor.run {
      card.addElements(from: items)
    }
  }
  return !items.isEmpty
}
Drag and drop text and images
Lqod unp lmom yoxl awx acomep

Dropping an Attributed String

As you’ve seen, you can define how dragged-in data is processed. Currently you’re processing text to result in the constant “Dragged Text”.

let text = String(decoding: data, as: UTF8.self)
return CustomTransfer(text: text)
DataRepresentation(importedContentType: .text) { data in
  let docType = NSAttributedString.DocumentType.html
  let encoding = String.Encoding.utf8.rawValue
  guard let text = try? NSAttributedString(
    data: data,
    options: [
      .documentType: docType,
      .characterEncoding: encoding
    ],
    documentAttributes: nil
  ) else {
    return CustomTransfer(text: nil)
  }
  return CustomTransfer(text: text.string)
}
Dragged text from Safari and News
Rjinyiz litx xjaq Cicewa eqb Nuzv

Pasting From Another App

Skills you’ll learn in this section: Cut and paste

ToolbarItem(placement: .topBarLeading) {
  PasteButton(payloadType: CustomTransfer.self) { items in
    Task {
      await MainActor.run {
        card.addElements(from: items)
      }
    }
  }
}
Copy an image
Cekl ef ubaso

Several paste operations
Cuvutol xekki epejosiakq

.labelStyle(.iconOnly)
.buttonBorderShape(.capsule)
Styled paste button
Whvhep muxba larfax

Adding a Pop-up Menu

Skills you’ll learn in this section: Pop-up menu; context menu; UIPasteBoard; remove from array

ToolbarItem(placement: .topBarTrailing) {
  menu
}
var menu: some View {
  // 1
  Menu {
    Button {
      // add action here
    } label: {
      Label("Paste", systemImage: "doc.on.clipboard")
    }
    // 2
    .disabled(!UIPasteboard.general.hasImages
      && !UIPasteboard.general.hasStrings)
  } label: {
    Label("Add", systemImage: "ellipsis.circle")
  }
}
Ellipsis pop-up menu
Onnutjel qil-eg mizi

if UIPasteboard.general.hasImages {
  if let images = UIPasteboard.general.images {
    for image in images {
      card.addElement(uiImage: image)
    }
  }
} else if UIPasteboard.general.hasStrings {
  if let strings = UIPasteboard.general.strings {
    for text in strings {
      card.addElement(text: TextElement(text: text))
    }
  }
}
Allow paste
Ipxux wibva

Copying Elements

You can copy from other apps, so it makes sense to implement copying elements within your own app.

import SwiftUI

struct ElementContextMenu: ViewModifier {
  @Binding var card: Card
  @Binding var element: CardElement

  func body(content: Content) -> some View {
    content
  }
}
.contextMenu {
  Button {
    if let element = element as? TextElement {
      UIPasteboard.general.string = element.text
    } else if let element = element as? ImageElement,
      let image = element.uiImage {
        UIPasteboard.general.image = image
    }
  } label: {
    Label("Copy", systemImage: "doc.on.doc")
  }
}
extension View {
  func elementContextMenu(
    card: Binding<Card>,
    element: Binding<CardElement>
  ) -> some View {
    modifier(ElementContextMenu(
      card: card,
      element: element))
  }
}
.elementContextMenu(
  card: $card,
  element: $element)
Copy Cards elements to Notes
Cayx Tejjt ucihovlz vo Xeviz

Deletion

You can easily add elements to your cards by copying and pasting them in, but if you make a mistake, you aren’t able to remove the element. In Chapter 15, “Structures, Classes & Protocols”, you achieved both Read and Update in the CRUD functions. Next, you’ll take on Deletion.

mutating func remove(_ element: CardElement) {
  if let index = element.index(in: elements) {
    elements.remove(at: index)
  }
}
Button(role: .destructive) {
  card.remove(element)
} label: {
  Label("Delete", systemImage: "trash")
}
Delete an element
Hocewu as aqaduwy

Challenge

Challenge: Delete a Card

You learned how to delete a card element and remove it from the card elements array. In this challenge, you’ll add a context menu to each card in the card list so that you can delete a card.

Delete a card
Bajava e zugm

Key Points

  • Instead of having to implement your own photos picker view, Apple provides the PhotosUI framework with a PhotosPicker view. It’s an easy way to select photos and videos from the photo library.
  • Uniform Type Identifiers identify file types so the system can determine the difference between, for example, images and text.
  • The Transferable protocol allows you to define how to transfer objects between processes. You define a custom Transferable object for drag and drop, pasting and sharing.
  • A Menu is a list of Buttons. Each Button can have a role. By making the role destructive, the menu item will appear in red.
  • PasteButton is a simple way of adding a button to paste in any copied item. If you want a more customized approach, you can access UIPasteBoard to paste in items.
  • You can attach a context menu to a view and add buttons to it in the same way as to a Menu. You access the context menu by a long press. SwiftUI brings the view to the foreground and darkens the other views. If this behavior is not what you want, you’ll have to create your own custom menu.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now