Chapters

Hide chapters

SwiftUI Apprentice

First Edition · iOS 14 · Swift 5.4 · Xcode 12.5

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 and, finally, how to Delete it. That will leave Create, and you’ll learn how to add new cards with photos and text in a later chapter.

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 a new operator to multiply a CGSize by a scalar.
  • Preview Assets.xcassets: contains three cute hedgehogs from http://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 are 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
Keqs oz pyu sijcoh mfuhdk

Data structure
Dope zsdimqiyo

Value and reference types

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

Value and reference types
Wiyoa iym hayohopvi ldxuj

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
Xukzifizouw

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 = "San Fransisco"
}

Creating a default protocol method

Part of a protocol blueprint might be requiring 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: didSet property observer; mutability

@EnvironmentObject var store: CardStore
.environmentObject(CardStore(defaultData: true))
ForEach(store.cards) { card in
let card: Card
.foregroundColor(card.backgroundColor)
struct CardThumbnailView_Previews: PreviewProvider {
  static var previews: some View {
    CardThumbnailView(card: initialCards[0])
  }
}
CardThumbnailView(card: card)
The card thumbnails
Blo yisg lbewmqoujv

Choosing a card

When you tap a card, you toggle viewState.showAllCards, and the parent view, CardsView, should show SingleCardView using the data for the selected card. Rather than pass bindings around for the selected card, you’ll hold it in ViewState.

var selectedCard: Card?
viewState.selectedCard = card

The didSet observer

Of course, when you are listing all cards and no card is currently selected, viewState.selectedCard should be nil. You control display of this list with viewState.showAllCards. You could go and hunt down all the places that you set showAllCards true, but you can use the property observer didSet instead.

@Published var showAllCards = true {
  didSet {
    if showAllCards {
      selectedCard = nil
    }
  }
}

Displaying the single card

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

if let selectedCard = viewState.selectedCard {
  NavigationView {
    ...
  }
  .navigationViewStyle(StackNavigationViewStyle())
}
CardDetailView(card: selectedCard)
func index(for card: Card) -> Int? {
  cards.firstIndex { $0.id == card.id }
}
@EnvironmentObject var store: CardStore
.environmentObject(CardStore(defaultData: true))
if let selectedCard = viewState.selectedCard,
  let index = store.index(for: selectedCard) {
CardDetailView(card: $store.cards[index])
@Binding var card: Card
CardDetailView(card: .constant(initialCards[0]))
A working app
U yechalw udj

Convenience initializer

➤ Open SingleCardView.swift.

convenience init(card: Card) {
  self.init()
  showAllCards = false
  selectedCard = card
}
.environmentObject(ViewState(card: initialCards[0]))
Card conveniently initialized
Zurw sochugaeqfwv ocasiefohuc

Adding elements to the card detail view

With the card passed to CardDetailView, you can now show the card’s elements.

var content: some View {
  ZStack {
    card.backgroundColor
      .edgesIgnoringSafeArea(.all)
  }
}
Card background from the preview data
Devt gibvbyeift vnaj dho klipoik xidi

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)
    }
  }
}
struct CardElementView_Previews: PreviewProvider {
  static var previews: some View {
    CardElementView(element: initialElements[0])
  }
}
The card element view
Ffu detj atowezs geer

Showing the card elements

➤ Open CardDetailView.swift, locate var content and add this after .edgesIgnoringSafeArea(.all):

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
Hxa kevm ubilojhf

Understanding @State and @Binding property wrappers

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

CardDetailView declarations
WustSugeebJoes hajlociroiyf

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 extract a binding transform from the immutable card element in CardDetailView. You’ll pass this transform to resizableView(), and ResizableView will connect to this binding instead of updating its own internal state transform property.

// 1
func bindingTransform(for element: CardElement)
  -> Binding<Transform> {
  // 2
  guard let index = element.index(in: card.elements) else {
    fatalError("Element does not exist")
  }
  // 3
  return $card.elements[index].transform
}
.resizableView(transform: bindingTransform(for: element))
@Binding var transform: Transform
.resizableView(transform: .constant(Transform()))
func resizableView(transform: Binding<Transform>) -> some View {
  return modifier(ResizableView(transform: transform))
}

Updating CardDetailView preview

The live preview in CardDetailView will no longer allow you to move or resize elements.

struct CardDetailView_Previews: PreviewProvider {
  struct CardDetailPreview: View {
    @State private var card = initialCards[0]
    var body: some View {
      CardDetailView(card: $card)
        .environmentObject(ViewState(card: card))
    }
  }

  static var previews: some View {
    CardDetailPreview()
  }
}
Elements in their correct position
Ofaraxqs ir lxaox docduwq jivewaey

.onAppear {
  previousOffset = transform.offset
}
Updating the card
Ujneyimk mxu navq

Deletion

Skills you’ll learn in this section: context menu; deletion; remove from array

func remove(_ element: CardElement) {
  if let index = element.index(in: elements) {
    elements.remove(at: index)
  }
}
mutating func remove(_ element: CardElement) {
.contextMenu {
  Button(action: { card.remove(element) }) {
    Label("Delete", systemImage: "trash")
  }
}
Delete card element
Qaqayu wohr ebalirk

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 so that you can delete a card.

Key points

  • Use value types in your app almost exclusively. However, use a reference type for persistent stored data. When you create a value type, you are always copying the data. Your stored data should be in one central place in your app, so you should not copy it. Occasionally, Apple’s APIs will require you to use a class, so you have no choice.
  • 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 get familiar with them.
  • When designing an app, consider how you’ll implement CRUD. In this chapter, you implemented Read, Update and Delete. Adding new data is always more difficult as you generally need a special button and, possibly, a special view. You’ll add photos from your photos collection to your cards later on.

Where to go from here?

You covered a lot of Swift theory in this chapter. The book Swift Apprentice https://bit.ly/3eFtqQa by our team 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