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

15. Structures, Classes & Protocols
Written by Caroline Begbie

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

It’s time to build the data model for your app so you have some data to show on your app’s views.

The four functions that data models need are frequently referred to as CRUD. That’s Create, Read, Update, Delete. The easiest of these is generally Read, so in this chapter, you’ll first create the data store, then build views that read the store and show the data. You’ll then learn how to Update the data and store it. That will leave Create and Delete. You’ll learn how to add new cards with photos and text, then remove them, in later chapters.

Starter Project Changes

There are a few differences between the challenge project from the last chapter and the starter project of this chapter:

  • Operators.swift: contains new operators.
  • Preview Assets.xcassets: contains three cute hedgehogs from https://pexels.com.
  • PreviewData.swift: contains sample data that you’ll use until you’re able to create and save data.
  • TextExtensions.swift: contains a new view modifier to scale text.

➤ If you’re continuing with your own project, be sure to copy these files into your project.

Data Structure

Take another look at the back of the napkin sketch:

Back of the napkin sketch
Josd ob tno wegzix xtopbv

Data structure
Kuhe ckfafgisu

Value and Reference Types

Skills you’ll learn in this section: differences between value and reference types

Value and reference types
Gemou irh vezuralhi zwxaf

Swift Dive: Structure vs Class

Skills you’ll learn in this section: how to use structures and classes

let iAmAStruct = AStruct()
let iAmAClass = AClass()
iAmAStruct.number = 10  // compile error
iAmAClass.number  = 10  // no error - `number` will update to 10
let pointA = CGPoint(x: 10, y: 20)
var pointB = pointA    // make a copy
pointB.x = 20          // pointA.x is still 10
let iAmAClass = AClass()
let iAmAClassToo = iAmAClass
iAmAClassToo.number = 20     // this updates iAmAClass
print(iAmAClass.number)      // prints 20

Creating the Card Store

Skills you’ll learn in this section: when to use classes and structures

import SwiftUI

struct CardElement {
}
import SwiftUI

struct Card: Identifiable {
  let id = UUID()
  var backgroundColor: Color = .yellow
  var elements: [CardElement] = []
}
import SwiftUI

class CardStore: ObservableObject {
  @Published var cards: [Card] = []
}

Class Inheritance

Skills you’ll learn in this section: class inheritance; composition vs inheritance

class CardElement {
  var transform: Transform
}

class ImageElement: CardElement {
  var image: Image?
}

class TextElement: CardElement {
  var text: String?
}

Composition vs Inheritance

With inheritance, you have tightly coupled objects. Any subclass of a CardElement class automatically has a transform property whether you want one or not.

Composition
Xekboyavoeg

Protocols

Skills you’ll learn in this section: create protocol; conform structures to protocol; protocol method

protocol CardElement {
  var id: UUID { get }
  var transform: Transform { get set }
}
struct ImageElement: CardElement {
  let id = UUID()
  var transform = Transform()
  var image: Image
}
struct TextElement: CardElement {
  let id = UUID()
  var transform = Transform()
  var text = ""
  var textColor = Color.black
  var textFont = "Gill Sans"
}

Creating a Default Protocol Method

A protocol blueprint might require the conforming type to implement a method. For example, this protocol requires all types that conform to it to implement find():

protocol Findable {
  func find()
}
let index = card.elements.firstIndex { $0.id == element.id }
extension CardElement {
  func index(in array: [CardElement]) -> Int? {
    array.firstIndex { $0.id == id }
  }
}
let index = element.index(in: card.elements)

The Preview Data

Skills you’ll learn in this section: using preview data

init(defaultData: Bool = false) {
  if defaultData {
    cards = initialCards
  }
}
@StateObject var store = CardStore(defaultData: true)
.environmentObject(store)
@EnvironmentObject var store: CardStore
.environmentObject(CardStore(defaultData: true))

Listing the Cards

Skills you’ll learn in this section: observing full screen cover property

ForEach(store.cards) { card in
let card: Card
.foregroundColor(card.backgroundColor)
CardThumbnail(card: initialCards[0])
CardThumbnail(card: card)
The card thumbnails
Tka tity gvopwroasm

Choosing a Card

When you tap a card, you set isPresented to true, which triggers the full screen modal for the single card. SingleCardView should now use the data for the selected card.

@State private var selectedCard: Card?
selectedCard = card
.fullScreenCover(item: $selectedCard) { card in

Displaying the Single Card

You can now pass the selected card to the single card view.

SingleCardView(card: card)
let card: Card
SingleCardView(card: initialCards[0])
var content: some View {
  card.backgroundColor
}
Selected card passed
Juyizrik qavf wezzoh

Mutability

Skills you’ll learn in this section: mutability

func index(for card: Card) -> Int? {
  cards.firstIndex { $0.id == card.id }
}
if let index = store.index(for: card) {
  SingleCardView(card: $store.cards[index])
} else {
  fatalError("Unable to locate selected card")
}
@Binding var card: Card
SingleCardView(card: .constant(initialCards[0]))

Adding Elements to the Card

➤ In the Single Card Views group, create a new SwiftUI View file named CardDetailView.swift.

import SwiftUI

struct CardDetailView: View {
  // 1
  @EnvironmentObject var store: CardStore
  @Binding var card: Card

  var body: some View {
    // 2
    ZStack {
      card.backgroundColor
    }
  }
}

struct CardDetailView_Previews: PreviewProvider {
  static var previews: some View {
    // 3
    CardDetailView(card: .constant(initialCards[0]))
      .environmentObject(CardStore(defaultData: true))
  }
}
Card background from the preview data
Velc cidmtmoewr sdiw tzu wceqaor zedu

Creating the Card Element View

➤ In the Single Card Views group, create a new SwiftUI View file named CardElementView.swift. This view will show a single card element.

struct ImageElementView: View {
  let element: ImageElement

  var body: some View {
    element.image
      .resizable()
      .aspectRatio(contentMode: .fit)
  }
}
struct TextElementView: View {
  let element: TextElement

  var body: some View {
    if !element.text.isEmpty {
      Text(element.text)
        .font(.custom(element.textFont, size: 200))
        .foregroundColor(element.textColor)
        .scalableText()
    }
  }
}
struct CardElementView: View {
  let element: CardElement

  var body: some View {
    if let element = element as? ImageElement {
      ImageElementView(element: element)
    }
    if let element = element as? TextElement {
      TextElementView(element: element)
    }
  }
}
CardElementView(element: initialElements[0])
The card element view
Gwa nosv avarufv baic

Showing the Card Elements

➤ Open CardDetailView.swift and, in body, add this after card.backgroundColor:

ForEach($card.elements, id: \.id) { $element in
  CardElementView(element: element)
    .resizableView()
    .frame(
      width: element.transform.size.width,
      height: element.transform.size.height)
}
The card elements
Sga xonw ayededkb

CardDetailView(card: $card)
Move the card elements
Xame lti xufp eqayogbw

Understanding @State and @Binding Property Wrappers

Skills you’ll learn in this section: @State; binding; generics

CardDetailView declarations
BojjRaheipGuid tuzpomuxuabn

Swift Dive: A Very Brief Introduction to Generics

Swift is a strongly typed language, which means that Swift has to understand the exact type of everything you declare. Binding has a generic type parameter <Value>. A generic type doesn’t actually exist except as a placeholder. When you declare a binding, you associate the current type of binding that you are using. You replace the generic term <Value> with your type, as in the above example Binding<Card>.

var cards: [Card] = []
var cards: Array<Card> = []

Binding Transform Data

Now that you’ve seen how generics work when composing a binding declaration, you’ll be able to pass the element’s transform to resizableView(), and ResizableView will connect to this binding instead of updating its own internal state transform property.

@Binding var transform: Transform
func resizableView(transform: Binding<Transform>) -> some View {
  modifier(ResizableView(transform: transform))
}
.resizableView(transform: .constant(Transform()))
.resizableView(transform: $element.transform)
Elements in their correct position
Ekonukbq en tweej nixhetf nihawaeq

Updating the Previews

Due to the changes you just made, Live Previews in SingleCardView, CardDetailView and ResizableView will no longer allow you to move or resize elements.

struct CardDetailView_Previews: PreviewProvider {
  struct CardDetailPreview: View {
    @EnvironmentObject var store: CardStore

    var body: some View {
      CardDetailView(card: $store.cards[0])
    }
  }

  static var previews: some View {
    CardDetailPreview()
      .environmentObject(CardStore(defaultData: true))
  }
}
.onAppear {
  previousOffset = transform.offset
}
Updating the card
Etfuroxx xto hoym

Key Points

  • Use value types in your app almost exclusively. However, use a reference type for persistent stored data. Your stored data should be in one central place in your app.
  • When designing a data model, make it as flexible as possible, allowing for new features in future app releases.
  • Use protocols to describe data behavior. An alternative approach to what you did in this chapter would be to require that all resizable Views have a transform property. You could create a Transformable protocol with a transform requirement. Any resizable view must conform to this protocol.
  • You had a brief introduction to generics in this chapter. Generics are pervasive throughout Apple’s APIs and are part of why Swift is so flexible, even though it is strongly typed. Keep an eye out for where Apple uses generics so that you can gradually become familiar with them.
  • When designing an app, consider how you’ll implement CRUD. In this chapter, you implemented Read and Update. Adding new data is always more difficult as you generally need a special button and, possibly, a special view.

Where to Go From Here?

You covered a lot of Swift theory in this chapter. Our team’s book Swift Apprentice contains more information about how and when to use value and reference types. It also covers generics and protocol oriented programming.

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now