BookManager
In this lab we are going to build a slight variation of the BookManager lab from 67-272.  However, at this stage, we don't have access to a database yet so we will be drawing data through an array of records we'll provide.  But it will allow us to use some mobile UI elements we discussed last year, in addition to a form to add records to the array and create some charts to analyze our book records.
When we are finished, we'll have an app like this:
 
Again, this lab can be completed on a Mac with Xcode or on an iPad with the Playgrounds app.  Unfortunately, this is a new lab and there is no Android version yet.
Part 0: Creating tab bar navigation
- 
Create a new app called BookManager that uses SwiftUI as we did last week.  In the main BookManagerAppfile, changeContentView()toAppView(). Create three folders --Views,Models, andViewModels.  In theViewsfolder, create four new files --AppView.swift,LibraryView.swift,NewBookView.swift, andChartsView.swift.  Copy the contents ofContentView.swiftinto the latter three files, adjusting the class names to match the file names. (LeaveAppViewuntouched.)  Once you done this, delete theContentView.swiftfile.
 
- 
Inside the AppView.swiftfile we are going to build our tabs.  The following code will get us started:
 
import SwiftUI
struct AppView: View {
    var body: some View {
      TabView {
        LibraryView()
        .tabItem {
          Image(systemName: "books.vertical")
          Text("Library")
        }
      }
    }
  }
struct AppView_Previews: PreviewProvider {
    static var previews: some View {
      AppView()
    }
  }
If we run this code now, we should see the following:
<img src="https://i.imgur.com/G7Aond2.png" width="30%"/>
- 
Now go ahead and add two more tabs, one with a text of "New Book" and an image of rectangle.stack.badge.plusand another tab with a text of "Charts" and an image ofchart.bar.xaxis.  If you want to choose different icons, you can do that; check out Apple's SF Symbols app for help with this.  Rerunning and flipping through the tabs, you should see this:
   
Part 1: Creating models
- We are going to be tracking the gender of the authors of our books, so to make that easier, we'll create an enum called Gender.swiftin theModelsfolder.  Add to that file the following code:
  enum Gender: String {
    case male = "Male"
    case female = "Female"
    case other = "Other"
    static let allGenders = ["Male", "Female", "Other"]
  }
- 
Now create a Bookstruct inside theModelsfolder. The struct should haveStringproperties fortitle,author, andgender, as well as a boolean property calleddisplayed.
 
- 
Of course, we need an init()method for this struct, which we will do with the following:
 
  init(title: String, author: String, gender: String, displayed: Bool) {
    self.title = title
    self.author = author
    self.gender = gender
    self.displayed = displayed
  }
- Since we are going to read the data from a dictionary later, rather than a database, there is no ID for these records that allow us to uniquely identify them.  In cases like this, we'd like to make this struct conform to Swift's Identifiableprotocol.  We will discuss this in class soon, but for now, doing this is really easy.  First, edit the first line of the struct to include the protocol as follows:
  struct Book: Identifiable {
Next, add the following line to the properties list created in the prior step to associate a Universally Unique ID for each object created:
  var id = UUID()    // To conform to Identifialbe protocol
- The other challenge with the struct is how do we compare objects build with it?  How do we sort them?  If we conform to the Comparableprotocol, we could solve these issues.  To make that happen, add this protocol to the struct declaration as follows:
  struct Book: Identifiable, Comparable {
Now to implement this protocol, we need a method to demonstrate equality.  We can do that with the following method that will declare an object identical if both the title and author strings match:
  static func ==(lhs: Book, rhs: Book) -> Bool {
    return lhs.title == rhs.title && lhs.author == rhs.author
  }
Next we need a method to determine ordering of object of type Book.  The following method orders objects alphabetically based on title:
  static func <(lhs: Book, rhs: Book) -> Bool {
    return lhs.title < rhs.title
  }
Part 2: Creating a library
- 
We have a Bookmodel which describes a book object and adds some basic protocols to extend functionality, but now we need a class (not struct) for a collection of books, which we will callLibrary; create that file now and make the class anObservableObject.
 
- 
Within the class we will declare a published array property for booksand then create aninit()method that will read in 77 classic books from a dictionary and populate thatbooksarray.  This code will do that:
 
    @Published var books: [Book] = []
    init() {
      let bookData = [
              "The Count of Monte Cristo":["Alexandre Dumas","Male"],
        "The Man in the Iron Mask":["Alexandre Dumas","Male"],
        "The Three Musketeers":["Alexandre Dumas","Male"],
        "Black Beauty":["Anna Sewell","Female"],
        "The Tenant of Wildfell Hall":["Anne Bronte","Female"],
        "The Prisoner of Zenda":["Anthony Hope","Male"],
        "Adventures of Sherlock Holmes":["Arthur Conan Doyle","Male"],
        "The Hound of the Baskervilles":["Arthur Conan Doyle","Male"],
        "Dracula":["Bram Stoker","Male"],
        "A Christmas Carol":["Charles Dickens","Male"],
        "A Tale of Two Cities":["Charles Dickens","Male"],
        "David Copperfield":["Charles Dickens","Male"],
        "Great Expectations":["Charles Dickens","Male"],
        "Hard Times":["Charles Dickens","Male"],
        "Nicholas Nickleby":["Charles Dickens","Male"],
        "Oliver Twist":["Charles Dickens","Male"],
        "The Pickwick Papers":["Charles Dickens","Male"],
        "Jane Eyre":["Charlotte Bronte","Female"],
        "Shirley":["Charlotte Bronte","Female"],
        "The Professor":["Charlotte Bronte","Female"],
        "Villette":["Charlotte Bronte","Female"],
        "The Age of Innocence":["Edith Wharton","Female"],
        "Wuthering Heights":["Emily Bronte","Female"],
        "Crime and Punishment":["Fyodor Dostoyevsky","Male"],
        "The Brothers Karamazov":["Fyodor Dostoyevsky","Male"],
        "The Phantom of the Opera":["Gaston Leroux","Male"],
        "1984":["George Orwell","Male"],
        "Animal Farm":["George Orwell","Male"],
        "To Kill A Mockingbird":["Harper Lee","Female"],
        "Uncle Tom's Cabin":["Harriet Beecher Stowe","Female"],
        "Walden":["Henry David Thoreau","Male"],
        "Last of the Mohicans":["James Fenimore Cooper","Male"],
        "The Deerslayer":["James Fenimore Cooper","Male"],
        "Emma":["Jane Austen","Female"],
        "Mansfield Park":["Jane Austen","Female"],
        "Northanger Abbey":["Jane Austen","Female"],
        "Persuasion":["Jane Austen","Female"],
        "Pride and Prejudice":["Jane Austen","Female"],
        "Sense and Sensibility":["Jane Austen","Female"],
        "Pilgrim's Progress":["John Bunyan","Male"],
        "Of Mice and Men":["John Steinbeck","Male"],
        "Gulliver's Travels":["Jonathan Swift","Male"],
        "Around the World in Eighty Days":["Jules Verne","Male"],
        "Journey to the Center of the Earth":["Jules Verne","Male"],
        "The Awakening":["Kate Chopin","Female"],
        "Anna Karenina":["Leo Tolstoy","Male"],
        "War and Peace":["Leo Tolstoy","Male"],
        "Alice's Adventures in Wonderland":["Lewis Carroll","Male"],
        "Through The Looking Glass":["Lewis Carroll","Male"],
        "Little Women":["Louisa May Alcott","Female"],
        "Frankenstein":["Mary Shelley","Female"],
        "The Scarlet Letter":["Nathaniel Hawthorne","Male"],
        "Fahrenheit 451":["Ray Bradbury","Male"],
        "Kidnapped":["Robert Louis Stevenson","Male"],
        "The Strange Case of Dr Jekyll and Mr Hyde":["Robert Louis Stevenson","Male"],
        "Treasure Island":["Robert Louis Stevenson","Male"],
        "The Red Badge of Courage":["Stephen Crane","Male"],
        "Rights of Man":["Thomas Paine","Male"],
        "Les Miserables":["Victor Hugo","Male"],
        "A Midsummer Night's Dream":["William Shakespeare","Male"],
        "Hamlet":["William Shakespeare","Male"],
        "Henry V":["William Shakespeare","Male"],
        "Julius Caesar":["William Shakespeare","Male"],
        "King Lear":["William Shakespeare","Male"],
        "Macbeth":["William Shakespeare","Male"],
        "Much Ado About Nothing":["William Shakespeare","Male"],
        "Othello":["William Shakespeare","Male"],
        "Romeo and Juliet":["William Shakespeare","Male"],
        "The Comedy of Errors":["William Shakespeare","Male"],
        "The Merchant of Venice":["William Shakespeare","Male"],
        "The Taming of the Shrew":["William Shakespeare","Male"],
        "The Tempest":["William Shakespeare","Male"],
        "Twelfth Night":["William Shakespeare","Male"],
        "The Hobbit or There and Back Again":["J.R.R. Tolkien","Male"],
        "The Fellowship of the Ring":["J.R.R. Tolkien","Male"],
        "The Two Towers":["J.R.R. Tolkien","Male"],
        "The Return of the King":["J.R.R. Tolkien","Male"]
      ]
  
      for (title, authData) in bookData {
        self.books.append(Book(title: title, author: authData[0], gender: authData[1], displayed: true))
        self.books = self.books.sorted()
      }
    }
The nice thing is that since books are Comparable, we can run the Swift method sorted() and get our books in alphabetical order by title, rather than some random order.
- We want this class to have a method to add a new book to the library.  The following method would accomplish this:
  // MARK: Adding books
  func addBookToLibrary(title: String, author: String, gender: String, displayed: Bool) {
    self.books.append(Book(title: title, author: author, gender: gender, displayed: displayed))
  }
- We also need some methods to extract subsets of our library. We will cover functional programming soon, but as a preview, Swift has a method called filterthat behaves likeselectin Ruby.  We can use it here to write a method to get all the books for a particular author:
  // MARK: Parsing methods
  func getBooksFor(_ author: String) -> [Book] {
    return self.books.filter { $0.author == author }
  }
Now write two more methods, one called getFemaleAuthoredBooks() which will return a list of books for all the books in the library written by women.  After that, write a similar method called getMaleAuthoredBooks().  We will need all three of these methods later in our charting section.
- The thing is that this is much more like a model than a controller, yet we'd like to create an instance of this class and pass it between our various tabs.  Let's move this file to the ViewModelsfolder so it's properly recognized for what it is.  Second, openAppViewand before we declare thebodyvariable, let's add the linevar library = Library().  Now we'd like to add to theTabViewanenvironmentObjectand put thelibraryobject there that all the tabs can share:
      }   // THIS BRACE CLOSES OFF THE TAB VIEW 
      .environmentObject(library)
    }
  }
Part 3: Building out the library view
- 
Let's build out the table view in LibraryViewfirst. Before thebodyvariable in that file, we want to add the environment object with the line@EnvironmentObject var library: Library.
 
- 
To make a simple list, we can just use the Listobject as follows:
 
   List(library.books) { book in
     Text(book.title)
       .fontWeight(.bold)
       .font(.body)
    }
If we add this code inside the body and then run it, we should get the following:
   
This is okay, but what if we wanted more information about the book (like author and such)?  We need a little more than a simple list -- as we saw last week with RailsCards, it'd be nice to navigate to a details view and then pop back to the list later.
- To make this desire happen, let's first create a simple BookDetailViewin theViewsfolder with the following code:
  import SwiftUI
  struct BookDetailsView: View {
    var book: Book
    var body: some View {
      Text("Qapla'")
     }
  }
We will clean this up later, but we just need something for now so the compiler won't gripe.
- Now we want to go back to LibraryViewand replacebodywith the following:
  var body: some View {
    NavigationView {
      List(library.books) { book in
        NavigationLink(
          destination: BookDetailsView(book: book),
          label: {
            Text(book.title)
              .fontWeight(.bold)
              .font(.body)
          })
      }.navigationBarTitle("Library")
    }
  }
Run this code and see that it works and you can navigate to a details view (which just says "Qapla'" right now for every book).
- This works, but is the code easy to read?  Look at the code below and see if it's not just a little easier to understand.
    NavigationView {
      List(library.books) { book in
        BookRowView(book: book)
      }.navigationBarTitle("Library")
    }
The library view is just making a list of book rows and if we want to learn more about book rows, we can go look there.  This is much like the partials we discussed again and again in 67-272.
- Of course, we need a BookRowView, but this is just a view with the code we deleted above added tobody:
      NavigationLink(
        destination: BookDetailsView(book: book),
        label: {
          Text(book.title)
            .fontWeight(.bold)
            .font(.body)
       })
The only gotcha is that we can't forget to add var book: Book just prior to body. (Don't worry -- if you forget, the compiler will let you know...)
- 
Now go back and create a book details view which has the book title, book author, and author's gender listed.  The view should use different fonts and/or colors to display this information.  Also, do NOT center it (as it would be default), but push this information to the top of the screen. 
- 
One last thing is that we'd like to swipe to the left of the table row and remove a book (it's a gesture we are used to in the mobile space).  As Paul Hudson explains in this article, Swift makes this easy with an onDelete()method, but we will have to rewrite theListto useForEach.  The following code would work:
 
    NavigationView {
      List{
        ForEach(library.books) { book in
          BookRowView(book: book)
        }.onDelete(perform: removeRows)
      }.navigationBarTitle("Library")
    }
Of course, you will need Paul's removeRows() method provided in the article.  His method will work if you replace his numbers with our equivalent here; instead of numbers, we have a list of library books  (hint, hint, wink, wink.)
Part 4: Building a new book form
- 
In our NewBookViewstart by adding@EnvironmentObject var library: Libraryas we did in the prior section.
 
- 
Add the following properties: 
  @State private var title = ""
  @State private var author = ""
  @State private var gender = "Male"
  @State private var displayed = false
- Replace the contents inside bodywith:
  VStack {
    Text("New Book")
      .font(.title)
      .fontWeight(.bold)
  }    
- The next element in our VStackis going to be aForm.  We'll start with just the two text fields for title and author:
  Form {
    TextField("Title", text: $title)
    TextField("Author", text: $author)
  }
Running this, you should see:
   
- Within Formwe need to add aPickerthat will allow us to choose the author gender:
    Picker(selection: $gender,
           label: Text("Author Gender")) {
      ForEach(Gender.allGenders, id: \.self) { gender in
           Text(gender).tag(gender)
      }
    }
Running this, you should see:
   
- Within Formwe need to add aTooglethat will allow us to choose whether to display. (In truth, this field only exists right now for us to do a toggle control and we don't use it.)
    Toggle(isOn: $displayed,
           label: {
             Text("Display book in library")
    })
Running this, you should see:
   
When we add in Anne Frank's "Diary of a Young Girl", we have a problem -- everything is there, but we can't add it.  Oh dear.
- Luckily this is easily overcome by adding a Buttonthat uses theaddBookToLibrary()method written earlier:
    Button("Add Book") {
      library.addBookToLibrary(title: title, author: author, gender: gender, displayed: displayed)
    }
Running this, you should see that you can add Anne Frank's book now:
   
- Note that the book was added to the end of the list -- we could correct this.  Likewise, we want the button to only be visible if the minimum is present (title and author).  Finally, we'd like to clear out the form fields after submission and give a notice that it was a success.  This lab is already long, so this is left as an exercise on your own and not required to get credit.
Part 5: Building out some simple charts
SwiftUI has the ability to make some basic charts and it would be good to have some familiarity with that.  The Charts library is more recent (summer 2022), so the documentation on this is still not fully developed.  Hopefully this brief exercise will help.
- 
In the ChartsViewfile, let's add in our@EnvironmentObjectas we did in the previous views.  We also need to importChartsin addition toSwiftUIat the top of the file.
 
- 
Our first chart will be a simple bar chart counting the books by author gender.  We'll use a VStackto first create a title ("Books by Author Gender").  We'll use those two methods you wrote earlier to build the chart.  Here's some code that would work if added to theVStack:
 
      Chart {
      BarMark(
        x: .value("Mount", "Male"),
        y: .value("Value", library.getMaleAuthoredBooks().count)
      )
      BarMark(
        x: .value("Mount", "Female"),
        y: .value("Value", library.getFemaleAuthoredBooks().count)
      )
    }
    .frame(height: 250)
    .padding(20)
    
    Spacer()  // To push the chart to the top instead of centering
Running this should give you the following:
   
- We are going to add the counts for some of the most popular authors.  We will hand-pick these now (we could write methods to dynamically generate the list).  Take what you learned in the graph above and pick 5 authors (two must be William Shakespeare and Jane Austen, but you can choose any others for the remaining 3; I did J.R.R. Tolkein, Charles Dickens, and Charlotte Bronte as my others, but do as you wish).  Use the getBooksFor(_ author: String)function we wrote inLibraryto get these authors' counts.  Also, change the color to green by adding at the end.foregroundColor(Color.green)after the padding. You should see something like this:
 
I know we've covered a lot of ground, but hopefully this gets you familiar with some basic UI elements and prepares you for future labs.  In our next lab, we will draw on data from an external API to power our application, which should be fun.  Qapla'