iOS Storyboard入門及一些高級使用

1、前言

Swift版本 4.0ios

Xcode版本 9.2git

這周原本我是想要寫其餘知識的,但在構建 Demo 工程的時候, 我不由自主的就使用了 Storyboard (下面簡稱 SB ),或者說是 Interface Builder (下面簡稱 IB),因此就想着寫一篇相關文章。github

這裏不討論使用這種方式的好壞,你們仁者見仁,智者見智,貓神的文章連接在後記裏面,個人觀點和他一致。swift


2、Storyboard基礎

這部分針對徹底沒用過 SB 的讀者,極其基礎,熟悉的直接跳過!安全

2.1 完成目標的概覽

下面是我這一小節須要完成目標的樣子,一個遊戲的展現界面和添加遊戲。app


2.2 界面初識

先創建一個 Demo 項目,點擊 Main.storyboard 出現以下界面:框架

首先須要瞭解 SB 中幾個重要的區域,這裏是按照個人理解取的名字,只是簡單說明區域的做用,後面會詳細使用這幾個區域,如上圖所示:ide

  • 一、菜單導航區域,添加的控制器、控制器之間的跳轉 Segue 及控制器上面的控件和佈局等等信息都在這裏顯示。
  • 二、工做展現區域: 能夠在這裏給各個控制器添加控件和預覽佈局後的控件。
  • 三、配置區域: 能夠在這裏將 SB 和代碼文件關聯和查看關聯後的信息,也能夠直接在這裏配置控件的屬性等等。
  • 四、佈局區域: 上面面有不少機型選擇,能夠直接選擇機型和方向,區域2會根據選擇的機型和自動佈局直接預覽控件顯示的效果和佈局,中間的加減符號能夠放大和縮小區域2中的內容,右上角的幾個按鈕能夠進行自動佈局操做。
  • 五、控件區域,咱們能夠直接在這裏選擇控件,而後拖入區域2中。

細心的讀者可能會發現,區域1中,控制器在一個 scene 的下面,在 SB 中,scene 就對應着一個控制器。區域2裏面還有一個灰色箭頭,它表明這個控制器是當前 SB 文件的入口,會在後面詳細的講解。佈局


2.3 添加控件

直接從控件區域拖拽了一個 UIView 控件到控制器上,而後在 Attributes inspector區域 (點擊配置區域中那個楔子形狀的按鈕)直接配置背景顏色爲灰色。若是顯示菜單欄中沒有沒有你想要的顏色,點擊 other ,裏面有多種方式配置顏色,如 RGB 和16進制顏色等等。學習

上圖這個區域裏還有一些其餘的屬性能夠配置,例如 UILabel 控件字體和字體顏色等等屬性等,就不深刻去展開了。

讀者確定注意到,控件在拖拽中,控制器出現了輔助虛線,能夠提醒你相對其餘視圖的位置信息,圖中所示的其中一條就是父視圖的中線。


2.4 佈局控件

解釋一下上圖的自動佈局操做:

  • 1.選中控件, 點擊 Align 按鈕,勾選 Horizontalliy in ContainerVertically in Container,而後添加這兩個佈局,相對於父視圖水平和垂直居中
  • 2.而後點擊 Add New Constraints按鈕,添加 WidthHeight 約束,都爲200。

到這裏佈局就完成了,由於大小和位置都已經肯定。觀察上圖,我只添加了 Align 約束時,界面出現了紅線,這表明約束不完整。而且菜單欄上方出現帶有箭頭的小紅點,能夠點擊進去查看還有哪些約束沒有完成。這裏還有其餘的約束選項,讀者能夠自行嘗試。

  • 三、圖中最後,我在 Size inspector 區域 (點擊配置區域中那個小直尺按鈕) 雙擊寬度約束,進入了詳情配置界面,這裏能夠對約束進行二次修改。點擊菜單欄的約束,一樣能夠進入這個界面。

這裏還有另外一種方式進行自動佈局,如圖所示:

按住 Ctrl,而後選中灰色 View ,移動鼠標會出現一條線,拖到你想要相對其佈局的控件,圖中選擇的是父視圖,出現了一個菜單讓你選擇約束條件。一樣的操做也能直接在菜單欄中進行。甚至當控制器上控件比較多不容易選中時,能夠直接從控制器上拖到菜單欄上的控件上。

這一部分的操做是很簡單的,不過須要自動佈局的相關知識。

2.5 開始一個TableView界面

選中菜單欄的 View Controller Scene,而後點擊鍵盤上的 delete 鍵,刪除咱們鼓搗的控制器。

從控件區域拖拽一個 UITabBarController 到工做展現區域:

在工做展現空白區域,雙擊鼠標左鍵和單點鼠標右鍵,能夠放大,縮小顯示內容。

如圖,UITabBarController (它和 UINavigationController 都是容器控制器) 會自帶兩個子控制器,而且有兩個箭頭從 TabBarController 指向它們,這個箭頭的術語叫作 Segue, 這裏的是 Relationship Segue ,表明控制器之間的關係。

刪掉 item1 的控制器,拖拽一個 UITableViewController 出來,而後讓它成爲 UINavigationController 的子控制器, 再讓 UINavigationController 成爲 UITabBarController 控制器的子控制器,操做如圖所示:

固然你也能夠直接拖拽一個 UINavigationController 出來,而後按住 Control 拖動選擇 view controllers。不過我以爲點擊 Editor 這種方式更加便捷。

讓咱們的關注點來到 UITableViewController ,看到界面上有一個 Prototype Cells ,能夠理解爲咱們平時使用的那種 Cell , 與之對應的是 Static Cells, 從名字就能夠看出來,這種是靜態的,不可以循環使用,而且只能在UITableViewController 上使用。

紅框中,和咱們代碼實現中官方提供的4種 Cell 同樣,不過這裏咱們須要自定義,下面是完成後的樣子。

可能對沒有接觸過 IB 的讀者來講,這裏仍是比較麻煩,因此詳細描述一下。

選中 Cell 在右上角 Size inspector 區域修改 Cell 的高度爲120,這裏的高度設置只是方便咱們進行佈局,並非實際顯示的高度。

拖動一個 UIImageView 控件到 Cell 裏面,進行佈局。

iOS8 之後更新了讓 Cell 本身自適應高度的新特性,因此這裏咱們不光要肯定本身的位置和大小,還須要將本身的大小反饋給 Cell 讓其自適應高度,後面詳細使用。

相對於父視圖:

距離右邊20,距離上邊10,寬高100,這就已經肯定了位置和大小,不過爲了讓 Cell 知道咱們的高度,還須要設置一個距離底部的距離。這樣 Cell 就知道顯示的時候須要的高度。結合咱們目標的樣子,底部距離的設置是有個小問題的,後面來糾正。

繼續拖動一個 UILableCell 裏。

相對於父視圖: 距離左邊15,上邊10

相對於 UIImageView : 距離它的左邊10

而後比較麻煩的地方來了。再拖動一個 UILableCell 裏。

相對於父視圖: 距離左邊15,距離底部10。

相對於 Game Name Label:距離其底部10。

相對於 UIImageView : 距離它的左邊10。

按照邏輯來講沒問題呀,由於上下左右都給了約束,是什麼緣由呢?

咱們點擊紅框中的小紅點進行查看:

UILabelUIButton等控件有一個特色,它會根據內容自適應本身的大小。

如圖所示兩個 Label 在反饋大小給 Cell 時,Cell 也一樣會反饋本身的大小給兩個 Label,這就會產生兩個問題:

  • 1.若是 Cell 高度比內容反饋須要的高度大的時候,須要拉伸哪一個部分的內容?

  • 2.若是 Cell 高度比內容反饋須要的高度小的時候,須要壓縮哪一個部份內容?

這裏就須要談到 AutoLayout 中的 Content HuggingContent Compression Resistance

  • 1.Content Hugging Priority: 對應上面的第1中狀況,這個屬性的值越高,就越不容易被拉伸。
  • 2.Content Compression Resistance:對應上面的第2種狀況,這個屬性的值越高,就越不容易被壓縮。

顯然上面報錯的緣由是 Cell 的高度比兩個 Label 的內容高度大了,屬於第一種狀況,咱們讓 Game Name Label 不拉伸, 增長它的 Content Hugging Priority (默認值爲251)比另外一個 Label大(增長到252)。

這個問題解決了,但新問題又出現了:

由於 Game Detail Label 被拉伸,致使了內容居中,這看上去怪怪的,之前看到有關於討論讓 Label 居上的問題。但這並非這裏的解決辦法。還記得前面說過能夠對約束進行二次編輯嗎?選中 Game Detail LabelBottom 約束,能夠在菜單區域選擇或者在小直尺圖標區域裏面找到它進行雙擊,就來到以下界面:

這裏有個 Relation 選項,點看能夠看到:

沒錯咱們選擇讓這個約束大於或等於10:

看上去是完成了,回到最初添加 UIImageView 約束的時候,我說過有一個小問題,UIImageView 的約束強行的讓 Cell 的高度爲120了。當 Label 內容不少換行超過120的時候,就會出現上面的第2種狀況, Cell 高度不夠完整顯示內容,這顯然不是咱們想要的結果。因此修改 UIImageViewBottom約束也爲距離底部大於等於10,到這裏佈局就結束了,最後別忘了設置 identifier :

爲了讓 SB 和代碼關聯起來,建立一個繼承自 UITableControllerGameVC.swift 文件、繼承自 UITableViewCellGameCell.swift 文件和數據模型 Game.swift 文件,而後依次選中 SB 中的文件關聯:

繼續將 SB 中的屬性和代碼關聯起來:

也能夠直接從控制器中選中控件並按住 Control 進行拖動連線,這裏就再也不舉例了,這裏不只僅只能屬性連線,例如 UIButton 能夠直接連線一個點擊響應方法等等。連線後能夠在 Connections inspectors (圓圈包含一個箭頭的按鈕) 查看:

注意: 一個控件屬性關聯屢次或者其餘關聯錯誤會引起運行奔潰,這是新手最容易犯的問題,若是名字寫錯了,須要先取消上次的關聯,再從新關聯。

Game.swift文件中:

struct Game {
    let name: String
    let detail: String
    let pictureName: String
    
    static func getData() -> [Game] {
        return [
        Game(name: "絕地求生",
        detail: "神仙打架遊戲",
        pictureName: "game_one"),
        Game(name: "英雄聯盟",
        detail: "《英雄聯盟》(簡稱LOL)是由美國拳頭遊戲(Riot Games)開發、中國大陸地區騰訊遊戲代理運營的英雄對戰MOBA競技網遊。遊戲裏擁有數百個個性英雄,並擁有排位系統、符文系統等特點養成系統。《英雄聯盟》還致力於推進全球電子競技的發展,除了聯動各賽區發展職業聯賽、打造電競體系以外,每一年還會舉辦「季中冠軍賽」「全球總決賽」「All Star全明星賽」三大世界級賽事,得到了億萬玩家的喜好,造成了本身獨有的電子競技文化",
        pictureName: "game_two")]
    }
}
複製代碼

GameVC.swift 文件中:

class GameVC: UITableViewController {
    var games: [Game] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        games = Game.getData()
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return games.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "GameCell", for: indexPath) as! GameCell
        cell.game = games[indexPath.row]
        return cell
    }
}
複製代碼

GameCell.swift 文件中:

class GameCell: UITableViewCell {
    var game: Game! {
        didSet {
            gameNameLabel.text = game.name
            gameDetailLabel.text = game.detail
            gameImageView.image = UIImage(named: game.pictureName)
        }
    }
    
    @IBOutlet weak var gameImageView: UIImageView!
    @IBOutlet weak var gameNameLabel: UILabel!
    @IBOutlet weak var gameDetailLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }
}
複製代碼

準備工做完畢,運行 Demo:

出錯了,細心的讀者確定早就發現這個問題了,那就是前面說的那個入口箭頭:

配置完畢後再運行:

2.6 界面之間跳轉

先給控制器增長一個 title:

而後把 Game 控制器拖到 UITabBarController 第一個位置:

繼續添加一個 UIBarButtonItem, 並設置風格爲 Add

添加一個 UITableViewController 並讓其成爲 UINavigationController的子控制器 ,按住Control 點擊 Add Item ,拖動到新的控制器上,會出現彈窗選擇跳轉方式,這裏選擇 Present Modally ,對應着咱們代碼中 Present 那個方法。

這裏的 Show 表明,若是是 UINavigationController 的子控制器就會執行 Push 方法,不是就會執行 Present 方法。

兩個控制器之間多出了一個帶箭頭的連線,這能夠理解爲界面切換 Segue ,用來描述控制器之間的跳轉,一個界面切換 Segue 只能單向跳轉。

設置新控制器的 titleGame Add ,左邊添加一個 Cancle Item, 右邊添加一個 Done Item 。而後繼續在 GameVC.Swift 的底部添加分類。

// MARK: - IBActions
extension GameVC {
    @IBAction func cancelToGameVC(_ segue: UIStoryboardSegue) {
        
    }
    @IBAction func saveGameDetail(_ segue: UIStoryboardSegue) {
        
    }
}
複製代碼

這是 unwind Segue,用來返回到目標控制器。直接上圖:

選中 Game Add 中的 TableView.接下來我直接用 static cell 進行佈局,

  • 1.content 中選擇 Static CellsStyle 中選擇 Grouped
  • 2.將出現的 Section 中的 Cell 刪除到只剩一個,設置 CellSelectionNone, 直接複製 Section,這樣就有兩個含有一個 CellSection
  • 3.給 Section 設置標題( SB 中的 Header )爲 Game NameGame Detail
  • 4.將第一個 Section 高度設爲50,第二個設爲200。拖一個 UITextField 到第一個 Cell ,佈局上0底0左10右10,拖一個 UITextView 到第二個 Cell,佈局上底左右都是10。
  • 5.建立繼承於 UITableViewControllerGameAddVC.swift 文件,而後將裏面方法刪除到只剩 viewDidLoad , 並關聯這個 SB
  • 6.將步驟3中的 UITextFieldUITextView 連線到 GameAddVC.swift文件中生成 @IBOutlet 屬性。
@IBOutlet weak var gameNameTextField: UITextField!
@IBOutlet weak var gameDetailTextView: UITextView!
複製代碼

這裏之因此能直接將 Cell 中的屬性直接連線到控制器中,是由於靜態 Cell 不會重用。

配置完成以下:

這裏省略了添加圖片的步驟,直接設定一個默認圖片。

  • 選中剛纔 Done Item 添加的 Segue,而後設置它的 IdentifierAddGameDetail

  • AddGameVC.swift 中重寫父類方法並添加代碼:

var game: Game?

// 這個方法點擊 `Done` 的時候會調用
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "GameAddDetail",
    let name = gameNameTextField.text {
        game = Game(name: name,
             detail: gameDetailTextView.text!,
             pictureName: "game_default")
    }
}
複製代碼
  • GameVC.swift 中,添加以下代碼:
// 這個方法前面有,只是添加方法內的內容
@IBAction func saveGameDetail(_ segue: UIStoryboardSegue) {
    guard let gameAddVC = segue.source as? GameAddVC,
        let game = gameAddVC.game else {
            return
    }
  
    games.append(game)
    let indexPath = IndexPath(row: games.count - 1, section: 0)
    tableView.insertRows(at: [indexPath], with: .automatic)
}
複製代碼

運行 Demo, 以下:

2.7 Storyboard Reference

SB中,若是多我的同時對一個地方(例如同一個控制器)進行修改,很容易形成 Git 衝突,這也是反對者們反對使用 SB 的一個理由。不過在蘋果增長 Storyboard Reference 功能後,這種狀況在開發中徹底能夠避免了。

Demo 中的控制器數量較少,但在實際項目中,若是多我的都都只操做這個 Main.storyboard ,那將是一件很恐怖的事情。以前沒有 Storyboard Reference 功能時,多個 SB 之間的跳轉只能使用代碼的方式實現。如今來看看 Storyboard Reference 吧。

  • 1.按住鼠標左鍵,而後圈中你想要脫離 Main.storyboard 的控制器,就像桌面用鼠標多選文件那樣。
  • 2.點擊 Editor>Refactor to Storyboard。
  • 3.取名爲 Game.storyboard,選擇在哪一個文件夾下面建立,而後肯定。

完成後,咱們能夠看到 Main.storyboard 中的控制器變成了一個了 Storyboard Reference,其餘控制器移到咱們新建立的 Game.storyboard 中去了。多人開發時,各自操做本身的業務 SB ,就基本避免了 Git 衝突。

一樣,咱們也能夠先直接建立 SB 文件,而後再從控件區拖拽一個 Storyboard Reference, 而後再讓它和咱們新建立的 SB 文件關聯。

到這裏這一下節就結束了,我自認爲是寫得比較囉嗦,不過這也是沒有辦法的選擇,這部分知識更多的是界面上的操做,若是不寫明白,不容易闡述清楚!

3 Storyboard高級用法

這裏的所謂高級用法,是我一廂情願認爲的。

3.1 @IBInspectable

若是你以前沒有見過這個東西,那麼你確定爲某些屬性沒有暴露在 IB 的設置面板中而困擾過。@IBInspectable 的用處很簡單,就是讓咱們自定義的屬性也能直接在 IB 中選擇,例如貓神的文章中的建議:

  • 爲一個顯示文字的 view 設置本地化字符串:
extension UILabel {
    @IBInspectable var localizedKey: String? {
        set {
            guard let newValue = newValue else { return }
            text = NSLocalizedString(newValue, comment: "")
        }
        get { return text }
    }
}

extension UIButton {
    @IBInspectable var localizedKey: String? {
        set {
            guard let newValue = newValue else { return }
            setTitle(NSLocalizedString(newValue, comment: ""), for: .normal)
        }
        get { return titleLabel?.text }
    }
}

extension UITextField {
    @IBInspectable var localizedKey: String? {
        set {
            guard let newValue = newValue else { return }
            placeholder = NSLocalizedString(newValue, comment: "")
        }
        get { return placeholder }
    }
}
複製代碼

IB 中能夠直接設置:

  • 爲一個 image view 設置圓角(這裏能夠直接擴展 UIView
@IBInspectable var cornerRadius: CGFloat {
   get {
       return layer.cornerRadius
   }
   
   set {
       layer.cornerRadius = newValue
       layer.masksToBounds = newValue > 0
   }
}
複製代碼

IB 中能夠直接設置:

僅僅使用 @IBInspectable 沒法將屬性的設置實時顯示出來,還須要另外一個關鍵字的幫助。

3.2 @IBDesignable

它可以將一些繪圖代碼和 UIView 及其子類的 @IBInspectable 屬性實時渲染到 IB 中。

  • 1.結合 @IBInspectable 使用,建立 UIView 子類 CustomView。拖拽一個 UIView 到另外一個 Item 控制器上,佈局上下居中,款高200,而後將它們關聯。此時如圖所示:

  • 2.在 CustomView.swift 中添加代碼,注意 @IBDesignable 的位置:
@IBDesignable
class CustomView: UIView {

    @IBInspectable var cornerRadius: CGFloat {
        get {
            return layer.cornerRadius
        }
        
        set {
            layer.cornerRadius = newValue
            layer.masksToBounds = newValue > 0
        }
    }

    @IBInspectable var borderColor: UIColor = UIColor.white {
        didSet {
            layer.borderColor = borderColor.cgColor
        }
    }

    @IBInspectable var borderWidth: Int = 1 {
        didSet {
            layer.borderWidth = CGFloat(borderWidth)
        }
    }

}
複製代碼

而後看效果:

  • 3.再添加繪圖代碼,並將上面的 Corner Radius 設爲0:
override func draw(_ rect: CGRect) {
    let path = UIBezierPath(ovalIn: rect)
    UIColor.green.setFill()
    path.fill()
}
複製代碼

結果如圖:

3.3 自定義Segue跳轉動畫

咱們都知道 Presnet 切換時系統默認的公開動畫有四種,若是咱們想自定義的話,須要建立一個 UIStoryboardSegue 的子類。

class CustomAnimationPresentationSegue: UIStoryboardSegue, , UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning {

    override func perform() {
        destination.transitioningDelegate = self
        super.perform()
    }
    
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self
    }
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        
        let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
        
        if transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) == destination {
            // Presenting.
            UIView.performWithoutAnimation {
                toView.alpha = 0
                containerView.addSubview(toView)
            }
            
            let transitionContextDuration = transitionDuration(using: transitionContext)
            
            UIView.animate(withDuration: transitionContextDuration, animations: {
                toView.alpha = 1
            }, completion: { success in
                transitionContext.completeTransition(success)
            })
        }
        else {
            // Dismissing.
            let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
            
            UIView.performWithoutAnimation {
                containerView.insertSubview(toView, belowSubview: fromView)
            }
            
            let transitionContextDuration = transitionDuration(using: transitionContext)
            
            UIView.animate(withDuration: transitionContextDuration, animations: {
                fromView.alpha = 0
            }, completion: { success in
                transitionContext.completeTransition(success)
            })
        }
    }
    
}
複製代碼

自定義了一個簡單的漸隱動畫,這裏關於自定義的跳轉動畫的部分我不想仔細探討(排在我想寫內容的隊列總)。而後咱們在 IB 關聯跳轉到添加遊戲的 SegueCancle&Doneunwind SegueCustomAnimationPresentationSegue。 演示效果:

3.4 使用R.swift三方框架

R.swift Github地址

其實這不算是 IB 的高級使用,它可以掃描整個項目中的資源文件(好比圖片名,View Controllersegueidentifier 等),並生成一種類型安全的獲取方式。

let icon = UIImage(named: "settings-icon")
let viewController = UIStoryboard(name: "Main",
bundle: nil).instantiateViewController(withIdentifier: "myViewController") as! MyViewController
複製代碼
let icon = R.image.settingsIcon()
let viewController = R.storyboard.main.myViewController()
複製代碼

4、後記及Demo

Demo Github地址

關於 IB 的操做目前我知道的就這些,若是你有更好的使用技巧能夠評論分享討論一下。

最近我撿起了個人微博,由於不少 iOS 界的前輩都喜歡微博分享技術,我也關注了不少,收益匪淺。例如這個 OC 項目 ZHNCosmos Github地址,代碼工整,邏輯清晰,我這個菜鳥準備好好學習一下。

另外附上個人微博,我天天都會轉發一些大佬的技術動態,請你們隨緣關注:

個人微博地址

參考文章

貓神博客 再看關於 Storyboard 的一些爭論

WWDC2015視頻自帶中文字幕 What's New in Storyboards

英文 Storyboards Tutorial for iOS: Part 1

英文 Storyboards Tutorial for iOS: Part 2

相關文章
相關標籤/搜索