HW#44 Pickerview Practice (模仿HSR App) -Page 2–1訂票頁面

Page 2的訂票頁面的介面會稍微複雜一點,你會看到上方的T-Express logo是設定UINavigationBarAppearance 裡的appearancescrollEdgeAppearance,並且設定navigationBar 裡的backgroundColor 以及barTintColor 以及將navigationItem 裡的titleView 設定為已經設定好的hsrLogoImageView 型別是UIImageView。(我認為Programmatically UI這種東西說再多都是沒用的,很多時候就是 Just do it!,做就對了,犯錯了沒關係,想辦法修好就好!)

    func customNavigationBar () {
// scrollEdgeAppearance
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = Colors.navigationBarColor
self.navigationItem.scrollEdgeAppearance = appearance

// Set up navigation item
self.navigationItem.titleView?.backgroundColor = Colors.navigationBarColor
self.navigationController?.navigationBar.backgroundColor = Colors.navigationBarColor
self.navigationController?.navigationBar.barTintColor = Colors.navigationBarColor

// Set up UIBarButtonItem
let speakerBarButton: UIBarButtonItem = UIBarButtonItem(image: Images.speakerWave, style: .plain, target: self, action: #selector(speakerBarButtonTapped))
let accountBarButton: UIBarButtonItem = UIBarButtonItem(image: Images.personfill, style: .plain, target: self, action: #selector(accountBarButtonTapped))
let fixedSpace: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: self, action: nil)

// Set up barButton's tintColor
speakerBarButton.tintColor = Colors.white
accountBarButton.tintColor = Colors.white

// hsrLogo Set up
hsrLogoImageView = UIImageView(image: Images.hsrImage)
hsrLogoImageView.contentMode = .scaleAspectFit

// Set up navigationItem's rightBarButton
self.navigationItem.rightBarButtonItems = [accountBarButton, fixedSpace, speakerBarButton]
// Set up navigationItem's titleView
self.navigationItem.titleView = hsrLogoImageView
}

Reference:

  • Customize navigation bar with title view
Floating Segmented Control

再來就是要處理這個Segmented Control,我當初不是很清楚這是用什麼方式製作的,但剛好看到其他同學所製作的UI,就找到了,覺得實在非常地幸運!

  • 作法:

主要的做法,其實就是先建立一個新的containerView,並設立你所想要的backgroundColor根據你的UI設定,並將segmented Control 裝到我所建立的containerView裡面,這樣就有辦法有做出一模ㄧ樣的效果。

再來就是下面橘色所移動的浮標,其實它會是一個UIView,它可以根據你所點選的segmented ControlselectedIndex做位移,可以讓你在點選不同segmented Control 時,做出移動的效果,其實挺有趣的(你可以在很多的App上,看到類似這樣的功能露出,例如: Ubereats!)

  • SegmentedControlContainerView UI Set up:

首先先設定SegmentedControlContainerViewbackgroundColorColors.navigationBarColor

再來是設定segmentedControl本身,運用這方法setTitleTextAttributes去設定segmentedControl 在被選取時跟不被選取時的狀態。

再來就是設定segmentedControl本身的內容設定,設定segmentedControl 的title,並按照順序去安插所對應的內容。(index本身是從0開始,之前有做過練習的話,這對你來說應該非常簡單~)

再來就是要將segmentedControl 本身的backgroundColor設定為.clear,因為這樣才有辦法在加入segmentedControlContainerView 時,不會有背景色上顏色重疊的問題。

    // MARK: - SegmentedControl
func configureSegmentedControlContainerView () {

// segmentedControlContainerView
segmentedControlContainerView.backgroundColor = Colors.navigationBarColor

// segmentedControl's text
segmentedControl.setTitleTextAttributes([
NSAttributedString.Key.foregroundColor: UIColor.white,
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16, weight: .regular)], for: .normal)

segmentedControl.setTitleTextAttributes([
NSAttributedString.Key.foregroundColor: UIColor.white,
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16, weight: .semibold)], for: .selected)
segmentedControl.insertSegment(withTitle: "一般訂票", at: 0, animated: true)
segmentedControl.insertSegment(withTitle: "信用卡優惠", at: 1, animated: true)
segmentedControl.insertSegment(withTitle: "今日自由座", at: 2, animated: true)
segmentedControl.sizeToFit()
segmentedControl.backgroundColor = .clear
segmentedControl.tintColor = .white
segmentedControl.selectedSegmentIndex = 0
segmentedControl.isEnabled = true
segmentedControl.addTarget(self, action: #selector(segmentedControlTapped), for: .valueChanged)
segmentedControl.setBackgroundImage(UIImage(), for: .normal, barMetrics: .default)
segmentedControl.setDividerImage(UIImage(), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)

// underline
underlineView.backgroundColor = Colors.orangeBrandColor
underlineView.layer.cornerRadius = Constants.underlineViewHeight / 2
}
  • Constraints Segmented Control:

先把segmentedControlContainerView 加入到原有的view裡,再把segmentedControl 加入到 segmentedControlContainerView ,然後再設定Auto-Layout

    func constraintSegmentedControl () {
view.addSubview(segmentedControlContainerView)
segmentedControlContainerView.addSubview(segmentedControl)
view.addSubview(underlineView)

segmentedControlContainerView.translatesAutoresizingMaskIntoConstraints = false
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
underlineView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
segmentedControlContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
segmentedControlContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
segmentedControlContainerView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor),
segmentedControlContainerView.heightAnchor.constraint(equalToConstant: Constants.segmentedControlHeight),
])

NSLayoutConstraint.activate([
segmentedControl.centerXAnchor.constraint(equalTo: segmentedControlContainerView.centerXAnchor),
segmentedControl.centerYAnchor.constraint(equalTo: segmentedControlContainerView.centerYAnchor),
segmentedControl.leadingAnchor.constraint(equalTo: segmentedControlContainerView.leadingAnchor),
segmentedControl.heightAnchor.constraint(equalToConstant: Constants.segmentedControlHeight)
])

NSLayoutConstraint.activate([
underlineView.bottomAnchor.constraint(equalTo: segmentedControlContainerView.bottomAnchor),
underlineView.leadingAnchor.constraint(equalTo: segmentedControlContainerView.leadingAnchor, constant: 50),
underlineView.heightAnchor.constraint(equalToConstant: Constants.underlineViewHeight),
underlineView.widthAnchor.constraint(equalToConstant: Constants.underlineViewWidth)
])
}
  • Floating orange view moving:(目前這個功能尚未完成,之後會修好,2024/3/4記)
    func changeSegmentedControlLinePosition() {
UIView.animate(withDuration: 0.3) {
// Calculate the width of each segment
let segmentWidth = self.segmentedControl.frame.width / CGFloat(self.segmentedControl.numberOfSegments)
// Calculate the offset x based on the selected segment index
let offsetX = segmentWidth * CGFloat(self.segmentedControl.selectedSegmentIndex)
// Update the underlineView's x position to align with the selected segment
self.underlineView.frame.origin.x = offsetX + self.segmentedControl.frame.minX
print("offsetX Value is \(offsetX)")
}
}
一般訂票頁面

一般頁面的這一頁的畫面,我主要是使用了四個tableView,並且把這個四個客製化的tableView內容,裝在一個backgroundView裡面(我不確定這樣做是不是正確的),會這麼做主要是考慮到使用者在點擊畫面的時候,可以明確的知道是在點擊哪一個tableView的畫面,這樣可以很直覺的去找到該服務所產生的內容。

為了呈現跟高鐵App一模一樣的內容,我參考原App的功能,在點擊第二個的tableViewCell 裡的南港 & 左營的UIButton時,會呈現UIPickView,這時你可以需求選擇不同的站名,也可以使用起始站互換。

首先先設定pickerViewdelegate & dataSource

  func addPickerViewDelegateAndDataSource () {
pickerView.delegate = self
pickerView.dataSource = self
}

Delegate:

在這次的練習當中,我使用了titleRow & didSelectRow

  • titleRow:

設定titleRow的內容,可以用if else的寫法(也可以用enum),去回覆一個我所建立的值。

  • didSelectRow:

我製作了一個空的值叫selectedFromStationRow,當component等於0時,選取pickerView裡第一列的值,將stationName[row]陣列裡面的內容,會存到selectedFromStationRow ,並且顯示這個內容,第二列也是同樣的作法。

// MARK: - Extension for PickerView's delegate :
extension TickerOrderViewController: UIPickerViewDelegate {
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
print("component \(component) row \(row)")
return stationName[row]
} else {
print("component \(component) row \(row)")
return depatureStationName[row]
}
}

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if component == 0 {
selectedFromStationRow = stationName[row]
} else {
selectedDepatureStationRow = depatureStationName[row]
}
print("起程點為\(selectedFromStationRow),到達站為\(selectedDepatureStationRow)")
chooseStationTableView.reloadData()
}
}

DataSource:

  • numberOfComponents:

首先需要先定義需要幾個components

  • numberOfRowsInComponent:

然後為了讓程式知道component知道該讀取哪個資料,所以運用if else去定義出當component等於0的時候,會回傳stationName.count的陣列,當不等於的時候,會回傳depatureStationName.count 的值。

extension TickerOrderViewController: UIPickerViewDataSource {
// Keyword: numberOfComponents 意思是顯示總共幾行
// 舉例: 啟程點的南港是一行,點擊到達站 顯示的pickerView裡,左營站是一行
// 所以當 return 2時,就意味著會顯示兩行(numberOfComponents)的內容。
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}

// Keyword: numberOfRowsInComponent意思是顯示一行裡面產生多少內容
// 舉例: 顯示啟程點的站名,是從南港到左營,總共11站。
// 所以當components == 0時,components的內容會從左至右產生,並且會產生stationName陣列裡面的內容。
// 反觀當components != 0時,則產生 depatureStationName 陣列裡面的內容。
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return stationName.count
} else {
return depatureStationName.count
}
}
}
toolBar / Components index

再來是建立pickerView的UI,並在pickerView的上方加上custom toolBar。

custom toolBar裡面會有 起程站 / 完成的按鈕,而這些按鈕是由UIBarButtonItem 所製作的,在點擊完成按鈕之後,pickerView會消失。

func configurePickerView () {
pickerView.backgroundColor = UIColor.white
pickerView.selectedRow(inComponent: 0)
pickerView.tintColor = Colors.orangeBrandColor

// Set up selectRow in first row component zero in pickerView.
let firstRowIndexInFirstComponent = stationName.startIndex
pickerView.selectRow(firstRowIndexInFirstComponent, inComponent: 0, animated: false)
print(firstRowIndexInFirstComponent)
// Set up selectRow in last row component one in pickerView.
let lastRowIndexInSecondComponent = depatureStationName.count - 1
pickerView.selectRow(lastRowIndexInSecondComponent, inComponent: 1, animated: false)
print(lastRowIndexInSecondComponent)

fromLocationLabel.frame = CGRect(x: 90, y: 3, width: 60, height: 20)
fromLocationLabel.text = "起程點"
fromLocationLabel.textColor = Colors.pickerViewLightColorForLabel
fromLocationLabel.font = UIFont.systemFont(ofSize: 12)

departureLabel.frame = CGRect(x: 288, y: 3, width: 60, height: 20)
departureLabel.text = "到達站"
departureLabel.textColor = Colors.pickerViewLightColorForLabel
departureLabel.font = UIFont.systemFont(ofSize: 12)

pickerView.addSubview(fromLocationLabel)
pickerView.addSubview(departureLabel)

// Custom UIBarButtonItem
let switchStationBarButtton: UIBarButtonItem = UIBarButtonItem(title: "起始站互換", style: .plain, target: self, action: #selector(switchButtonTapped))
let doneBarButton: UIBarButtonItem = UIBarButtonItem(title: "完成", style: .plain, target: self, action: #selector(doneButtonTapped))
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)

// Set up tintColor
switchStationBarButtton.tintColor = Colors.orangeBrandColor
doneBarButton.tintColor = Colors.orangeBrandColor

// customToolbar
customToolbar.barStyle = UIBarStyle.default
customToolbar.backgroundColor = .white
customToolbar.setItems([switchStationBarButtton, flexibleSpace, doneBarButton], animated: true)
customToolbar.barTintColor = .white
customToolbar.layer.cornerRadius = 10
customToolbar.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
customToolbar.layer.borderColor = UIColor.systemGray3.cgColor
customToolbar.layer.borderWidth = 0.2
customToolbar.clipsToBounds = true

constraintPickerView()
}

Reference:

  • programmatically adding button to UIToolbar
  • 點擊 起程站互換按鈕:

在一開始的站名建立的資料陣列,分別為起程站和到達站。

// Create an array named stationName for HSR station starting from Nangang.
var stationName: [String] = [
"南港","台北","板橋","桃園","新竹","苗栗","台中","彰化","雲林","嘉義","台南","左營"
]

// Create an array named depatureStationName for HSR station starting from Nangang.
var depatureStationName: [String] = [
"南港","台北","板橋","桃園","新竹","苗栗","台中","彰化","雲林","嘉義","台南","左營"
]

作法: 先將原本選取的component存到一個常數,再將定義過所選取的常數,轉換到pickerView的顯示內容上。

    @objc func switchButtonTapped (_ sender: UIBarButtonItem) {
print("switchButtonTapped")

// Store the selecteRow in pickView's components.
let selectedRowInFirstComponent = pickerView.selectedRow(inComponent: 0)
let selectedRowInSecondComponent = pickerView.selectedRow(inComponent: 1)

print("selectedRowInFirstComponent is \(selectedRowInFirstComponent),",
"selectedRowInSecondComponent is \(selectedRowInSecondComponent)"
)

// selectRow change to other components.
pickerView.selectRow(selectedRowInSecondComponent, inComponent: 0, animated: true)
pickerView.selectRow(selectedRowInFirstComponent, inComponent: 1, animated: true)

selectedFromStationRow = depatureStationName[selectedRowInSecondComponent]
selectedDepatureStationRow = stationName[selectedRowInFirstComponent]

pickerView.reloadAllComponents ()
chooseStationTableView.reloadData()
}
  • 點擊 完成按鈕。

作法: 點擊按鈕之後,pickerView會消失,並且將tabBarController的tabBar隱藏。

    @objc func doneButtonTapped (_ sender: UIBarButtonItem) {
pickerStackView.removeFromSuperview()
chooseStationTableView.reloadData ()
print("doneButtonTapped")
self.tabBarController?.tabBar.isHidden = false
}

最後再來設定pickerView的Auto-Layout。

    func constraintPickerView () {
view.addSubview(pickerView)
view.addSubview(customToolbar)

customToolbar.heightAnchor.constraint(equalToConstant: 50).isActive = true
pickerView.heightAnchor.constraint(equalToConstant: 250).isActive = true

pickerStackView.addArrangedSubview(customToolbar)
pickerStackView.addArrangedSubview(pickerView)

pickerStackView.axis = .vertical
pickerStackView.spacing = 0
pickerStackView.distribution = .fill

pickerStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pickerStackView)
NSLayoutConstraint.activate([
pickerStackView.leadingAnchor.constraint(equalTo: view .leadingAnchor),
pickerStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
pickerStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
pickerStackView.heightAnchor.constraint(equalToConstant: 300)
])
}

Reference:

  • Set PickerView
  • Make UIPickerView in front of Tab Bar
  • 建立tableView & tableViewCell 以及點擊tableViewCell之後產生AlertController

在點擊不同tableViewCell之後,會根據不同的條件跳alertViewController,但首先要先建立不同的tableView,因為有四個tableView,所以在dataSource & delegate的定義上,會跟單純只有一個tableView設定方法會稍嫌有點不同,必須多增加if else的寫法,去讓 dataSource去找到不同tableView的內容。

dataSource裡面有numberOfRowsInSection & cellForRowAt 兩種內容,兩個內容在之前的文章有提過~

  • numberOfRowsInSection
// MARK: - Extension for tableView dataSource:
extension TickerOrderViewController: UITableViewDataSource {

// 用if else去呈現不同的tableView,並用return返回,顯示不同數值列的內容。
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == trainStatusTableView {
return 1
} else if tableView == chooseStationTableView {
return 1
} else if tableView == searchTableView {
return 1
} else if tableView == serviceTableView {
print("servicesData.count is \(servicesData.count) qty")
return servicesData.count
} else {
return 0
}
}
  • cellForRowAt:

if else的寫法讓tableView找到所對應的tableView,並用guard let的寫法找到dequeueReusableCell去找到我所建立好的tableViewCell,如果沒有,則找到回傳fatalError

   // MARK: - cellForRowAt
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

if tableView == trainStatusTableView {
guard let trainStatusTableViewCell = trainStatusTableView.dequeueReusableCell(withIdentifier: TrainStatusTableViewCell.identifier, for: indexPath) as? TrainStatusTableViewCell else {
fatalError("Unable to dequeue Resable trainStatusTableViewCell")
}
// Set up selectionStyle.
trainStatusTableViewCell.selectionStyle = .none
return trainStatusTableViewCell

} else if tableView == chooseStationTableView {
guard let chooseStationTableViewCell = chooseStationTableView.dequeueReusableCell(withIdentifier: ChooseStationTableViewCell.identifier, for: indexPath) as? ChooseStationTableViewCell else {
fatalError("Unable to dequeue Resable chooseStationTableViewCell")
}
// Set up selectionStyle, setTitle.
chooseStationTableViewCell.selectionStyle = .none
chooseStationTableViewCell.fromStationButton.setTitle(selectedFromStationRow, for: .normal)
chooseStationTableViewCell.departureStationButton.setTitle(selectedDepatureStationRow, for: .normal)

// Add Target.
chooseStationTableViewCell.fromStationButton.addTarget(self, action: #selector(showFromLocationPickerView), for: .touchUpInside)
chooseStationTableViewCell.departureStationButton.addTarget(self, action: #selector(showDepartureLocationPickerView), for: .touchUpInside)

chooseStationTableViewCell.ticketStatusSegmentendControl.selectedSegmentIndex = 0
chooseStationTableViewCell.ticketStatusSegmentendControl.isEnabled = true

return chooseStationTableViewCell

} else if tableView == searchTableView {

guard let searchTableViewCell = searchTableView.dequeueReusableCell(withIdentifier: SearchTableViewCell.identifier, for: indexPath) as? SearchTableViewCell else {
fatalError("Unable to dequeue Resable searchTableViewCell")
}
// Set up selectionStyle.
searchTableViewCell.selectionStyle = .none

// Add Target
searchTableViewCell.searchButton.addTarget(self, action: #selector(searchButtonTapped), for: .touchUpInside)
return searchTableViewCell

} else if tableView == serviceTableView {

guard let serviceTableViewCell = serviceTableView.dequeueReusableCell(withIdentifier: ServiceTableViewCell.identifier, for: indexPath) as? ServiceTableViewCell else {
fatalError("Unable to dequeue Resable serviceSelectionTableViewCell")
}

// Set up selectionStyle, text, image.
serviceTableViewCell.selectionStyle = .none
serviceTableViewCell.accessoryType = .disclosureIndicator
serviceTableViewCell.serviceStatusLabel.text = servicesData[indexPath.row].service
serviceTableViewCell.serviceImageView.image = servicesData[indexPath.row].serviceIcon
serviceTableViewCell.statusLabel.text = servicesData[indexPath.row].subtitleService
return serviceTableViewCell

} else {
print("Nothing")
}
tableView.reloadData()
return UITableViewCell()
}
}

Reference:

My GitHubs:

--

--