HW#43 World Time by using tableView

Hi everyone,

Today we’ll talk about how to build a project by using tableView to show the world time and time difference.

ScreenShot from iPhone (Left) / Simulator (Right)

The skill we’re training today is:

  • TableView
  • Data structure
  • TableView Cell (By using Nib)
  • Search Controller
  • Swipe Action
  • TimeZone Calculation
  • Auto-Layout
  • Xcode 15 Preview

1. Set up TableView in view controller:

Create a tableView called tableView.

    static var tableView: UITableView   = UITableView ()

Create a function named setupTableView, register the WorldTimeTableViewCell using a Nib, and set up properties such as rowHeight, backgroundColor, separatorColor, and separatorStyle.

 func setupTableView () {
WorldTimeTableViewController.tableView.rowHeight = 105
WorldTimeTableViewController.tableView.backgroundColor = SystemColor.black
WorldTimeTableViewController.tableView.separatorColor = SystemColor.darkGray
WorldTimeTableViewController.tableView.separatorStyle = .singleLine
WorldTimeTableViewController.tableView.register(WorldTimeTableViewCell.nib(), forCellReuseIdentifier: WorldTimeTableViewCell.identifer)
}

Set up tableView’s auto layout.

    func configureTableView () {
view.addSubview(WorldTimeTableViewController.tableView)
WorldTimeTableViewController.tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
WorldTimeTableViewController.tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
WorldTimeTableViewController.tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
WorldTimeTableViewController.tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
WorldTimeTableViewController.tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}

Set up tableView delegate & dateSource.

func addDelegateAndDataSource () {
WorldTimeTableViewController.tableView.delegate = self
WorldTimeTableViewController.tableView.dataSource = self
}

I’m using extension for writing UITableViewDataSource & UITableViewDelegate , also set up numberOfRowsInSection & cellForRowAt in the same time.

numberOfRowsInSection which means how many rows in section.

意思在 UITableView 的每個 section 中有多少行(rows)。

cellForRowAt which means asks the data source for a cell to insert in a particular location of the table view.

意思是為每一行(row)配置和返回一個 UITableViewCell。

extension WorldTimeTableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return timeInfoData.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: WorldTimeTableViewCell.identifer, for: indexPath) as? WorldTimeTableViewCell else {
fatalError("Unable to dequeueReusableCell")
}

cell.timeDifferenceLabel.text =
timeInfoData[indexPath.row].jetLag
cell.dateStatusLabel.text =
timeInfoData[indexPath.row].dateStatus

cell.cityNameLabel.text = timeInfoData[indexPath.row].cityName
cell.nowTimeLabel.text = timeInfoData[indexPath.row].currentTime
cell.selectionStyle = .none

return cell
}

}

Reference:

Set up UITableViewDelegate , set up heightForRowAt as height for row.

extension WorldTimeTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 105
}
}

Also set up delegate by usingtrailingSwipeActionsConfigurationForRowAt when you swipe to trailing(Right) to leading(Left), it will delete the row.

It’s a closure for handing style destructive which means

func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteRow = UIContextualAction(style: .destructive, title: "delete") { (action, view, completionHandler) in
timeInfoData.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .left)
completionHandler(true)
}
return UISwipeActionsConfiguration(actions: [deleteRow])
}

Reference:

2. How to get Time Data and create time data by calculation:

Create a struct named TimeInfo and set up currentTime, jetLag, cityName, dateStatus properties as string.

struct TimeInfo {
let currentTime: String
let jetLag: String
let cityName: String
let dateStatus: String
}

🌟If we wanna to get current time you need to use the TimeZone.knownTimeZoneIdentifiers to get the all the timeZone.

This is main key to get all the time zone.

What I test in Swift playground.
let timeZone = TimeZone.knownTimeZoneIdentifiers.description
let regions: [Locale.Region] = Locale.Region.isoRegions
let specificRegion = Locale.Region.taiwan

The getCityName function in Swift is designed to extract and format the city name from a given timezone identifier string.

/*
* Using this line of code to separate components from a string.
>>> let components = cityName.split(separator: "/")
EXPLAIN: seprate the cityName like "Europe/London" turns to "Europe London".

* Using this line of code to get cityName.
>>> guard components.count > 1 else { return cityName }
EXPLAIN: To get "Europe London" to London.(components[0] is "Europe", components[1] is "London".

* Using this line of code to store new variable .
>>> var city = String(components[1])
EXPLAIN: And store the components by Substring into the variable and transfer components[1] to string called "city".

>>> if city.contains("_") {
city = city.replacingOccurrences(of: "_", with: "")
}
return city
}

EXPLAIN: if city contains "_", it'll replace from "_" to " ".
And return new variable called city.

*/

// Get City name from Components.
func getCityName(cityName: String) -> String {
let components = cityName.split(separator: "/")
guard components.count > 1 else { return cityName }

var city = String(components[1])

if city.contains("_") {
city = city.replacingOccurrences(of: "_", with: "")
}
return city
}

Get the current time and set up time format by using dateFormatter to return the currentTime.

// Get currentTime from now by using dateFormatter.
func getCurrentTime(city: String) -> String {
let now = Date.now
let dateFormatter = DateFormatter()
dateFormatter.timeStyle = .short
dateFormatter.timeZone = TimeZone(identifier: city)
let currentTime = dateFormatter.string(from: now)
return currentTime
}

The function calculateTheDate compares the current date in a specified city's timezone with the current date in the device's timezone to determine if it's the same day (Today), the next day (Tomorrow), or the previous day (Yesterday). If the timezone for the specified city cannot be determined or the days' difference is other than -1, 0, or 1, appropriate error messages or an empty string is returned.

// Get the date and define which day is
func calculateTheDate(city: String) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.timeZone = .current

let currentTime = dateFormatter.string(from: Date())
dateFormatter.timeZone = TimeZone(identifier: city)

let comparedTimeZone = dateFormatter.string(from: Date())
let calendar = Calendar.current

guard let currentTimeZoneDate = dateFormatter.date(from: currentTime),
let comparedTimeZoneDate = dateFormatter.date(from: comparedTimeZone) else {
return "Unable to conver date's data."
}

let dateComponentCurrentInfo = calendar.dateComponents([.year, .month, .day], from: currentTimeZoneDate)
let dateComponentCamparedInfo = calendar.dateComponents([.year, .month, .day], from: comparedTimeZoneDate)

guard let daysDifference = calendar.dateComponents([.day], from: dateComponentCurrentInfo, to: dateComponentCamparedInfo).day else {
return "Unable to calucate the daysDifference"
}

switch daysDifference {
case 0 :
return "Today,"
case 1 :
return "Tomorrow,"
case -1:
return "Yesterday,"
default:
return ""
}
}

Create a function named calculateTimeDifference, calculate the time difference in hours between the current timezone of the device and the timezone of a specified city.

func calculateTimeDifference(city: String) -> String {
// Initialize City's Timezone:
guard let cityTimeZone = TimeZone(identifier: city) else {
return "Unable to get city's timeZone."
}
// Get Current Timezone:
let currentTime = TimeZone.current
// Calculate Time Difference in Seconds:
let timeDifference = cityTimeZone.secondsFromGMT() - currentTime.secondsFromGMT()
// Convert Seconds to Hours:
let hoursDifference = timeDifference / 3600
// Format and Return Result:
return String(format: "%+dHRS", hoursDifference)
}

To create an array timeInfoData with four TimeInfo structures representing different time zones from the allTimeZone array, you can do the following. Assume that TimeInfo is a struct with properties currentTime, jetLag, cityName, and dateStatus, and that you have functions getCurrentTime, calculateTimeDifference, getCityName, and calculateTheDate to get the respective values.

var timeInfoData = [TimeInfo(currentTime: getCurrentTime(city: allTimeZone[0]),
jetLag: calculateTimeDifference(city: allTimeZone[0]),
cityName: getCityName(cityName: allTimeZone[0]),
dateStatus: calculateTheDate(city: allTimeZone[0]))
]


let allTimeZone: [String] = ["Africa/Abidjan", "Africa/Accra", "Africa/Addis_Ababa", "Africa/Algiers", "Africa/Asmara", "Africa/Bamako", "Africa/Bangui", "Africa/Banjul", "Africa/Bissau", "Africa/Blantyre", "Africa/Brazzaville", "Africa/Bujumbura", "Africa/Cairo", "Africa/Casablanca", "Africa/Ceuta", "Africa/Conakry", "Africa/Dakar", "Africa/Dar_es_Salaam", "Africa/Djibouti", "Africa/Douala", "Africa/El_Aaiun", "Africa/Freetown", "Africa/Gaborone", "Africa/Harare", "Africa/Johannesburg", "Africa/Juba", "Africa/Kampala", "Africa/Khartoum", "Africa/Kigali", "Africa/Kinshasa", "Africa/Lagos", "Africa/Libreville", "Africa/Lome", "Africa/Luanda", "Africa/Lubumbashi", "Africa/Lusaka", "Africa/Malabo", "Africa/Maputo", "Africa/Maseru", "Africa/Mbabane", "Africa/Mogadishu", "Africa/Monrovia", "Africa/Nairobi", "Africa/Ndjamena", "Africa/Niamey", "Africa/Nouakchott", "Africa/Ouagadougou", "Africa/Porto-Novo", "Africa/Sao_Tome", "Africa/Tripoli", "Africa/Tunis", "Africa/Windhoek", "America/Adak", "America/Anchorage", "America/Anguilla", "America/Antigua", "America/Araguaina", "America/Argentina/Buenos_Aires", "America/Argentina/Catamarca", "America/Argentina/Cordoba", "America/Argentina/Jujuy", "America/Argentina/La_Rioja", "America/Argentina/Mendoza", "America/Argentina/Rio_Gallegos", "America/Argentina/Salta", "America/Argentina/San_Juan", "America/Argentina/San_Luis", "America/Argentina/Tucuman", "America/Argentina/Ushuaia", "America/Aruba", "America/Asuncion", "America/Atikokan", "America/Bahia", "America/Bahia_Banderas", "America/Barbados", "America/Belem", "America/Belize", "America/Blanc-Sablon", "America/Boa_Vista", "America/Bogota", "America/Boise", "America/Cambridge_Bay", "America/Campo_Grande", "America/Cancun", "America/Caracas", "America/Cayenne", "America/Cayman", "America/Chicago", "America/Chihuahua", "America/Ciudad_Juarez", "America/Costa_Rica", "America/Creston", "America/Cuiaba", "America/Curacao", "America/Danmarkshavn", "America/Dawson", "America/Dawson_Creek", "America/Denver", "America/Detroit", "America/Dominica", "America/Edmonton", "America/Eirunepe", "America/El_Salvador", "America/Fort_Nelson", "America/Fortaleza", "America/Glace_Bay", "America/Godthab", "America/Goose_Bay", "America/Grand_Turk", "America/Grenada", "America/Guadeloupe", "America/Guatemala", "America/Guayaquil", "America/Guyana", "America/Halifax", "America/Havana", "America/Hermosillo", "America/Indiana/Indianapolis", "America/Indiana/Knox", "America/Indiana/Marengo", "America/Indiana/Petersburg", "America/Indiana/Tell_City", "America/Indiana/Vevay", "America/Indiana/Vincennes", "America/Indiana/Winamac", "America/Inuvik", "America/Iqaluit", "America/Jamaica", "America/Juneau", "America/Kentucky/Louisville", "America/Kentucky/Monticello", "America/Kralendijk", "America/La_Paz", "America/Lima", "America/Los_Angeles", "America/Lower_Princes", "America/Maceio", "America/Managua", "America/Manaus", "America/Marigot", "America/Martinique", "America/Matamoros", "America/Mazatlan", "America/Menominee", "America/Merida", "America/Metlakatla", "America/Mexico_City", "America/Miquelon", "America/Moncton", "America/Monterrey", "America/Montevideo", "America/Montreal", "America/Montserrat", "America/Nassau", "America/New_York", "America/Nipigon", "America/Nome", "America/Noronha", "America/North_Dakota/Beulah", "America/North_Dakota/Center", "America/North_Dakota/New_Salem", "America/Nuuk", "America/Ojinaga", "America/Panama", "America/Pangnirtung", "America/Paramaribo", "America/Phoenix", "America/Port-au-Prince", "America/Port_of_Spain", "America/Porto_Velho", "America/Puerto_Rico", "America/Punta_Arenas", "America/Rainy_River", "America/Rankin_Inlet", "America/Recife", "America/Regina", "America/Resolute", "America/Rio_Branco", "America/Santa_Isabel", "America/Santarem", "America/Santiago", "America/Santo_Domingo", "America/Sao_Paulo", "America/Scoresbysund", "America/Shiprock", "America/Sitka", "America/St_Barthelemy", "America/St_Johns", "America/St_Kitts", "America/St_Lucia", "America/St_Thomas", "America/St_Vincent", "America/Swift_Current", "America/Tegucigalpa", "America/Thule", "America/Thunder_Bay", "America/Tijuana", "America/Toronto", "America/Tortola", "America/Vancouver", "America/Whitehorse", "America/Winnipeg", "America/Yakutat", "America/Yellowknife", "Antarctica/Casey", "Antarctica/Davis", "Antarctica/DumontDUrville", "Antarctica/Macquarie", "Antarctica/Mawson", "Antarctica/McMurdo", "Antarctica/Palmer", "Antarctica/Rothera", "Antarctica/South_Pole", "Antarctica/Syowa", "Antarctica/Troll", "Antarctica/Vostok", "Arctic/Longyearbyen", "Asia/Aden", "Asia/Almaty", "Asia/Amman", "Asia/Anadyr", "Asia/Aqtau", "Asia/Aqtobe", "Asia/Ashgabat", "Asia/Atyrau", "Asia/Baghdad", "Asia/Bahrain", "Asia/Baku", "Asia/Bangkok", "Asia/Barnaul", "Asia/Beirut", "Asia/Bishkek", "Asia/Brunei", "Asia/Calcutta", "Asia/Chita", "Asia/Choibalsan", "Asia/Chongqing", "Asia/Colombo", "Asia/Damascus", "Asia/Dhaka", "Asia/Dili", "Asia/Dubai", "Asia/Dushanbe", "Asia/Famagusta", "Asia/Gaza", "Asia/Harbin", "Asia/Hebron", "Asia/Ho_Chi_Minh", "Asia/Hong_Kong", "Asia/Hovd", "Asia/Irkutsk", "Asia/Jakarta", "Asia/Jayapura", "Asia/Jerusalem", "Asia/Kabul", "Asia/Kamchatka", "Asia/Karachi", "Asia/Kashgar", "Asia/Kathmandu", "Asia/Katmandu", "Asia/Khandyga", "Asia/Krasnoyarsk", "Asia/Kuala_Lumpur", "Asia/Kuching", "Asia/Kuwait", "Asia/Macau", "Asia/Magadan", "Asia/Makassar", "Asia/Manila", "Asia/Muscat", "Asia/Nicosia", "Asia/Novokuznetsk", "Asia/Novosibirsk", "Asia/Omsk", "Asia/Oral", "Asia/Phnom_Penh", "Asia/Pontianak", "Asia/Pyongyang", "Asia/Qatar", "Asia/Qostanay", "Asia/Qyzylorda", "Asia/Rangoon", "Asia/Riyadh", "Asia/Sakhalin", "Asia/Samarkand", "Asia/Seoul", "Asia/Shanghai", "Asia/Singapore", "Asia/Srednekolymsk", "Asia/Taipei", "Asia/Tashkent", "Asia/Tbilisi", "Asia/Tehran", "Asia/Thimphu", "Asia/Tokyo", "Asia/Tomsk", "Asia/Ulaanbaatar", "Asia/Urumqi", "Asia/Ust-Nera", "Asia/Vientiane", "Asia/Vladivostok", "Asia/Yakutsk", "Asia/Yangon", "Asia/Yekaterinburg", "Asia/Yerevan", "Atlantic/Azores", "Atlantic/Bermuda", "Atlantic/Canary", "Atlantic/Cape_Verde", "Atlantic/Faroe", "Atlantic/Madeira", "Atlantic/Reykjavik", "Atlantic/South_Georgia", "Atlantic/St_Helena", "Atlantic/Stanley", "Australia/Adelaide", "Australia/Brisbane", "Australia/Broken_Hill", "Australia/Currie", "Australia/Darwin", "Australia/Eucla", "Australia/Hobart", "Australia/Lindeman", "Australia/Lord_Howe", "Australia/Melbourne", "Australia/Perth", "Australia/Sydney", "Europe/Amsterdam", "Europe/Andorra", "Europe/Astrakhan", "Europe/Athens", "Europe/Belgrade", "Europe/Berlin", "Europe/Bratislava", "Europe/Brussels", "Europe/Bucharest", "Europe/Budapest", "Europe/Busingen", "Europe/Chisinau", "Europe/Copenhagen", "Europe/Dublin", "Europe/Gibraltar", "Europe/Guernsey", "Europe/Helsinki", "Europe/Isle_of_Man", "Europe/Istanbul", "Europe/Jersey", "Europe/Kaliningrad", "Europe/Kiev", "Europe/Kirov", "Europe/Kyiv", "Europe/Lisbon", "Europe/Ljubljana", "Europe/London", "Europe/Luxembourg", "Europe/Madrid", "Europe/Malta", "Europe/Mariehamn", "Europe/Minsk", "Europe/Monaco", "Europe/Moscow", "Europe/Oslo", "Europe/Paris", "Europe/Podgorica", "Europe/Prague", "Europe/Riga", "Europe/Rome", "Europe/Samara", "Europe/San_Marino", "Europe/Sarajevo", "Europe/Saratov", "Europe/Simferopol", "Europe/Skopje", "Europe/Sofia", "Europe/Stockholm", "Europe/Tallinn", "Europe/Tirane", "Europe/Ulyanovsk", "Europe/Uzhgorod", "Europe/Vaduz", "Europe/Vatican", "Europe/Vienna", "Europe/Vilnius", "Europe/Volgograd", "Europe/Warsaw", "Europe/Zagreb", "Europe/Zaporozhye", "Europe/Zurich", "GMT", "Indian/Antananarivo", "Indian/Chagos", "Indian/Christmas", "Indian/Cocos", "Indian/Comoro", "Indian/Kerguelen", "Indian/Mahe", "Indian/Maldives", "Indian/Mauritius", "Indian/Mayotte", "Indian/Reunion", "Pacific/Apia", "Pacific/Auckland", "Pacific/Bougainville", "Pacific/Chatham", "Pacific/Chuuk", "Pacific/Easter", "Pacific/Efate", "Pacific/Enderbury", "Pacific/Fakaofo", "Pacific/Fiji", "Pacific/Funafuti", "Pacific/Galapagos", "Pacific/Gambier", "Pacific/Guadalcanal", "Pacific/Guam", "Pacific/Honolulu", "Pacific/Johnston", "Pacific/Kanton", "Pacific/Kiritimati", "Pacific/Kosrae", "Pacific/Kwajalein", "Pacific/Majuro", "Pacific/Marquesas", "Pacific/Midway", "Pacific/Nauru", "Pacific/Niue", "Pacific/Norfolk", "Pacific/Noumea", "Pacific/Pago_Pago", "Pacific/Palau", "Pacific/Pitcairn", "Pacific/Pohnpei", "Pacific/Ponape", "Pacific/Port_Moresby", "Pacific/Rarotonga", "Pacific/Saipan", "Pacific/Tahiti", "Pacific/Tarawa", "Pacific/Tongatapu", "Pacific/Truk", "Pacific/Wake", "Pacific/Wallis"]

3. Custom tableViewCell by using Nib file:

CitySelectionTableViewCell:

Using static let to let rest of view controller easy access CitySelectionTableViewCell .

static let identifer: String = "CitySelectionTableViewCell"

Create a static function named nib, and return UINib object.

static func nib() -> UINib {
return UINib(nibName: "CitySelectionTableViewCell", bundle: nil)
}

Here is the full code for CitySelectionTableViewCell.

import UIKit

class CitySelectionTableViewCell: UITableViewCell {

static let identifer: String = "CitySelectionTableViewCell"

@IBOutlet weak var countryLabel: UILabel!

override func awakeFromNib() {
super.awakeFromNib()
// Initialization code

self.backgroundColor = SystemColor.citySelectionBackgroundColor

countryLabel.text = "Taiwan / Taipei"
countryLabel.textColor = SystemColor.white
countryLabel.textAlignment = .left
countryLabel.numberOfLines = 0
}

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

// Configure the view for the selected state
}

static func nib() -> UINib {
return UINib(nibName: "CitySelectionTableViewCell", bundle: nil)
}

}

Set up Auto-Layout:

Due to label’s text will be much longer than we thought, so we need to set up the width to equal or greater than status.

And set up the label to centerY to horizontal to tableViewCell, and set up spacing -2 between the view’s leading.

so that we could make sure the label won’t mess around, it could fix in leading of view even the context is much longer than we think and the label won’t go away.

WorldTimeTableViewCell:

I’m using same way for doing WorldTimeTableViewCell, pretty much the same way for doing this, even the set up UI.

import UIKit

class WorldTimeTableViewCell: UITableViewCell {

static let identifer: String = "WorldTimeTableViewCell"

@IBOutlet weak var dateStatusLabel: UILabel!
@IBOutlet weak var timeDifferenceLabel: UILabel!
@IBOutlet weak var cityNameLabel: UILabel!
@IBOutlet weak var nowTimeLabel: UILabel!

override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.backgroundColor = SystemColor.black

nowTimeLabel.adjustsFontSizeToFitWidth = true
cityNameLabel.adjustsFontSizeToFitWidth = true
dateStatusLabel.adjustsFontSizeToFitWidth = true
}

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

static func nib() -> UINib {
return UINib(nibName: "WorldTimeTableViewCell", bundle: nil)
}
}

4. Configure and set up Search Controller

  1. Create a searchController.
    let searchController: UISearchController = UISearchController(searchResultsController: nil)

2. Set up searchController in navigationItem’s searchController, and set up multiple option for searchController & searchBar in seachController.

func configureSearchController () {

let prompt: String = "Change Country"

let scrollEdgeAppearance = self.navigationItem.scrollEdgeAppearance
self.navigationController?.navigationItem.scrollEdgeAppearance = scrollEdgeAppearance

self.navigationItem.searchController = searchController
self.navigationItem.hidesSearchBarWhenScrolling = false
searchController.hidesNavigationBarDuringPresentation = true
searchController.automaticallyShowsCancelButton = true

searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false

searchController.searchBar.placeholder = "Search"
searchController.searchBar.prompt = prompt
searchController.searchBar.tintColor = SystemColor.orange
searchController.searchBar.returnKeyType = .go
searchController.searchBar.barStyle = .black
searchController.searchBar.showsCancelButton = true
searchController.searchBar.isTranslucent = false
searchController.searchBar.searchBarStyle = .default

searchController.searchBar.searchTextField.textColor = SystemColor.white


searchController.isActive = true
definesPresentationContext = true

print("configureSearchBar success")
}

3. Set up UISearchBarDelegate:

Basically all the function was followed Apple’s app even the tintColor and showCancelButton method.

And set up when user tapped the cancelButton, the view will dismiss.

extension CountryTableViewController: UISearchControllerDelegate, UISearchBarDelegate  {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchBar.showsCancelButton = true
searchBar.tintColor = SystemColor.orange
}

func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
searchBar.showsCancelButton = true
searchBar.tintColor = SystemColor.orange
}

func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
self.dismiss(animated: true)
}

4. Set up UISearchResultsUpdating:

extension CountryTableViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
if let searchText = searchController.searchBar.text, searchText.isEmpty == false {
filterTimeZoneData = allTimeZone.filter ({ cellOfCountryLabelText in
cellOfCountryLabelText.description.localizedStandardContains(searchText)
})
} else {
filterTimeZoneData = allTimeZone
}
tableView.reloadData()
}
}

Reference:

5. Xcode 15 Preview:

I think Xcode 15 preview is very good way to help programmatically.

When you trying to present a view you can just directly to show the UI’s detail.

Here is the WWDC23 sample to show controller by Apple.

#Preview {
var controller = SavedCollagesController()
controller.dataSource = CollagesDataStore.sample
controller.layoutMode = .grid
return controller
}

This time I’m using very simple to show view controller.

#Preview {
UINavigationController(rootViewController: WorldTimeTableViewController())
}

Reference:

TableView:

  • Table View 的基本功能
  • Set up tableView’s rowHeight
  • dequeueReusableCell
  • 和 if let 很像的 guard let

TableView Cell:

  • Adding a separator line under custom tableview cell in UITableViewCell
  • UITableView, Separator color where to set?

Search Controller:

  • Swift — Customizing UISearchBar design programmatically

Time Calculation:

  • Swift Date Formatting: 10 Steps Guide

Auto Layout:

Xcode15 Preview:

My GitHubs Links:

--

--