30.%d Xcode — 你漏掉的生命週期 LoadView()

春麗 S.T.E.M.
11 min readDec 1, 2021

--

目錄

⦿ 程式碼刻畫面
⦿ Life Cycle
⦿ ViewController 中的多個 view
⦿ 重設一個 CustomView
⦿ 寫一個 Protocol

程式碼刻畫面

首先,要感謝下面這篇,借用了他可愛的圖。

我們看看下面這個 APP,它的首頁也是 ViewController,ViewController 下自然有一個底 View,如下:

View 上有許多東西,米花市老鼠、方塊、Image、Label 跟 Button,按下 Tap Me 這個 Button 後,會 print 出 Hello Swift!,但這個 View 並不是 ViewController 預設的 View,而是使用 loadView() 設置的另一個 View。

咦,什麼是 loadView()?

別急,先讓我們回憶一下 VC 的生命週期。

繼續閱讀|回目錄

Life Cycle

我們都熟悉 View Controller 的 Life Cycle,例如 viewDidLoad、viewWillAppear、viewWillLayoutSubviews⋯⋯,先看到如下:

VC init.
VC did load.
VC will appear.
VC will layout subviews.
VC did layout subviews.
VC did appear.
Second VC init.
Second VC did load.
VC will disappear.
Second VC will appear.
Second VC will layout subviews.
Second VC did layout subviews.
VC will layout subviews.
VC did layout subviews.
VC did disappear.
Second VC did appear.

Second VC will disappear.
VC will appear.
VC will layout subviews.
VC did layout subviews.
Second VC did disappear.
VC did appear.
Second VC deinit.

如果在 Main.storyboard 中點擊 ViewController(第一頁)的 Button,會跳到 SecondViewController(第二頁),從第二頁回到第一頁後,我們看到的生命週期就是這樣。

按下按鈕後,第二頁 init,接著 did load,此時第一頁將要消失,第二頁定下 layout,當第一頁確實消失時,第二頁才確實出現,而第二頁要消失時,動作又反過來,當第一頁確實出現時,第二頁才 deinit。

整個過程都安全地進行,這牽涉到你傳值時需注意頁面是否已經出現實例的產生,所以了解生命週期以免傳值漏接,在 iOS 開發中是相當重要的一件事。

這邊需要特別注意兩點,第一點是 layoutSubviews 在頁面間是經常呼叫的函式,第二點是,第二頁產生時會重複呼叫 viewDidLoad(),但第一頁是一直存在的,只有在第一次產生時會 viewDidLoad()。

然而,在 viewDidLoad() 之前,我們還有一個 loadView(),它也是生命週期的一部份,先看到如下:

// The getter first invokes [self loadView] if the view hasn't been set yet. Subclasses must call super if they override the setter or getter.
@property(null_resettable, nonatomic,strong) UIView *view;
// This is where subclasses should create their custom view hierarchy if they aren't using a nib. Should never be called directly.
- (void)loadView;

這裡告訴我們,在 ViewController 這個 view 尚未設定前,存取這個 view 時會自動調用 loadView,如果你不想在 Storyboard 或 xib 來創建 view,那麼就會使用程式碼來重寫 loadView,重寫 loadView 有什麼好處呢?

繼續閱讀|回目錄

ViewController 中的多個 view

如果我們都用程式碼刻畫面,這些 UI 看起來很簡單,但其實一下子就塞到一百幾十行了,光宣告基本的 UIElement 就有點累,如下:

一般而言,每個元件的基本條件是位置大小,再來我們會設置顏色,如果是 ImageView,我們會設定 image,如果是 Label,我們設定 text。

在設定 NSLayoutConstraint.activate([]),我們會先將這些 UIElement 的 translatesAutoresizingMaskIntoConstraints 設定為 false,如果你是在 Storyboard 中操作,本來自動推斷 Constraint(Auto Resizing)也會自動取消,變成你設置的約束條件,但程式碼中,我們必須要記得將之設定為 false。

接著看到如下:

我們可以分別寫這些 UIElement 的 setup,再把這些 setup 放到一個 setupUI 裡面,最後只要在 viewDidLoad 去呼叫 setupUI 即可。

用程式碼刻畫面,記得讓底 View 呼叫 .addsubview;相對而言,這些 UIElement,Button 要做的事比較多,如設置 title、圓角、還有 Button Action,Button Action 是按下 Button 後會呼叫 selecter,selecter 會放入一個 Objective-C 的方法。

再來,我們還必須寫一大堆 UIBezierPath,最後,設定完 UI,在 View Controller 中還會加入許多 function。

想像一下,頁面有許多點擊事件,事件中有跳轉頁面的邏輯,有改變某個 UIElement 的狀態或 UI 呈現,將游標放在 Textfiled 上彈出日期選擇器,或是鍵盤,按下 RightBarButton 開始編輯列表⋯⋯。

等等,這太麻煩了吧?我們希望把 UI 及 UI 的點擊事件拆分出去,不然 View Controller 會變得很龐大,當龐大的程式碼塞在一個 VC 中,也表示著該 VC 不容易維護。

下面我們來把 View 上的 UIElement 丟出去,重新形成一個 UIView。

繼續閱讀|回目錄

重設一個 CustomView

我們希望 VC 可以整理成這樣:

class ViewController: UIViewController, HasCustomView {
typealias CustomView = MyView
override func viewDidLoad() {
super.viewDidLoad()

let customView = CustomView()
customView.myButton.addTarget(
self,
action: #selector(buttonTapped),
for: .touchUpInside
)
view = customView

}
@objc func buttonTapped() {
print("Hello Swift!")
}
}

我們當然知道 CustomView 可以拆出去,不過這種設定方式仍然是在 viewDidLoad 中進行,最後我們希望程式碼可以整理成如下:

class ViewController: UIViewController, HasCustomView {
typealias CustomView = CombineView

override func loadView() {
let customView = CustomView()
view = customView
}
}

沒了,就這樣,很簡潔。

但要怎麼做呢?

寫一個 Protocol

參照如下:

associatedtype 可說是 protocol 中的泛型,當你遵循這個 protocol,就必須定義這個 CustomView,CustomView 並不是真有這個型別了,是當你設定後才有,我們可以把原先所有的 UI 跟 Action 都放到一個新的 CombineView 中,因為 CustomView 是 UIView,所以 CombineView 也得是 UIView。

而因為 extension,所以在 UIViewController 下的 customView 就不再需要透過轉型處理了,在我們定下 CombineView 時,就可以直接用 CustomView 生成 CombineView,並將其賦值給底 View。

並且這是在 loadView 中設置的。

那麼,所有原先在 VC 中的東西都可以順勢搬到 CombineView 上,只不過畫貝茲曲線就要在 :

override func draw(_ rect: CGRect) { }

去運作了。而本來在 viewDidLoad 中做的事,搬到 loadView 只需要做到小幅度的改動,如下:

override func loadView() {
let myView = MyView()
myView.myButton.addTarget(
self,
action: #selector(buttonTapped),
for: .touchUpInside
)
view = myView
}

那麼,將 UI 呈現交還給 View,甚至 View 上的一些元件基本控制邏輯都可放在 View 中,而 VC 可專心負責跳轉邏輯,call API 或其他事情。

完成了!

這次就分享到這,感謝您的閱讀。

繼續閱讀|回目錄

以下 Reference:

附上 GitHub:

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

No responses yet