# 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畫面:

GIF

作業摘要:

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 檔案

--

--