Preview | Home | Detail | Register |
---|---|---|---|
Preview | Storyboard | Set Delegate |
---|---|---|
extension NewBookTableViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == titleTextField {
return authorextField.becomeFirstResponder()
} else {
return textField.resignFirstResponder()
}
}
}
Sort | Storyboard |
---|---|
enum SortStyle {
case title
case author
case readme
}
class LibrayTableViewController: UITableViewController {
var dataSource: LibraryDataSource!
@IBOutlet var sortButtons: [UIBarButtonItem]!
@IBAction func sortByTitle(_ sender: UIBarButtonItem) {
dataSource.update(sortStyle: .title)
updateTintColors(tappedButton: sender)
}
@IBAction func sortByAuthor(_ sender: UIBarButtonItem) {
dataSource.update(sortStyle: .author)
updateTintColors(tappedButton: sender)
}
@IBAction func sortByReadMe(_ sender: UIBarButtonItem) {
dataSource.update(sortStyle: .readme)
updateTintColors(tappedButton: sender)
}
func updateTintColors(tappedButton: UIBarButtonItem) {
sortButtons.forEach { button in
button.tintColor = button == tappedButton
? button.customView?.tintColor : .secondaryLabel
}
}
func update(sortStyle: SortStyle, animatingDifferences: Bool = true) {
currentSortStyle = sortStyle
var newSnappshot = NSDiffableDataSourceSnapshot<Section, Book>()
newSnappshot.appendSections(Section.allCases)
let booksByReadMe: [Bool: [Book]] = Dictionary(grouping: Library.books, by: \.readMe)
for (readMe, books) in booksByReadMe {
var sortedBooks: [Book]
switch sortStyle {
case .title:
sortedBooks = books.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending }
case .author:
sortedBooks = books.sorted { $0.author.localizedCaseInsensitiveCompare($1.author) == .orderedAscending }
case .readme:
sortedBooks = books
}
newSnappshot.appendItems(sortedBooks, toSection: readMe ? .readMe : .finished)
}
newSnappshot.appendItems([Book.mockBook], toSection: .addNew)
apply(newSnappshot, animatingDifferences: animatingDifferences)
}
Sort |
---|
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = editButtonItem
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
guard let book = self.itemIdentifier(for: indexPath) else {
return
}
Library.delete(book: book)
update(sortStyle: currentSortStyle)
}
}
Move Item |
---|
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
guard sourceIndexPath != destinationIndexPath,
sourceIndexPath.section == destinationIndexPath.section,
let bookToMove = itemIdentifier(for: sourceIndexPath),
let bookAtDestination = itemIdentifier(for: destinationIndexPath) else {
apply(snapshot(), animatingDifferences: false)
return
}
Library.reorderBooks(bookToMove: bookToMove, bookAtDestination: bookAtDestination)
update(sortStyle: currentSortStyle, animatingDifferences: false)
}
static func reorderBooks(bookToMove: Book, bookAtDestination: Book) {
let destinationIndex = Library.books.firstIndex(of: bookAtDestination) ?? 0
books.removeAll(where: { $0.title == bookToMove.title })
books.insert(bookToMove, at: destinationIndex)
saveAllBooks()
}
Preview |
---|
class DetailTableViewController: UITableViewController {
@IBOutlet var reviewTextview: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
reviewTextview.addDoneButton()
}
}
extension UITextView {
func addDoneButton() {
let toolbar = UIToolbar()
toolbar.sizeToFit()
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(self.resignFirstResponder))
toolbar.items = [flexSpace, doneButton]
self.inputAccessoryView = toolbar
}
}
Preview |
---|
class DetailTableViewController: UITableViewController {
@IBAction func updateImage() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = UIImagePickerController.isSourceTypeAvailable(.camera) ? .camera : .photoLibrary
imagePicker.allowsEditing = true
present(imagePicker, animated: true)
}
}
extension DetailTableViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let selectedImage = info[.editedImage] as? UIImage else { return }
imageView.image = selectedImage
Library.saveImage(selectedImage, forBook: book)
dismiss(animated: true)
}
}
Light | Light | Dark | Dark |
---|---|---|---|
Assets | Global Tint |
---|---|
Set Size | Set Class |
---|---|
class LibraryHeaderView: UITableViewHeaderFooterView {
static let reuseIdentifier = "\(LibraryHeaderView.self)"
@IBOutlet var titleLabel: UILabel!
}
class LibrayTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "\(LibraryHeaderView.self)", bundle: nil), forHeaderFooterViewReuseIdentifier: LibraryHeaderView.reuseIdentifier)
}
}
import UIKit
class LibraryHeaderView: UITableViewHeaderFooterView {
static let reuseIdentifier = "\(LibraryHeaderView.self)"
@IBOutlet var titleLabel: UILabel!
}
enum SortStyle {
case title
case author
case readme
}
enum Section: String, CaseIterable {
case addNew
case readMe = "Read Me!"
case finished = "Fished"
}
class LibrayTableViewController: UITableViewController {
var dataSource: LibraryDataSource!
@IBOutlet var sortButtons: [UIBarButtonItem]!
@IBAction func sortByTitle(_ sender: UIBarButtonItem) {
dataSource.update(sortStyle: .title)
updateTintColors(tappedButton: sender)
}
@IBAction func sortByAuthor(_ sender: UIBarButtonItem) {
dataSource.update(sortStyle: .author)
updateTintColors(tappedButton: sender)
}
@IBAction func sortByReadMe(_ sender: UIBarButtonItem) {
dataSource.update(sortStyle: .readme)
updateTintColors(tappedButton: sender)
}
func updateTintColors(tappedButton: UIBarButtonItem) {
sortButtons.forEach { button in
button.tintColor = button == tappedButton
? button.customView?.tintColor : .secondaryLabel
}
}
@IBSegueAction func showDetailView(_ coder: NSCoder) -> DetailTableViewController? {
guard let indexPath = tableView.indexPathForSelectedRow,
let book = dataSource.itemIdentifier(for: indexPath) else {
fatalError("Nothing selected")
}
return DetailTableViewController(coder: coder, book: book)
}
@IBSegueAction func registerView(_ coder: NSCoder) -> NewBookTableViewController? {
return NewBookTableViewController(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = editButtonItem
tableView.register(UINib(nibName: "\(LibraryHeaderView.self)", bundle: nil), forHeaderFooterViewReuseIdentifier: LibraryHeaderView.reuseIdentifier)
configureDataSource()
dataSource.update(sortStyle: .readme)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
dataSource.update(sortStyle: dataSource.currentSortStyle)
}
// MARK: - Delegate
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
section == 1 ? "Read Me!" : nil
}
// MARK: - Data Source
override func numberOfSections(in tableView: UITableView) -> Int {
2
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 { return nil }
guard let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: LibraryHeaderView.reuseIdentifier) as? LibraryHeaderView else {
return nil
}
headerView.titleLabel.text = Section.allCases[section].rawValue
return headerView
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section != 0 ? 60 : 0
}
// MARK:- Data Source
func configureDataSource() {
dataSource = LibraryDataSource(tableView: tableView) { (tableView, indexPath, book) -> UITableViewCell? in
if indexPath == IndexPath(row: 0, section: 0) {
let cell = tableView.dequeueReusableCell(withIdentifier: "NewBookCell", for: indexPath)
return cell
}
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(BookCell.self)", for: indexPath) as? BookCell else {
fatalError("Could not create BookCell")
}
cell.titleLabel.text = book.title
cell.authorLabel.text = book.author
cell.bookThumbnail.image = book.image ?? LibrarySymbol.letterSquare(letter: book.title.first).image
cell.bookThumbnail.layer.cornerRadius = 12
if let review = book.review {
cell.reviewLabel.text = review
cell.reviewLabel.isHidden = false
}
cell.readMeBookmark.isHidden = !book.readMe
return cell
}
}
}
class LibraryDataSource: UITableViewDiffableDataSource<Section, Book> {
var currentSortStyle: SortStyle = .title
func update(sortStyle: SortStyle, animatingDifferences: Bool = true) {
currentSortStyle = sortStyle
var newSnappshot = NSDiffableDataSourceSnapshot<Section, Book>()
newSnappshot.appendSections(Section.allCases)
let booksByReadMe: [Bool: [Book]] = Dictionary(grouping: Library.books, by: \.readMe)
for (readMe, books) in booksByReadMe {
var sortedBooks: [Book]
switch sortStyle {
case .title:
sortedBooks = books.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending }
case .author:
sortedBooks = books.sorted { $0.author.localizedCaseInsensitiveCompare($1.author) == .orderedAscending }
case .readme:
sortedBooks = books
}
newSnappshot.appendItems(sortedBooks, toSection: readMe ? .readMe : .finished)
}
newSnappshot.appendItems([Book.mockBook], toSection: .addNew)
apply(newSnappshot, animatingDifferences: animatingDifferences)
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
indexPath.section == snapshot().indexOfSection(.addNew) ? false : true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
guard let book = self.itemIdentifier(for: indexPath) else {
return
}
Library.delete(book: book)
update(sortStyle: currentSortStyle)
}
}
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
if indexPath.section != snapshot().indexOfSection(.readMe)
&& currentSortStyle == .readme{
return false
} else {
return true
}
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
guard sourceIndexPath != destinationIndexPath,
sourceIndexPath.section == destinationIndexPath.section,
let bookToMove = itemIdentifier(for: sourceIndexPath),
let bookAtDestination = itemIdentifier(for: destinationIndexPath) else {
apply(snapshot(), animatingDifferences: false)
return
}
Library.reorderBooks(bookToMove: bookToMove, bookAtDestination: bookAtDestination)
update(sortStyle: currentSortStyle, animatingDifferences: false)
}
}
import UIKit
class NewBookTableViewController: UITableViewController {
@IBOutlet var titleTextField: UITextField!
@IBOutlet var authorextField: UITextField!
@IBOutlet var bookImageView: UIImageView!
var newBookImage: UIImage?
@IBAction func cancel() {
navigationController?.popViewController(animated: true)
}
@IBAction func saveNewBook() {
guard let title = titleTextField.text,
let author = authorextField.text,
!title.isEmpty,
!author.isEmpty else { return }
Library.addNew(book: Book(title: title, author: author, readMe: true, image: newBookImage))
navigationController?.popViewController(animated: true)
}
@IBAction func updateImage() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = UIImagePickerController.isSourceTypeAvailable(.camera) ? .camera : .photoLibrary
imagePicker.allowsEditing = true
present(imagePicker, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
bookImageView.layer.cornerRadius = 16
}
}
extension NewBookTableViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let selectedImage = info[.editedImage] as? UIImage else { return }
bookImageView.image = selectedImage
newBookImage = selectedImage
dismiss(animated: true)
}
}
extension NewBookTableViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == titleTextField {
return authorextField.becomeFirstResponder()
} else {
return textField.resignFirstResponder()
}
}
}
import UIKit
class DetailTableViewController: UITableViewController {
@IBOutlet var titleLabel: UILabel!
@IBOutlet var authorLabel: UILabel!
@IBOutlet var imageView: UIImageView!
@IBOutlet var reviewTextview: UITextView!
var book: Book
@IBOutlet var readMeButton: UIButton!
@IBAction func toggleReadMe() {
book.readMe.toggle()
let image = book.readMe
? LibrarySymbol.bookmarkFill.image
: LibrarySymbol.bookmark.image
readMeButton.setImage(image, for: .normal)
}
@IBAction func saveChanges() {
Library.update(book: book)
navigationController?.popViewController(animated: true)
}
@IBAction func updateImage() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = UIImagePickerController.isSourceTypeAvailable(.camera) ? .camera : .photoLibrary
imagePicker.allowsEditing = true
present(imagePicker, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = book.image ?? LibrarySymbol.letterSquare(letter: book.title.first).image
imageView.layer.cornerRadius = 60
titleLabel.text = book.title
authorLabel.text = book.author
if let review = book.review {
reviewTextview.text = review
}
let image = book.readMe
? LibrarySymbol.bookmarkFill.image
: LibrarySymbol.bookmark.image
readMeButton.setImage(image, for: .normal)
reviewTextview.addDoneButton()
}
required init?(coder: NSCoder) {
fatalError("This should never be called!")
}
init?(coder: NSCoder, book: Book) {
self.book = book
super.init(coder: coder)
}
}
extension DetailTableViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let selectedImage = info[.editedImage] as? UIImage else { return }
imageView.image = selectedImage
book.image = selectedImage
dismiss(animated: true)
}
}
extension DetailTableViewController: UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
book.review = textView.text
textView.resignFirstResponder()
}
}
extension UITextView {
func addDoneButton() {
let toolbar = UIToolbar()
toolbar.sizeToFit()
let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(self.resignFirstResponder))
toolbar.items = [flexSpace, doneButton]
self.inputAccessoryView = toolbar
}
}
import UIKit
class BookCell: UITableViewCell {
@IBOutlet var titleLabel: UILabel!
@IBOutlet var authorLabel: UILabel!
@IBOutlet var reviewLabel: UILabel!
@IBOutlet var readMeBookmark: UIImageView!
@IBOutlet var bookThumbnail: UIImageView!
}
import UIKit
// MARK:- Reusable SFSymbol Images
enum LibrarySymbol {
case bookmark
case bookmarkFill
case book
case letterSquare(letter: Character?)
var image: UIImage {
let imageName: String
switch self {
case .bookmark, .book:
imageName = "\(self)"
case .bookmarkFill:
imageName = "bookmark.fill"
case .letterSquare(let letter):
guard let letter = letter?.lowercased(),
let image = UIImage(systemName: "\(letter).square")
else {
imageName = "square"
break
}
return image
}
return UIImage(systemName: imageName)!
}
}
// MARK:- Library
enum Library {
private static let starterData = [
Book(title: "Ein Neues Land", author: "Shaun Tan", readMe: true),
Book(title: "Bosch", author: "Laurinda Dixon", readMe: true),
Book(title: "Dare to Lead", author: "Brené Brown", readMe: false),
Book(title: "Blasting for Optimum Health Recipe Book", author: "NutriBullet", readMe: false),
Book(title: "Drinking with the Saints", author: "Michael P. Foley", readMe: true),
Book(title: "A Guide to Tea", author: "Adagio Teas", readMe: false),
Book(title: "The Life and Complete Work of Francisco Goya", author: "P. Gassier & J Wilson", readMe: true),
Book(title: "Lady Cottington's Pressed Fairy Book", author: "Lady Cottington", readMe: false),
Book(title: "How to Draw Cats", author: "Janet Rancan", readMe: true),
Book(title: "Drawing People", author: "Barbara Bradley", readMe: false),
Book(title: "What to Say When You Talk to Yourself", author: "Shad Helmstetter", readMe: true)
]
static var books: [Book] = loadBooks()
private static let booksJSONURL = URL(fileURLWithPath: "Books",
relativeTo: FileManager.documentDirectoryURL).appendingPathExtension("json")
/// This method loads all existing data from the `booksJSONURL`, if available. If not, it will fall back to using `starterData`
/// - Returns: Returns an array of books, loaded from a JSON file
private static func loadBooks() -> [Book] {
let decoder = JSONDecoder()
guard let booksData = try? Data(contentsOf: booksJSONURL) else {
return starterData
}
do {
let books = try decoder.decode([Book].self, from: booksData)
return books.map { libraryBook in
Book(
title: libraryBook.title,
author: libraryBook.author,
review: libraryBook.review,
readMe: libraryBook.readMe,
image: loadImage(forBook: libraryBook)
)
}
} catch let error {
print(error)
return starterData
}
}
private static func saveAllBooks() {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
let booksData = try encoder.encode(books)
try booksData.write(to: booksJSONURL, options: .atomicWrite)
} catch let error {
print(error)
}
}
/// Adds a new book to the `books` array and saves it to disk.
/// - Parameters:
/// - book: The book to be added to the library.
/// - image: An optional image to associate with the book.
static func addNew(book: Book) {
if let image = book.image { saveImage(image, forBook: book) }
books.insert(book, at: 0)
saveAllBooks()
}
/// Updates the stored value for a single book.
/// - Parameter book: The book to be updated.
static func update(book: Book) {
if let newImage = book.image {
saveImage(newImage, forBook: book)
}
guard let bookIndex = books.firstIndex(where: { storedBook in
book.title == storedBook.title } )
else {
print("No book to update")
return
}
books[bookIndex] = book
saveAllBooks()
}
/// Removes a book from the `books` array.
/// - Parameter book: The book to be deleted from the library.
static func delete(book: Book) {
guard let bookIndex = books.firstIndex(where: { storedBook in
book == storedBook } )
else { return }
books.remove(at: bookIndex)
let imageURL = FileManager.documentDirectoryURL.appendingPathComponent(book.title)
do {
try FileManager().removeItem(at: imageURL)
} catch let error { print(error) }
saveAllBooks()
}
static func reorderBooks(bookToMove: Book, bookAtDestination: Book) {
let destinationIndex = Library.books.firstIndex(of: bookAtDestination) ?? 0
books.removeAll(where: { $0.title == bookToMove.title })
books.insert(bookToMove, at: destinationIndex)
saveAllBooks()
}
/// Saves an image associated with a given book title.
/// - Parameters:
/// - image: The image to be saved.
/// - title: The title of the book associated with the image.
static func saveImage(_ image: UIImage, forBook book: Book) {
let imageURL = FileManager.documentDirectoryURL.appendingPathComponent(book.title)
if let jpgData = image.jpegData(compressionQuality: 0.7) {
try? jpgData.write(to: imageURL, options: .atomicWrite)
}
}
/// Loads and returns an image for a given book title.
/// - Parameter title: Title of the book you need an image for.
/// - Returns: The image associated with the given book title.
static func loadImage(forBook book: Book) -> UIImage? {
let imageURL = FileManager.documentDirectoryURL.appendingPathComponent(book.title)
return UIImage(contentsOfFile: imageURL.path)
}
}
extension FileManager {
static var documentDirectoryURL: URL {
return `default`.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
}
import UIKit
struct Book: Hashable {
let title: String
let author: String
var review: String?
var readMe: Bool
var image: UIImage?
static let mockBook = Book(title: "", author: "", readMe: true)
}
extension Book: Codable {
enum CodingKeys: String, CodingKey {
case title
case author
case review
case readMe
}
}