Daily App 純程式畫面

Eason
彼得潘的 Swift iOS / Flutter App 開發教室
18 min readJul 28, 2023

動機:每間公司畫面創建都不太一樣,這次改用純程式把Daily App的畫面創建出來。

重點整理:
1.在 iOS 13 以前如果要用程式刻畫面需要在 AppDelegate 裡面創建一個 Window 利用程式把畫面寫出來。
2.在 iOS 13 以後需要在 SceneDelegate 裡面創建一個 Window 利用程式把畫面寫出來

在SceneDelegate裡面創建完 TabBarController 與 NavigationController 來管理之後要創建的 Controller。


//SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).


guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)

let tabBarController = MainTabBarController()
let navigationController = MainNavigationController()


let loginViewController = LoginViewController()
let firstNavigationController = UINavigationController(rootViewController:loginViewController)
loginViewController.tabBarItem = UITabBarItem(title: "Login", image: UIImage(systemName: "person.crop.rectangle"), selectedImage: nil)

let newsTableViewController = NewsTableViewController()
let secondNavigationController = UINavigationController(rootViewController: newsTableViewController)
newsTableViewController.tabBarItem = UITabBarItem(title: "News", image: UIImage(systemName: "newspaper"), selectedImage: nil)

let weatherViewController = WeatherViewController()
let thirdNavigationController = UINavigationController(rootViewController: weatherViewController)
weatherViewController.tabBarItem = UITabBarItem(title: "Weather", image: UIImage(systemName: "cloud"), selectedImage: nil)

let googleMapTableViewController = GoogleMapTableViewController()
let fourthNavigationController = UINavigationController(rootViewController: googleMapTableViewController)
googleMapTableViewController.tabBarItem = UITabBarItem(title: "GoogleMap", image: UIImage(systemName: "map"), selectedImage: nil)


tabBarController.viewControllers = [secondNavigationController,thirdNavigationController,fourthNavigationController,firstNavigationController]

window?.rootViewController = tabBarController
window?.makeKeyAndVisible()
}

以及專案 > Info 底下的 Main storyboard file base name 要把它移除

與 Application Scene Manifest > Scene Configuration > Application Sesssion Role > iten 0 > Storyboard Name 移除

LoginViewController 在 viewDidLoad 把元件加入到畫面上。

給 View 加入一個背景圖片

 // 設置視圖背景為圖片
let backgroundImage = UIImage(named: "pic")
let imageView = UIImageView(frame: view.bounds)
imageView.image = backgroundImage
imageView.contentMode = .scaleAspectFill
view.addSubview(imageView)
view.sendSubviewToBack(imageView)

利用 SnapKit 套件進行排版

 //帳號
let Account = UILabel()
Account.text = "帳號:"
Account.font = UIFont.systemFont(ofSize: 20)
Account.textColor = .black
view.addSubview(Account)
Account.translatesAutoresizingMaskIntoConstraints = false

Account.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).inset(40)
make.left.equalToSuperview().inset(30)
make.width.equalTo(100)
make.height.equalTo(30)
}

利用程式設定 Button 的 event ,使用Anchor 排版

//登入 Button
let LoginButton = UIButton()
LoginButton.setTitle("登入", for: .normal)
LoginButton.addTarget(self, action: #selector(goLogin), for: .touchUpInside)

LoginButton.backgroundColor = UIColor.orange
LoginButton.setTitleColor(.white, for: .normal)
LoginButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
LoginButton.layer.cornerRadius = 8.0

LoginButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(LoginButton)

LoginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
LoginButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 80).isActive = true
LoginButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 30).isActive = true
LoginButton.widthAnchor.constraint(equalToConstant: 200).isActive = true
LoginButton.heightAnchor.constraint(equalToConstant: 40).isActive = true

@objc func goLogin(_ sender: Any) {
}

NewsTableViewController 上加入 Cell 需要註冊 Cell 以及在 Cell 裡面排版

NewsTableViewCell 內的元件以及排版, Cell需要把元件加入到contentView的上面。

並且在寫在
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?)

    let InnerLabel: UILabel = {
let InnerLabel = UILabel()
InnerLabel.translatesAutoresizingMaskIntoConstraints = false
InnerLabel.textAlignment = .left
InnerLabel.numberOfLines = 0 // 設定為 0 表示允許多行文字
InnerLabel.lineBreakMode = .byWordWrapping // 根據單詞換行
return InnerLabel
}()

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)

// Add the customLabel and customImageView to the cell's contentView
contentView.addSubview(InnerLabel)
contentView.addSubview(NewsImage)

// Set AutoLayout constraints for customLabel
NSLayoutConstraint.activate([
InnerLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
InnerLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
InnerLabel.widthAnchor.constraint(equalToConstant: 200),
InnerLabel.heightAnchor.constraint(equalToConstant: 80)
])


}

NewsTableViewController裡面註冊Cell。

self.tableView.rowHeight = 120
self.tableView.register(NewsTableViewCell.self, forCellReuseIdentifier: "NewsTableViewCell")

TableViewController要在底下的 func didSelectRowAt 設定點選後要做什麼事情。

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let url = URL(string: articles[indexPath.row].url) else {return}
let vc = SFSafariViewController(url: url)
present(vc, animated: true)
}

GoogleMapTableViewController 加入一個 SreachBar

    func addSearchBar() {
let searchBar = UISearchBar(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 44))
tableView.tableHeaderView = searchBar
}

一樣註冊一個已經排版好的 Cell 以及設定好 delegate

tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = 130
tableView.register(GoogleMapTableViewCell.self, forCellReuseIdentifier: "GoogleMapTableViewCell")

GoogleMapTableViewController 傳送資料給 DetailViewController

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("HELLOO")
guard let controller = DetailViewController() as? DetailViewController else {
print("I'm die")
return

}
controller.mapInfos = mapInfo[indexPath.row]
print(mapInfo[indexPath.row])
navigationController?.pushViewController(controller, animated: true)
}

卡最久的地方來了,DetailViewController 接收 GoogleMapTableViewController傳過來的資料,要跟原畫面一樣,一個 ViewController 上面要有 CollectionView 與 TableView,一開始一直無法顯示於是問了 GPT。

GPT 說明:

在 Swift 中,變數的聲明和初始化是分開的步驟。當你聲明一個變數時,你可以選擇性地賦予它一個初始值,也可以在稍後的時間點進行初始化。

var photoCollection = UICollectionView() 是一個將 photoCollection 初始化為一個空的 UICollectionView 的聲明和初始化語句。這種初始化方式稱為"隱式解包選擇性",這意味著該變數在設置值後不需要再使用 if letguard let 進行解包,而可以直接使用 photoCollection

然而,這種初始化方式有一些風險,如果在沒有初始化的情況下直接使用該變數,會引發運行時錯誤,因為該變數是一個空的選擇性。為了避免這種情況,你需要確保在使用該變數之前,已經對其進行了初始化。

另一方面,var photoCollection: UICollectionView! 是一個聲明了 photoCollection 為一個隱式解包選擇性 UICollectionView 的語句,但是在聲明時並未進行初始化。這樣做的目的是告訴編譯器,在某些時刻這個變數可能是空的,但是在使用前,你保證會對其進行初始化,避免了直接使用空的情況。

通常,使用 var photoCollection: UICollectionView! 這種方式,是因為該變數的初始化可能在稍後的代碼中進行,或者在 Storyboard 或 Nib 文件中進行連結,因此在聲明時無法立即進行初始化。

總結來說,如果你確定會在聲明的時候就進行初始化,可以使用 var photoCollection = UICollectionView(),如果你希望稍後進行初始化,或者在 Storyboard 或 Nib 文件中進行連結,可以使用 var photoCollection: UICollectionView!,但在使用時,請確保已經對其進行了初始化。

在ViewController 上加入 CollectionView 與 TableView 需要注意順序問題

CollectionView 使用 UICollectionViewFlowLayout創建以及設定裡面的內容

let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 128, height: 128)
layout.minimumInteritemSpacing = 1
layout.estimatedItemSize = .zero
photoCollection = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.addSubview(photoCollection)
photoCollection.delegate = self
photoCollection.dataSource = self
photoCollection.translatesAutoresizingMaskIntoConstraints = false
photoCollection.register(PhotoCollectionViewCell.self, forCellWithReuseIdentifier: "PhotoCollectionViewCell")

photoCollection.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(5)
make.width.equalTo(view.bounds.width)
make.height.equalTo(300)
}

TableView 使用 UITableView(frame: .zero, style: .plain) 創建內容

        reviewsTable = UITableView(frame: .zero, style: .plain)
reviewsTable.allowsSelection = false
reviewsTable.translatesAutoresizingMaskIntoConstraints = false
reviewsTable.register(ReviewsTableViewCell.self, forCellReuseIdentifier: "ReviewsTableViewCell")
reviewsTable.rowHeight = 180
view.addSubview(reviewsTable)
reviewsTable.dataSource = self
reviewsTable.delegate = self
reviewsTable.snp.makeConstraints { make in
make.top.equalTo(DescriptionLabel.snp_bottom).offset(5)
make.width.equalTo(view.bounds.width)
make.height.equalTo(300)
}

最後成果:

--

--