Chapters

Hide chapters

SwiftUI Apprentice

Second Edition · iOS 16 · Swift 5.7 · Xcode 14.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. The full image might be located in the cloud, and you have no control over the quality of the user’s internet connection.

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 in the background. When the photo is fully downloaded, your app will then add the photo to the card.

This all takes an indeterminate amount of time that depends on internet availability and connection. Whenever a task isn’t straightforward, you should perform it asynchronously, so you don’t hold up the main thread. You’ll learn more about asynchronous operations in Section III, but you’ll have a brief encounter with them here when you load photos.

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)
    }
  }
}
PhotosModal(card: .constant(Card()))
PhotosPicker view button
LqobecQidlog fueb wotzoz

The system photos picker
Gto xhqpoj jzokof wezyaj

Adding the Photos Picker to Your App

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

case .photoModal:
  PhotosModal(card: $card)
Two Photos buttons
Tbe Jdamen fubvogb

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
Rwe mfvraj tciloz nukbuy

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
Goqrugu eikjac

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
.tgq ixh yegy

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)")
}
Photos added to the card
Qvavew ivcez ji pyi jepg

Adding Photos to Simulator

If you want more photos 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
Mzlim Qiad

Cards and Safari in Split View
Pezkh efd Tugoce ur Wgcoz Zaor

Drag a giraffe
Dnin e nejaqso

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
Jjoh ag iqxoze

A tower of giraffes
A qurov ej zoqolsap

Conforming Types to Transferable

As mentioned earlier, UIImage doesn’t conform to Transferable, so you can’t currently use it as a transferable type when adding photos or during drag and drop. To conform a type to Transferable, you describe the representation of the data.

import SwiftUI
// 1
extension UIImage: Transferable {
  // 2
  public static var transferRepresentation: some TransferRepresentation {
    // 3
    DataRepresentation(importedContentType: .image) { image in
      // 4
      UIImage(data: image) ?? errorImage
    }
  }

  public static var errorImage: UIImage {
    UIImage(named: "error-image") ?? UIImage()
  }
}

Updating the Drag and Drop

You can now import dropped photos using UIImage instead of Data. This makes your code correspond more closely to your intent. When you see the word “data”, it’s not always obvious what type that data is.

.dropDestination(for: UIImage.self) { images, location in
  print(location)
  for image in images {
    card.addElement(uiImage: image)
  }
  return !images.isEmpty
}

Drag and Drop of Custom Types

You can now drag in images from another app, but you also want to drag in text. You can try this naively and see how it will work.

mutating func addElement(text: TextElement) {
  elements.append(text)
}
.dropDestination(for: String.self) { strings, _ in
  for text in strings {
    card.addElement(text: TextElement(text: text))
  }
  return !strings.isEmpty
}
Dropped text
Nkayzoc socr

import SwiftUI

struct CustomTransfer: Transferable {
  var image: UIImage?
  var text: String?

  public static var transferRepresentation: some TransferRepresentation {
    DataRepresentation(importedContentType: .image) { data in
      let image = UIImage(data: data)
        ?? UIImage(named: "error-image")
      return CustomTransfer(image: image)
    }
    DataRepresentation(importedContentType: .text) { data in
      let text = String(decoding: data, as: UTF8.self)
      return CustomTransfer(text: 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 {
    card.addElements(from: items)
  }
  return !items.isEmpty
}
Drag and drop text and image
Yraq oyr pgik kulp iqr eleci

Pasting From Another App

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

ToolbarItem(placement: .navigationBarLeading) {
  PasteButton(payloadType: CustomTransfer.self) { items in
    Task {
      card.addElements(from: items)
    }
  }
}
Copy an image
Jehc uy obiqe

Several paste operations
Falewur fidre axaficaakh

.labelStyle(.iconOnly)
.buttonBorderShape(.capsule)
Styled paste button
Kxbdos rubno wenjij

Adding a Pop-up Menu

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

ToolbarItem(placement: .navigationBarTrailing) {
  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
Unsiqwug mig-uz xalo

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
Imbav kehpu

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
Xorf Jomrr uwivuqqc xe Mihab

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
Joluxo an olilosg

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
Xajoyi a gazs

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 use Transferable for drag and drop, pasting and sharing. When you have a custom object, you can define custom Transferable objects to transfer between apps.
  • 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.
© 2024 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