[譯]純程式碼建立 UIView

嘎嘣脆發表於2019-03-03

[譯]純程式碼建立 UIView

翻譯自:

https://medium.com/written-code/creating-uiviews-programmatically-in-swift-55f5d14502ae/

讀完這篇文章,你能得到啥?

  • 瞭解 iOS 的螢幕構成
  • 檢視關係
  • 何時適合使用程式碼方式構建檢視
  • MVC 模式下,該如何組織程式碼
  • 自定義 UIView
  • 一個 Twitter iOS App 作為示例
  • 避免構建 Massive View Controller
  • 使用 PureLayout 構建約束

瞭解 iOS 螢幕

iOS App 由許多檢視組成。檢視的顯示依賴四個值:x,y,width,height。

檢視的基本構成

三種方式構建檢視:StoryboardsNib files編碼實現

UIKit 包含許多標準元件,從簡單的按鈕,到複雜的表格。他們用處廣泛,如 UILabel 物件繪製文字字串,UIImageView 物件繪製影象。

檢視可以被嵌入到其他檢視,從而在檢視之間產生父子檢視關係,一個檢視的父檢視被稱為superview,子檢視被稱為subview

檢視關係

檢視關係

如何組織你的檢視關係著你的應用程式的視覺效果和事件行為。舉一個例子,有兩個檢視,他們的父子關係決定了如何捕獲事件及響應事件的順序。類似的,當手機方向發生變化,檢視的父子關係也決定了他們做如何修改。

何時使用程式碼形式構建檢視

以下情景,通常都是適合使用程式碼構建檢視的情況:

  • 動態佈局
  • 檢視需要實現一些效果,如圓角,陰影這類
  • 任何你感覺使用 Storyboard 實現會複雜的時候

如何組織程式碼(MVC 模式)

Model-View-Controller 是最常用的設計模式。然而在 iOS App 開發過程中,通常要面臨一個問題:檢視控制器常常變得過於龐大,修改和重構都很痛苦。所以 MVC 也被戲稱為Massive View Controller

遵循此模式,我們應該儘量確保專案中的每個類都是Controller、Model或者View。這能有效避免程式碼失控。我們也可以建立其他的分組和類,但 App 的核心部分應該是這三種組成。

目錄組織

準備

建立專案的時候,Xcode 會自動為我們增加一個 storyboard。為了展示自定義檢視,我們幹掉他先。

然後,建立兩個檔案:ProfileView 繼承自 UIView,放到 View 分類中。ProfileViewController,繼承自 UIViewController,放在 Controller 分類中。

Auto Layout

Auto Layout 決定了螢幕上檢視的 frame。每個檢視都包含約束條件,通過這些條件來計算出檢視的 width,height,x,y。直接編寫 Auto Layout 程式碼並不容易,這裡我們使用 PureLayout,它提供了功能強大,使用友好的介面來幫助我們編寫 Auto Layout

首先新增 PureLayout 到你的專案中。我使用 CocoaPods 進行包管理,它依賴 Podfile 檔案:

platform :ios, '8.0'
use_frameworks!
pod 'PureLayout', '~> 2.0.5'
複製程式碼

執行程式碼以安裝依賴:

pod install
複製程式碼

這條命令將建立一個以.xcworkspace為副檔名的新的工程檔案。現在,使用 Xcode 開啟它。

Building custom classes

ProfileView.swift 檔案當前是一個自定義 UIView 類的模板:

import UIKit
class ProfileView: UIView {

}
複製程式碼

我們需要初始化它。初始化在 Swift 中是值得重視的事,你將在這裡瞭解到更多關於它的事。現在,我們只需要知道有一個主要的初始化器負責初始化當前類所有的屬性。這是一個典型的實現:

import UIKit
import PureLayout

class ProfileView: UIView {
  var shouldSetupConstraints = true
    
  override init(frame: CGRect) {
    super.init(frame: frame)
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
    
  override func updateConstraints() {
    if(shouldSetupConstraints) {
      // AutoLayout constraints
      shouldSetupConstraints = false
    }
    super.updateConstraints()
  }
}
複製程式碼

給自定義的檢視新增約束之前,我們要覆蓋 updateConstraints 方法。這個方法在執行期間可能會被呼叫多次。為了避免多次新增約束給檢視,我們需要立一個 flag(shouldSetupContraints)來標示是否已經新增過約束。這個方法的最後,我們也必須呼叫父類中的同名方法。(如果你在約束髮生改變之前就呼叫,可能會 crash)。

Example

我們來仿寫一個 Twitter iOS app 的個人資訊檢視。在下面的圖片中,可以看到頂部檢視包括一個 Banner 圖,使用者頭像,以及使用者資訊。然後下方是所有推展示在列表中以及tabbar。所有的 UIView 元素都拿紫色標註了起來。接下來我們聚焦在 header view,橙色區域。

[譯]純程式碼建立 UIView

我們先看主要的三部分。banner 和 使用者頭像使用 UIImageViews 進行展示,button 區域使用 UISegmentedControl。

[譯]純程式碼建立 UIView

首先,我們定義三個元素在我們的 ProfileView 類。bannerView,profileView 和 segmentedControl。

//ProfileView.swift
import UIKit
import PureLayout

class ProfileView: UIView {
  var shouldSetupConstraints = true
    
  var bannerView: UIImageView!
  var profileView: UIImageView!
  var segmentedControl: UISegmentedControl!
    
  override init(frame: CGRect){
    super.init(frame: frame)
    
  }
  
  ...
複製程式碼

在 init 方法中,我們初始化這些檢視元素的屬性。背景顏色、邊框顏色和其他基本的視覺屬性。初始化他們的 frame 為 zero,AutoLayout 會自動調整大小和位置。

將這些檢視元素新增為 ProfileView 的子檢視使用 addSubview 方法。這個方法將被操作的檢視放在其他子元素的最上面。程式碼如下:

//ProfileView.swift
import UIKit
import PureLayout

class ProfileView: UIView {
  var shouldSetupConstraints = true
    
  var bannerView: UIImageView!
  var profileView: UIImageView!
  var segmentedControl: UISegmentedControl!
    
  let screenSize = UIScreen.main.bounds
  
  override init(frame: CGRect){
    super.init(frame: frame)
        
    bannerView = UIImageView(frame: CGRect.zero)
    bannerView.backgroundColor = UIColor.gray
        
    bannerView.autoSetDimension(.height, toSize: screenSize.width / 3)
    
    self.addSubview(bannerView)
        
    profileView = UIImageView(frame: CGRect.zero)
    profileView.backgroundColor = UIColor.gray
    profileView.layer.borderColor = UIColor.white.cgColor
    profileView.layer.borderWidth = 1.0
    profileView.layer.cornerRadius = 5.0
        
    profileView.autoSetDimension(.width, toSize: 124.0)
    profileView.autoSetDimension(.height, toSize: 124.0)
    
    self.addSubview(profileView)
        
    segmentedControl = UISegmentedControl(items: ["Tweets", "Media", "Likes"])
        
    self.addSubview(segmentedControl)
  }
  ...
複製程式碼

使用 PureLayout 設定約束

佈局約束

佈局約束用來描述檢視與其他檢視的關係和屬性。通過 NSLayoutConstraint 類來使用。

約束有以下幾種:

  • 尺寸約束 - 如描述一個圖片的寬為200px
  • 對齊約束 - 如描述一個 label 垂直居中在螢幕
  • 間隙約束 - 如描述兩個元素之間的間隙

Attributes

PureLayout 定義了用來建立約束的檢視屬性,見圖:

[譯]純程式碼建立 UIView

開始搞事吧!

螢幕上有三個巨星元素,我們要把他們的位置大小調整如 Twitter 個人頁面。

[譯]純程式碼建立 UIView

//ProfileView.swift
...
  override func updateConstraints() {
    if(shouldSetupConstraints) {

      let edgesInset: CGFloat = 10.0
      let centerOffset: CGFloat = 62.0
            
      bannerView.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero, excludingEdge: .bottom)
            
      profileView.autoPinEdge(toSuperviewEdge: .left, withInset: edgesInset)
      // ?? profileView.autoAlignAxis(.horizontal, toSameAxisOf: bannerView, withOffset: centerOffset)
      profileView.autoPinEdge(.bottom, to: .bottom, of: bannerView, withOffset: centerOffset)
            
      segmentedControl.autoPinEdge(toSuperviewEdge: .bottom, withInset: edgesInset)
      segmentedControl.autoPinEdge(toSuperviewEdge: .left, withInset: edgesInset)
      segmentedControl.autoPinEdge(toSuperviewEdge: .right, withInset: edgesInset)
            
      shouldSetupConstraints = false
    }
    
    super.updateConstraints()
...
}
複製程式碼

避免 MVC 成為 Massive View Controllers

下面進行完成 ProfileView 的最後一步。我們需要在我們的 Controller(ProfileViewController)中呼叫。Xcode 已經建立了一個 Controller 模板,幷包含 viewDidLoad: 方法。這個方法會在 Controller 顯示前進行回撥。接下來我們需要例項化我們的 ProfileView 並展示它。

//ProfileViewController.swift
import UIKit

class ViewController: UIViewController {
  var profile: ProfileView!
  
  override func viewDidLoad() {
    super.viewDidLoad()

    profile = ProfileView(frame: CGRect.zero)
    self.view.addSubview(profile)
    
    // AutoLayout
    profile.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero)
  }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }
}
複製程式碼

(最後:本文中的程式碼用於展示 Auto Layout。你需要補充其他程式碼才能使其成為一個完整的專案,加油!???)

相關文章