#22 實作 Table View 的基本功能 -FB 頁面 part3

Lou
彼得潘的 Swift iOS / Flutter App 開發教室
31 min readJul 20, 2023

ViewController+TableViewController

貼文內容Container View連結TableViewController

Container View連結TableViewController

CommentViewController貼文頂部及留言功能

CommentTableViewController貼文內容及留言內容

點進貼文觀看留言

CommentViewController

接收上一頁的資料

selectSection選擇的貼文以及shouldOpenKeyboard點選貼文進入內文不會彈出鍵盤,點選留言按鈕進入內文會彈出鍵盤

class CommentViewController: UIViewController {

var selectSection: Int
var shouldOpenKeyboard: Bool

init?(coder: NSCoder, selectSection: Int, shouldOpenKeyboard: Bool ) {
self.selectSection = selectSection
self.shouldOpenKeyboard = shouldOpenKeyboard
super.init(coder: coder)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

}

自訂Navigation bar

貼文者資料的Navigation bar
    func setNavigationbar() {
// 設定左邊返回按鈕
let backButton = UIButton(type: .system)
backButton.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
let backSymbolConfig = UIImage.SymbolConfiguration(weight: .heavy)
let backImage = UIImage(systemName: "chevron.backward", withConfiguration: backSymbolConfig)
backButton.setImage(backImage, for: .normal)
backButton.tintColor = UIColor(red: 5/255, green: 5/255, blue: 5/255, alpha: 1)
backButton.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside)
let backButtonItem = UIBarButtonItem(customView: backButton)


// 設定左邊ImageView
let profilePictureImageView = UIImageView()
profilePictureImageView.frame = CGRect(x: 0, y: 0, width: 44, height: 44)
profilePictureImageView.image = UIImage(named: postArray[selectSection].profilePictureName)
profilePictureImageView.contentMode = .scaleAspectFill
profilePictureImageView.layer.cornerRadius = 22
profilePictureImageView.clipsToBounds = true
let profilePictureItem = UIBarButtonItem(customView: profilePictureImageView)

// 貼文者資料view
let container = UIView()
// 發文者名稱
let nameLabel = UILabel()
nameLabel.text = postArray[selectSection].userName
nameLabel.font = UIFont.systemFont(ofSize: 17)
nameLabel.textColor = UIColor(red: 5/255, green: 5/255, blue: 5/255, alpha: 1)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(nameLabel)
// 發文時間
let timeLabel = UILabel()
timeLabel.text = timeAgoDisplay(date: postArray[selectSection].timestamp)
timeLabel.font = UIFont.systemFont(ofSize: 17)
timeLabel.textColor = UIColor(red: 101/255, green: 103/255, blue: 106/255, alpha: 1)
timeLabel.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(timeLabel)
// 分隔符號
let separatorLabel = UILabel()
separatorLabel.text = "."
separatorLabel.font = UIFont(name: "蘋方-繁", size: 17)
separatorLabel.textColor = UIColor(red: 101/255, green: 103/255, blue: 106/255, alpha: 1)
separatorLabel.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(separatorLabel)
// 貼文狀態
let stateButton = UIButton()
if postArray[selectSection].isPublic {
stateButton.setImage(UIImage(systemName: "globe.asia.australia.fill"), for: .normal)
} else {
stateButton.setImage(UIImage(systemName: "person.2.fill"), for: .normal)
}
stateButton.tintColor = UIColor(red: 101/255, green: 103/255, blue: 106/255, alpha: 1)
stateButton.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(stateButton)

// 設定auto layout條件
NSLayoutConstraint.activate([
profilePictureImageView.widthAnchor.constraint(equalToConstant: 44),
profilePictureImageView.heightAnchor.constraint(equalToConstant: 44),
nameLabel.heightAnchor.constraint(equalToConstant: 22),
timeLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor),
timeLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
timeLabel.heightAnchor.constraint(equalToConstant: 22),
separatorLabel.widthAnchor.constraint(equalToConstant: 17),
separatorLabel.heightAnchor.constraint(equalToConstant: 22),
stateButton.widthAnchor.constraint(equalToConstant: 22),
stateButton.heightAnchor.constraint(equalToConstant: 22),

nameLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
nameLabel.topAnchor.constraint(equalTo: container.topAnchor),
timeLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
timeLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor),
separatorLabel.leadingAnchor.constraint(equalTo: timeLabel.trailingAnchor),
separatorLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor),
stateButton.leadingAnchor.constraint(equalTo: separatorLabel.trailingAnchor),
stateButton.bottomAnchor.constraint(equalTo: container.bottomAnchor)
])


let userItem = UIBarButtonItem(customView: container)

navigationItem.leftBarButtonItems = [backButtonItem, profilePictureItem, userItem]
}

// 返回上一頁按鈕的功能
@objc func backButtonTapped () {
navigationController?.popViewController(animated: true)
}

留言按鈕樣式設定

系統原有的圖示向上 旋轉調整成向右

    func updateUI() {
sendButton.transform = CGAffineTransform(rotationAngle: .pi/2)
sendButton.tintColor = UIColor(red: 100/255, green: 103/255, blue: 106/255, alpha: 1)
}

留言欄隨著鍵盤變動位置(通知功能)

鍵盤顯示或隱藏 留言欄變動位置
    // ContainerView與留言欄auto layout條件
@IBOutlet weak var postContainerViewBottomConstraint: NSLayoutConstraint!

func registerForKeyboardNotifications() {
// 鍵盤出現
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWasShown(_:)), name: UIResponder.keyboardDidShowNotification, object: nil)
// 鍵盤隱藏
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillBeHidden(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}

@objc func keyboardWasShown(_ notification: NSNotification) {
guard let info = notification.userInfo,
let keyboardFrameValue = info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
let tabHeight = tabBarController?.tabBar.frame.size.height else { return }
let keyboardFrame = keyboardFrameValue.cgRectValue
let keyboardSize = keyboardFrame.size
self.commentView.transform = CGAffineTransform(translationX: 0, y: -(keyboardSize.height - tabHeight))
// 變動ContainerView與留言欄auto layout條件
postContainerViewBottomConstraint.constant = -(commentView.frame.height)

// 留言欄移動的動畫設定
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}

}


@objc func keyboardWillBeHidden(_ notification: NSNotification) {
// 重置ContainerView與留言欄auto layout條件
postContainerViewBottomConstraint.constant = 0
commentView.transform = CGAffineTransform(translationX: 0, y: 0)
}

留言功能

留言欄位
    @IBAction func commentChangeButtonColor(_ sender: Any) {

if let text = commentTextField.text, !text.isEmpty {
// 輸入文字發送按鈕變藍色
sendButton.tintColor = UIColor(red: 23/255, green: 119/255, blue: 241/255, alpha: 1)
} else {
sendButton.tintColor = UIColor(red: 100/255, green: 103/255, blue: 106/255, alpha: 1)
}

}

@IBAction func comment(_ sender: Any) {
if let text = commentTextField.text, !text.isEmpty {
let postComment = PostComment(userName: "Lou", profilePictureName: "userphoto", content: text, timestamp: Date()) // 留言功能
postArray[selectSection].postComments.append(postComment)
delegate?.updateTable() // 代理CommentTableViewController更新表格
commentTextField.text = "" // 留言發送後 清除文字
view.endEditing(true) // 留言發送後 收鍵盤
sendButton.tintColor = UIColor(red: 100/255, green: 103/255, blue: 106/255, alpha: 1) // 發送鈕顏色變回預設
}
}

// 點選其他地方 收鍵盤
@IBAction func closeKeyboard(_ sender: Any) {
view.endEditing(true)
}

CommentTableViewController委託CommentViewController代理更新表單

ViewController需要用到TableViewController的更新表單功能

且傳遞資料到TableViewController

import UIKit

// 建立更新表格的協議
protocol CommentViewControllerDelegate: AnyObject {
func updateTable()
}

class CommentViewController: UIViewController {
weak var delegate: CommentViewControllerDelegate? // 代理人

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "embedCommentTableViewControllerSegue",
let commentTableVC = segue.destination as? CommentTableViewController {
// 傳遞資料
commentTableVC.selectSection = selectSection
// CommentTableViewController設定為代理人
commentTableVC.delegate = self
// CommentViewController設定為CommentTableViewController代理人
self.delegate = commentTableVC
}
}
}

extension CommentViewController: CommentButtonDelegate {

func commentButtonTapped() {
commentTextField.becomeFirstResponder()
}
}

CommentViewController委託CommentTableViewController代理鍵盤彈出

TableViewController需要用到ViewController留言欄的鍵盤彈出功能

import UIKit

protocol CommentButtonDelegate: AnyObject {
func commentButtonTapped()
}

class CommentTableViewController: UITableViewController {

weak var delegate: CommentButtonDelegate?

@IBAction func commentButtonTapped(_ sender: UIButton) {
delegate?.commentButtonTapped()
}
}


extension CommentTableViewController: CommentViewControllerDelegate {

func updateTable() {
tableView.reloadData()
}
}

CommentTableViewController

TableViewCell分佈
override func numberOfSections(in tableView: UITableView) -> Int {
guard let selectSection = selectSection else { return 3 }
return 3 + postArray[selectSection].postComments.count // 留言數增加Section
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell
guard let selectSection = selectSection else { return UITableViewCell() }

if indexPath.section == 0 {
guard let postCell = tableView.dequeueReusableCell(withIdentifier: "\(PostTableViewCell.self)", for: indexPath) as? PostTableViewCell else { fatalError("PostTableViewCell failed") }
let postInfo = postArray[selectSection]
postCell.updateUI(with: postInfo)
cell = postCell

} else if indexPath.section == 1 {
guard let postButtonCell = tableView.dequeueReusableCell(withIdentifier: "\(PostButtonTableViewCell.self)", for: indexPath) as? PostButtonTableViewCell else { fatalError("PostBarTableViewCell failed") }
let postInfo = postArray[selectSection]
postButtonCell.updateUI(with: postInfo)
cell = postButtonCell

} else if indexPath.section == 2 {
guard let likesCell = tableView.dequeueReusableCell(withIdentifier: "\(LikesTableViewCell.self)", for: indexPath) as? LikesTableViewCell else { fatalError("LikesTableViewCell failed") }
let postInfo = postArray[selectSection]
likesCell.updateUI(with: postInfo)
cell = likesCell

} else {
guard let commentCell = tableView.dequeueReusableCell(withIdentifier: "\(CommentTableViewCell.self)" , for: indexPath) as? CommentTableViewCell else { fatalError("CommentTableViewCell failed ") }
let postComment = postArray[selectSection].postComments[indexPath.section - 3]
commentCell.updateUI(with: postComment)
cell = commentCell

}
// 設定背景色為透明
let backgroundView = UIView()
backgroundView.backgroundColor = UIColor.clear

// cell 的 selectedBackgroundView
cell.selectedBackgroundView = backgroundView
return cell
}

四種自訂的TableViewCell

PostTableViewCell (Section 0) 顯示貼文文字內容與圖片

import UIKit

class PostTableViewCell: UITableViewCell {

@IBOutlet weak var postMessageLabel: UILabel!
@IBOutlet weak var postImageView: UIImageView!


override func awakeFromNib() {
super.awakeFromNib()
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}

func updateUI(with postInfo: PostInfo) {

postMessageLabel.text = postInfo.text
if let imageName = postInfo.imageName {
if UUID(uuidString: imageName) != nil { // imageName是UUID,從文件系統讀取
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = documentsDirectory.appendingPathComponent(imageName)

postImageView.image = UIImage(contentsOfFile: fileURL.path)
} else { // 從Asset圖庫讀取
postImageView.image = UIImage(named: imageName)
}
} else {
postImageView.image = nil
}
}
}

PostButtonTableViewCell (Section 1) 按讚 留言 分享功能欄

import UIKit

class PostButtonTableViewCell: UITableViewCell {

@IBOutlet weak var likeButton: UIButton!
@IBOutlet weak var commentButton: UIButton!

override func awakeFromNib() {
super.awakeFromNib()
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}

func updateUI(with postInfo: PostInfo){
if postInfo.isLiked {
likeButton.setImage(UIImage(systemName: postInfo.likeButtonImageName), for: .normal)
likeButton.tintColor = UIColor(red: 23/255, green: 119/255, blue: 241/255, alpha: 1)
} else {
likeButton.setImage(UIImage(systemName: postInfo.likeButtonImageName), for: .normal)
likeButton.tintColor = UIColor(red: 100/255, green: 103/255, blue: 106/255, alpha: 1)
}
}
}

LikesTableViewCell (Section 2) 按讚數量 分享數量

import UIKit

class LikesTableViewCell: UITableViewCell {

@IBOutlet weak var LikesLabel: UILabel!
@IBOutlet weak var sharesLabel: UILabel!

override func awakeFromNib() {
super.awakeFromNib()
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}

func updateUI(with postInfo: PostInfo){
LikesLabel.text = String(postInfo.likes)
sharesLabel.text = String(postInfo.shares)+"次分享"
}

}

CommentTableViewCell (Section 3) 留言內容

import UIKit

class CommentTableViewCell: UITableViewCell {

@IBOutlet weak var userImageView: UIImageView!
@IBOutlet weak var commentLabel: UILabel!
@IBOutlet weak var timeLabel: UILabel!
@IBOutlet weak var likeButton: UIButton!
@IBOutlet weak var likesLabel: UILabel!


override func awakeFromNib() {
super.awakeFromNib()
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}

func updateUI(with postComment: PostComment) {
userImageView.image = UIImage(named: postComment.profilePictureName)
commentLabel.text = "\(postComment.userName)\n\(postComment.content)"
timeLabel.text = timeAgoDisplay(date: postComment.timestamp)
if postComment.likes != 0 {
likesLabel.text = "\(postComment.likes)👍"
} else {
likesLabel.text = ""
}
if postComment.isLiked {
likeButton.tintColor = UIColor(red: 23/255, green: 119/255, blue: 241/255, alpha: 1)
} else {
likeButton.tintColor = UIColor(red: 100/255, green: 103/255, blue: 106/255, alpha: 1)
}
}
}

貼文按讚功能

貼文like
 @IBAction func likePost(_ sender: UIButton) {

guard let selectSection = selectSection else { return }

postArray[selectSection].isLiked = !postArray[selectSection].isLiked
let likeBtnImageName = postArray[selectSection].likeButtonImageName
sender.setImage(UIImage(systemName: likeBtnImageName), for: .normal)

if postArray[selectSection].isLiked {
sender.tintColor = UIColor(red: 23/255, green: 119/255, blue: 241/255, alpha: 1)
} else {
sender.tintColor = UIColor(red: 100/255, green: 103/255, blue: 106/255, alpha: 1)
}
// 取得likecountLabel所在的 indexPath
let postIndexPath = [IndexPath(row: 0, section: 2)]
// 重新載入表格
tableView.reloadRows(at: postIndexPath, with: .none)
}

留言按讚功能

留言like
    @IBAction func likeComment(_ sender: UIButton) {
let point = sender.convert(CGPoint.zero, to: tableView)
if let indexPath = tableView.indexPathForRow(at: point),
let selectSection = selectSection {
let section = indexPath.section
let commentSection = section - 3
postArray[selectSection].postComments[commentSection].isLiked = !postArray[selectSection].postComments[commentSection].isLiked

if postArray[selectSection].postComments[commentSection].isLiked {
sender.tintColor = UIColor(red: 23/255, green: 119/255, blue: 241/255, alpha: 1)
} else {
sender.tintColor = UIColor(red: 100/255, green: 103/255, blue: 106/255, alpha: 1)
}

let commentIndexPath = [IndexPath(row: 0, section: section)]
tableView.reloadRows(at: commentIndexPath, with: .none)
}

}

下篇繼續介紹part4

--

--