# 35 eBook of Jewel Changi Airport 星耀樟宜 電子書-程式版
Collection View, Table View & 資料傳遞
作業來源:
複習完 Collection View 思考要如何完成相關作業,忽然想到在文組班時做的電子書有用到照片牆,但當時是用不寫程式的方法,現在拿來重新製作成程式版。
文組班時的電子書作業作品:
做到的部分:
照片牆(Grid Photo Wall)
🌟 版本 2: 程式版,使用 collection view controller
點選照片後,切換到下一頁顯示大張圖片
🌟 版本 3: 在 view controller 上加入 collection view
水平捲動的 collection view
🌟 進階版
分頁功能,一頁顯示一個 cell
大張圖可以水平滑動瀏覽
Table View
🌟 客製 cell 顯示表格的內容
搭配 as! , as? 轉型
🌟 點選 cell 後可到下一頁顯示詳細資訊
🌟 cell 類別裡定義設定內容的 function
在 Table View 的 cell 裡設定 func updateUI( ) 使程式分散一些
頁面間資料傳遞
🌟 在下一頁 controller 定義 property 及 init
利用 IBSegueAction func 傳遞資料
APP畫面:
作業摘要:
storyboard 畫面,從文組班時期是這樣子
在彼得潘班變成這樣子
Collection View
基本上跟 Table View 作法非常類似,主要可以分成三種設計:
第一種:照片牆,就是IG上可以看到到配置(Collection View Controller)
第二種:水平滑動水平方向的多張照片(View Controller + Collection View)
第三種:一頁(一個 Controller)一張大圖片(一個 cell)滑動(View Controller + Collection View)
第二種跟第三種幾乎一樣作法,這次作業用了第一種跟第三種作法。
🌟 照片牆
這裡是新增一個 Collection View Controller,注意 Size Inspector, cell 的間距以及大小,如下圖橘色框框,將左右呈現出來的樣子
注意:因為未用到 AutoLayout,所以 Estimate Size 要選 None
再來是注意滑動方向,在 Attributes Inspector
跟 Table View 一樣,用 array 建立資料。程式也幾乎一樣,只是Table View 的 row,在 Collection View 變成了 item
一樣是用三個 function,去決定 section 數量,item 數量,
以及 cell 內容
override func numberOfSections(in collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
return jewelSpots.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(GridCollectionViewCell.self)", for: indexPath) as! GridCollectionViewCell
cell.GridImageView.image = UIImage(named: jewelSpots[indexPath.item].name)
return cell
}
🌟 一頁(一個 Controller)一張大圖片滑動
在照片牆點選後,設計可以連到這個 Collecetion View,
然後可以水平滑動繼續瀏覽照片
作法是新增一個 View Controller,裡面放一個 Collection View,
注意: data source 及 delegate 在這裡要自己拉!所有的 Controller 跟 Cell 都要自訂型別並套用上去。
因為會用自己定義的 cell ID,務必刪除兩行預設的程式碼
private let reuseIdentifier = "Cell"
以及
// Register cell classes
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
再來是資料傳遞的程式碼部分,從上一頁(照片牆)傳到下一頁(顯示一張大圖可水平滑動瀏覽):
Step 1:在下一頁,新增所需要資料的同樣型別的屬性,並定義好 init
Step 2:在上一頁,定義 IBSegueAction func
return 後面下一頁 Controller 的參數,對應著上一頁要傳到下一頁時產生的資料。
Step 3:回到下一頁,這裡比較特別一些,下一頁是傳入另一個 Collection View 去顯示,結合接收到的資料,所以寫入 func cellForItemAt 去設定 cell 的內容
extension OneViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
jewels.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(OneCollectionViewCell.self)", for: indexPath) as? OneCollectionViewCell else {fatalError("dequeueReusableCell OneCollectionViewCell Failed")}
let newIndex = (newItem + indexPath.item + jewels.count) % jewels.count
cell.oneImageView.image = UIImage(named: jewels[newIndex].name)
print("newItem:\(newItem), indexPath.item:\(indexPath.item), jewels.count:\(jewels.count), newIndex:\(newIndex)")
return cell
}
}
說明:
let newIndex = (newItem + indexPath.item + jewels.count) % jewels.count
cell.oneImageView.image = UIImage(named: jewels[newIndex].name)
在第一行,這裡的 newIndex 會取代原本第二行的 jewel[ ] 中括號裡的indexPath.item。
newItem:代表上一頁被點選的 cell 的 indexPath.item,型別是Int,代表被點選的照片,跟 array 一樣,第一張會是0,第二張會是1 ,以此類推。
indexPath.item:這邊因為只會顯示一張大圖,開始即產生一個 item,item 跟 array 一樣從0開始算起,所以一開始的 indexPath.item 等於 0 ,隨著水平滑動瀏覽照片的增加而增加。
jewels.count:資料的筆數,這裡是16。
結果:
透過常數 newIndex 的算式
let newIndex = (newItem + indexPath.item + jewels.count) % jewels.count
或是這樣(兩個算式差別在於商不一樣,餘數是一樣的)
let newIndex = (newItem + indexPath.item) % jewels.count
newIndex 都會等於上一頁的 indexPath.item,就會在下一頁顯示相同的照片。
而算式裡的 indexPath.item 是下一頁的,會隨瀏覽增加而增加,讓滑動照片時 newIndex 也跟著增加
cell.oneImageView.image = UIImage(named: jewels[newIndex].name)
隨著 newIndex 的增加,就會改變 array jewels 顯示照片的名稱,最終滑動時就會顯示下一張照片。
Table View
Table View 的作法在前兩篇有介紹過。
這裡稍微提一下當資料 array 有兩層時,傳遞資料時的寫法,觀念跟剛剛Collection View 是一樣的,
第一層資料的型別:
import Foundation
struct Shop {
let name: String
let attractions: [Attraction]
}
第二層資料的型別:
import Foundation
struct Attraction {
let name: String
let chineseName: String
let photo: String
let intro: String
let location: String
}
Step 1:在下一頁,新增所需要資料的同樣型別的屬性,並定義好 init
import UIKit
class DetailViewController: UIViewController {
let shops: [Shop]
let section: Int
let row: Int
init?(coder: NSCoder, shops: [Shop], section: Int, row: Int) {
self.shops = shops
self.section = section
self.row = row
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@IBOutlet weak var detailImageView: UIImageView!
@IBOutlet weak var detailNameLabel: UILabel!
@IBOutlet weak var detailChineseLabel: UILabel!
@IBOutlet weak var detailTextView: UITextView!
@IBOutlet weak var locationLabel: UILabel!
Step 2:在上一頁,定義 IBSegueAction func,return 後面下一頁 Controller 的參數,對應著上一頁要傳到下一頁時產生的資料。
@IBSegueAction func showDetail(_ coder: NSCoder) -> DetailViewController? {
guard let row = tableView.indexPathForSelectedRow?.row else {return nil}
let section = tableView.indexPathForSelectedRow?.section
return DetailViewController(coder: coder, shops: shops, section: section!, row: row)
}
特別注意因為有兩層 array,所以會需要用到 section 的數值
let section = tableView.indexPathForSelectedRow?.section
Step 3:回到下一頁,因為第一層的屬性原本就包含第二層的 array 型別,可以用來產生第二層的 array,如下
let attraction = shops[section].attractions[row]
最後,在讓傳回的資料,對應拉好的IBOutlet,及顯示出想要的樣式
override func viewDidLoad() {
super.viewDidLoad()
updateUI()
configutation()
}
func updateUI() {
let attraction = shops[section].attractions[row]
detailImageView.image = UIImage(named: attraction.photo)
detailNameLabel.text = attraction.name
detailChineseLabel.text = attraction.chineseName
detailTextView.text = attraction.intro
locationLabel.text = attraction.location
detailTextView.isEditable = false
}
func configutation() {
detailNameLabel.frame = CGRect(x: 25, y: 100, width: 140, height: 44)
detailNameLabel.numberOfLines = 0
detailNameLabel.textAlignment = .center
detailNameLabel.textColor = .orange
detailNameLabel.font = .systemFont(ofSize: 18, weight: .semibold)
detailChineseLabel.frame = CGRect(x: 180, y: 100, width: 140, height: 44)
detailChineseLabel.textAlignment = .left
detailChineseLabel.textColor = .orange
detailChineseLabel.font = .systemFont(ofSize: 18, weight: .medium)
detailImageView.frame = CGRect(x: 10, y: 150, width: 370, height: 300)
detailImageView.contentMode = .scaleAspectFit
detailTextView.frame = CGRect(x: 50, y: 480, width: 290, height: 190)
detailTextView.font = .systemFont(ofSize: 16)
detailTextView.backgroundColor = .clear
locationLabel.frame = CGRect(x: 75, y: 720, width: 250, height: 30)
locationLabel.textAlignment = .center
}
cell 類別裡定義設定內容的 function
在 Table View 的 cell 裡製作 function,作法有兩種:
🌟 作法一:把需要傳的資料的類別,寫成 cell 型別的屬性
🌟 作法二:把需要傳的資料的類別,寫成 function 的參數
這裡練習的是作法二
func updateUI(with attraction: Attraction) {
accessoryType = .disclosureIndicator
shopImageView.image = UIImage(named: attraction.photo)
nameLabel.text = attraction.name
chineseName.text = attraction.chineseName
shopImageView.frame = CGRect(x: 20, y: 10, width: 188, height: 133)
shopImageView.contentMode = .scaleAspectFill
nameLabel.frame = CGRect(x: 210, y: 40, width: 160, height: 44)
nameLabel.numberOfLines = 0
nameLabel.font = .systemFont(ofSize: 18, weight: .semibold)
//nameLabel.backgroundColor = .gray
chineseName.frame = CGRect(x: 210, y: 84, width: 160, height: 44)
chineseName.numberOfLines = 0
chineseName.font = .systemFont(ofSize: 16, weight: .medium)
//chineseName.backgroundColor = .lightGray
}
function 後的外部參數 with,可有可無,重點是後面的內部參數 attraction 及 類別 Attraction。
然後把 function 運用到對應的 Table View Controller 裡
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(ShopTableViewCell.self)", for: indexPath) as? ShopTableViewCell else {fatalError("dequeueReusableCell ShopTableViewCell Failed")}
let attraction = shops[indexPath.section].attractions[indexPath.item]
cell.updateUI(with: attraction)
return cell
}
把 function 裡參數的類別存入常數,這裡取跟內部參數同名 attraction,然後寫進 function 後的參數裡。
✨ 作法一:把需要傳的資料的類別,寫成 cell 型別的屬性:可參考另一篇 Medium 的最後兩個 GitHub 檔案