SwiftUI框架解析(一) —— 基于SwiftUI的一個簡單示例(一)

版本記錄

版本號 時間
V1.0 2019.06.14 星期五

前言

SwiftUI是2019年WWDC新推出的UI相關的API,相信大家都已經知道并想體驗一下,下面我們就去一起了解和學習了。

開始

首先看下寫作環境

Swift 5, iOS 13, Xcode 11

在這個SwiftUI教程中,您將學習如何通過聲明和修改視圖來布局UI,以及如何使用狀態變量來更新UI。 您將使用Xcode的新預覽和實時預覽,體驗保持代碼和WYSIWYG布局同步的樂趣。

自Apple于2014年宣布推出Swift以來,SwiftUI是最激動人心的消息。這是邁向Apple實現每個人都可以編碼,簡化基礎知識的目標邁出了一大步,因此您可以將更多時間花在滿足用戶需求的自定義功能上。一些開發人員開玩笑說他們可能被SwiftUI Sherlocked了!

SwiftUI允許您忽略Interface Builder(IB)storyboards,而無需編寫詳細的逐步說明來布置UI。 IBXcodeXcode 4之前是單獨的應用程序,每次編輯IBActionIBOutlet的名稱或從代碼中刪除它時,接縫仍會顯示,并且您的應用程序崩潰,因為IB沒有看到代碼更改。或者你已經對你必須在你的代碼中使用的segues或表格視圖單元格的字符串類型標識符感興趣,但Xcode無法檢查你,因為它們是字符串。而且,雖然在WYSIWYG編輯器中設計新UI可能更快更容易,但在用代碼編寫時,復制或編輯UI會更有效率。

SwiftUI來了可以做好這點!您可以與其代碼并排預覽SwiftUI視圖 - 對一側的更改將更新另一側,因此它們始終保持同步。沒有任何標識符字符串出錯。它是代碼,但比你為UIKit編寫的要少得多,因此它更容易理解,編輯和調試。

SwiftUI不會取代UIKit - 比如SwiftObjective-C,你可以在同一個應用程序中使用它們。您將無法在macOS上運行SwiftUI iOS應用程序 - 這就是Catalyst。但是,SwiftUI API在不同平臺上是一致的,因此在多個平臺上使用相同的源代碼開發相同應用程序將更容易。

在本教程中,您將使用SwiftUIiOS Apprentice構建我們著名的BullsEye游戲的變體。您將學習如何通過聲明和修改視圖來布局UI,以及如何使用狀態變量來更新UI。您將使用Xcode的一些新工具,尤其是預覽和實時預覽,并體驗保持代碼和WYSIWYG布局同步的樂趣。

注意:本教程假設您習慣使用Xcode開發iOS應用程序。 你需要Xcode 11 beta。 要查看SwiftUI預覽,您需要macOS 10.15 beta。 因此,使用beta版軟件也需要一定的舒適度。

如果您沒有備用Mac,則可以 install Catalina beta on a separate APFS volume

下載項目,并在RGBullsEye-Starter文件夾中構建并運行UIKit應用程序。 此游戲使用三個滑塊sliders - RGB顏色空間中的紅色,綠色和藍色值 - 以匹配目標顏色。

我為RWDevCon 2016編寫了這個應用程序,并在本教程中將代碼更新為Swift 5。 它運行在Xcode 10Xcode 11 beta中。 在本教程中,您將使用SwiftUI創建此游戲的基本版本。

Xcode 11 beta中,創建一個新的Xcode項目(Shift-Command-N),選擇iOS?Single View App,將項目命名為RGBullsEye,然后選中Use SwiftUI復選框:

將項目保存在RGBullsEye-Starter文件夾之外的某個位置。


Entering the New World of SwiftUI

在項目導航器中,打開RGBullsEye組以查看您的內容:舊的AppDelegate.swift現在拆分為AppDelegate.swiftSceneDelegate.swiftSceneDelegate具有window

SceneDelegate不是特定于SwiftUI,但這一行是:

window.rootViewController = UIHostingController(rootView: ContentView())

UIHostingControllerSwiftUI視圖ContentView創建一個視圖控制器。

注意:UIHostingController使您可以將SwiftUI視圖集成到現有應用程序中。 您可以在storyboard中添加一個Hosting View Controller,并從UIViewController創建一個segue。 然后按住Ctrl鍵從segue拖動到視圖控制器代碼中以創建IBSegueAction,您可以在其中指定hosting controllerrootView值 - SwiftUI視圖。

當應用程序啟動時,window會顯示ContentView的一個實例,該實例在ContentView.swift中定義。 它是一個符合View協議的struct

struct ContentView: View {
  var body: some View {
    Text("Hello World")
  }
}

這是SwiftUI聲明ContentView的主體body包含顯示Hello WorldText視圖。

DEBUG塊中,ContentView_Previews生成一個包含ContentView實例的視圖。

#if DEBUG
struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}
#endif

您可以在此處為預覽指定樣本數據。 但是這個預覽在哪里好像聽過?

代碼旁邊有一個很大的空白區域,位于頂部:

單擊Resume,稍等片刻以查看預覽:

注意:如果沒有看到Resume按鈕,請單擊editor options按鈕,然后選擇Editor and Canvas

如果您仍然沒有看到Resume按鈕,請確保您正在運行macOS 10.15 beta


Outlining Your UI

您沒有看到的一個文件是Main.storyboard - 您將在SwiftUI代碼中創建您的UI,密切關注預覽以查看它的外觀。 但不要擔心 - 您不會編寫數百行代碼來創建視圖!

SwiftUI是聲明性的:您聲明了UI的外觀,SwiftUI將您的聲明轉換為可以完成工作的高效代碼。 Apple鼓勵您根據需要創建任意數量的視圖,以使代碼易于閱讀和維護。 特別推薦使用可重用的參數化視圖 - 就像將代碼提取到函數中一樣,您將在本教程的后面創建一個。

RGBullsEye的UI有很多子視圖,所以你首先要創建一個outline,使用Text視圖作為占位符。

首先用以下代碼替換Text(“Hello World”)

Text("Target Color Block")

如有必要,請單擊Resume以刷新預覽。

現在Command-Click預覽中的Text視圖,然后選擇Embed in HStack

請注意您的代碼更新以匹配:

HStack {
  Text("Target Color Block")
}

你在這里使用horizontal stack是因為你想要顯示目標并且并排猜測顏色塊。

復制并粘貼Text語句,然后對其進行編輯,使HStack如下所示。 請注意,您不要使用逗號分隔兩個語句 - 只需在各自的行中寫入:

HStack {
  Text("Target Color Block")
  Text("Guess Color Block")
}

它在預覽中:

現在準備通過在VStack中嵌入HStack來添加滑塊占位符 - 這次,在代碼中按命令單擊HStack

選擇Embed in VStack,出現新代碼,但預覽不會更改 - 您需要在顏色塊下面添加視圖。

HStack閉包下面打開一個新行,單擊工具欄中的+按鈕打開Library,然后將Vertical Stack拖到新行中:

正如您所期望的那樣,代碼和預覽更新:

注意:迷你地圖不會出現在我的屏幕截圖中,因為我隱藏了它:Editor ? Hide Minimap

現在完成大綱,看起來像這樣:

VStack {
  HStack {
    Text("Target Color Block")
    Text("Guess Color Block")
  }

  Text("Hit me button")

  VStack {
    Text("Red slider")
    Text("Green slider")
    Text("Blue slider")
  }
}

新的VStack將包含三個滑塊sliders,顏色塊和滑塊之間會有一個按鈕。

注意:Xcode 11是測試版軟件,在編寫本教程時,很難弄清楚我做錯了什么。它有一些無用的錯誤消息,包括Unable to infer complex closure return type; add explicit type to disambiguate, ‘(LocalizedStringKey) -> Text’ is not convertible to ‘(LocalizedStringKey, String?, Bundle?, StaticString?) -> Text’ and Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type, which basically meant “huh??”。這些消息很少出現在我剛剛添加的代碼附近。如果您看到的消息似乎沒有幫助,或者指向以前完全正常的代碼,請嘗試注釋掉剛才添加的內容。還要檢查拼寫并確保您的大括號和括號匹配。最后,在beta期間重新啟動Xcode總是一個好主意!


Filling in Your Outline

現在,練習你的新SwiftUI-fu技能,開始填充HStack顏色塊,所以它看起來像這樣:

HStack {
  // Target color block
  VStack {
    Rectangle()
    Text("Match this color")
  }
  // Guess color block
  VStack {
    Rectangle()
    HStack {
      Text("R: xxx")
      Text("G: xxx")
      Text("B: xxx")
    }
  }
}

每個顏色塊都有一個Rectangle。 目標顏色塊在其Rectangle下方有一個Text視圖,而guess color block有三個Text視圖 - 在本教程的后面,您將替換每個xxx以顯示當前滑塊值。

不要擔心黑色矩形占據場景 - 它們會為滑塊騰出空間,你現在就可以設置它們的前景顏色了。


Using @State Variables

您可以在SwiftUI中使用normal常量和變量,但是如果UI在其值發生更改時應該更新,則將變量指定為@State變量。 這個游戲都是關于顏色的,所以影響guess rectangle顏色的所有變量都是@State變量。

struct ContentView的頂部,在body閉包上方添加這些行:

let rTarget = Double.random(in: 0..<1)
let gTarget = Double.random(in: 0..<1)
let bTarget = Double.random(in: 0..<1)
@State var rGuess: Double
@State var gGuess: Double
@State var bGuess: Double

R,G和B值介于0和1之間。將目標值初始化為隨機值。 您也可以將猜測值初始化為0.5,但如果您沒有初始化某些變量,我會將它們保留為未初始化以顯示您必須執行的操作。

向下滾動到DEBUG塊,該塊實例化要在預覽中顯示的ContentView。 初始化程序現在需要猜測值的參數值。 將ContentView()更改為:

ContentView(rGuess: 0.5, gGuess: 0.5, bGuess: 0.5)

創建滑塊時,它們將以居中顯示在預覽中。

您還必須修改SceneDelegate中的初始值,在scene(_:willConnectTo:options:),將ContentView()替換成下面這行:

window.rootViewController = UIHostingController(rootView:
  ContentView(rGuess: 0.5, gGuess: 0.5, bGuess: 0.5))

當應用加載其根場景時,滑塊將居中。

現在將前景色修改器添加到目標Rectangle

Rectangle()
  .foregroundColor(Color(red: rTarget, green: gTarget, blue: bTarget, opacity: 1.0))

修改器.foregroundColor創建一個新的Rectangle視圖,現在使用由隨機生成的RGB值指定的前景顏色。

同樣,修改guess Rectangle

Rectangle()
  .foregroundColor(Color(red: rGuess, green: gGuess, blue: bGuess, opacity: 1.0))

當R,G和B值均為0.5時,您會得到灰色。

單擊Resume,稍等片刻以進行預覽更新。

注意:預覽會定期刷新,以及單擊Resume或“實時預覽”按鈕時,所以不要驚訝地看到目標顏色經常變化。


Making Reusable Views

我向幾個人展示了這個游戲,他們發現它很容易讓人上癮 - 特別是平面設計師。 然后他們會要求我實現其他顏色空間之一,比如YUV。 但RGB是本教程的不錯選擇,因為滑塊基本相同,因此您將定義一個滑塊視圖,然后將其重用于其他兩個滑塊。

首先,假裝你沒有考慮重用,只需創建紅色滑塊。 在滑塊VStack中,用這個HStack替換Text(“Red slider”)占位符:

HStack {
  Text("0").color(.red)
  Slider(value: $rGuess, from: 0.0, through: 1.0)
  Text("255").color(.red)
}

您可以修改Text視圖以將文本顏色更改為紅色。 并且您使用值 - thumb的位置 - 在fromthrough值之間的范圍內初始化Slider

但是什么是$? 你剛剛適應了嗎 對于期權,現在是$

對于這樣一個小符號來說,它實際上非常酷而且非常強大。rGuess本身就是值 - 只讀。 $ rGuess是一個讀寫綁定 - 你需要它,在用戶更改滑塊的值時更新猜測矩形的前景色。

要查看差異,請在猜測矩形下方的三個Text視圖中設置值:

HStack {
  Text("R: \(Int(rGuess * 255.0))")
  Text("G: \(Int(gGuess * 255.0))")
  Text("B: \(Int(bGuess * 255.0))")
}

在這里,您只使用值而不是更改它們,因此您不需要$前綴。

注意:您和我知道滑塊從0變為1,但255結束標簽和0到255 RGB值適用于您的用戶,他們可能會覺得更喜歡在0到255之間的RGB值,如十六進制顏色的表示。

等待預覽刷新,看到你的第一個滑塊:

為了騰出空間,顏色塊略有縮小,但是滑塊看起來仍然有點局促 - 末端標簽太靠近窗口邊緣 - 所以在HStack中添加一些padding(另一個修改器):

HStack {
  Text("0").color(.red)
  Slider(value: $rGuess, from: 0.0, through: 1.0)
  Text("255").color(.red)
}.padding()

這就好多了

現在,如果您要復制粘貼編輯此HStack以創建綠色滑塊,則將.red更改為.green,將$ rGuess更改為$ gGuess。 這就是參數的所在。

Command-Click紅色滑塊HStack,然后選擇Extract Subview

這與Refactor ? Extract to Function的工作方式相同,但適用于SwiftUI視圖。

不要擔心出現的所有錯誤消息 - 當您編輯完新的子視圖后,它們會消失。

命名ExtractedView ColorSlider,然后在body閉合之前在頂部添加這些行:

@Binding var value: Double
var textColor: Color

然后用$ value替換$ rGuess,用textColor替換.red

Text("0").color(textColor)
Slider(value: $value, from: 0.0, through: 1.0)
Text("255").color(textColor)

然后返回到VStack中對ColorSlider()的調用,并添加您的參數:

ColorSlider(value: $rGuess, textColor: .red)

檢查預覽是否顯示紅色滑塊,然后復制粘貼編輯此行以將Text占位符替換為其他兩個滑塊:

ColorSlider(value: $gGuess, textColor: .green)
ColorSlider(value: $bGuess, textColor: .blue)

單擊Resume以更新預覽:

注意:您可能已經注意到您經常單擊Resume。 如果您不想從鍵盤上把手拿開,Option-Command-P將是您學習的最有用的鍵盤快捷鍵之一!

這就是整個應用程序的完成!

現在有趣的事情:在預覽設備的右下角,點擊實時預覽(live preview)按鈕:

實時預覽可讓您與預覽進行交互,就像您的應用程序在模擬器中運行一樣 - 太棒了!

等待預覽微調器(Preview spinner)停止,如有必要,請單擊Try Again

現在移動那些滑塊以匹配顏色!

精彩! 您是否喜歡Goodnight Developers video from the WWDC Keynote中的程序員? 太令人滿意了!


Presenting an Alert

在使用滑塊獲得良好的顏色匹配后,您的用戶可以點擊Hit Me按鈕,就像在原始的BullsEye游戲中一樣。 就像在BullsEye中一樣,會出現一個Alert,顯示分數。

首先,向ContentView添加一個方法來計算分數。 在@State變量和body之間,添加以下方法:

func computeScore() -> Int {
  let rDiff = rGuess - rTarget
  let gDiff = gGuess - gTarget
  let bDiff = bGuess - bTarget
  let diff = sqrt(rDiff * rDiff + gDiff * gDiff + bDiff * bDiff)
  return Int((1.0 - diff) * 100.0 + 0.5)
}

diff值只是三維空間中兩點之間的距離 - 用戶的錯誤。 要獲得分數,請從1減去diff,然后將其縮放到100以外的值。較小的diff會產生較高的分數。

接下來,使用Button視圖替換Text(“Hit me button”)占位符:

Button(action: {

}) {
  Text("Hit Me!")
}

Button有一個動作和一個標簽,就像一個UIButton。 您希望發生的操作是提供Alert視圖。 但是如果你只是在Button動作中創建一個Alert,它將不會做任何事情。

而是將Alert創建為ContentView的子視圖之一,并添加Bool類型的@State變量。 然后,在希望顯示Alert時將此變量的值設置為true - 在這種情況下,在Button操作中。 當用戶移除Alert時,該值將重置為false

所以添加這個@State變量,初始化為false

@State var showAlert = false

然后將此行添加為Button操作:

self.showAlert = true

你需要self,因為showAlert在一個閉包內。

最后,向Button添加一個presentation修飾符,這樣你的Button視圖如下所示:

Button(action: {
  self.showAlert = true
}) {
  Text("Hit Me!")
}
.presentation($showAlert) {
  Alert(title: Text("Your Score"), message: Text("\(computeScore())"))
}

您傳遞綁定$ showAlert,因為當用戶移除alert時,其值將更改。

SwiftUI具有用于Alert視圖的簡單初始化器,就像許多開發人員在UIAlertViewController擴展中為自己創建的初始化器一樣。 這個有一個默認的OK按鈕,所以你甚至不需要將它作為參數包含在內。

關閉live preview,單擊Resume以刷新預覽,然后啟用實時預覽live preview,并嘗試匹配目標顏色:

嘿,當你有實時預覽時,誰需要iOS模擬器? 盡管如果您在模擬器中運行應用程序,可以將其旋轉為橫向:

本教程幾乎沒有涉及SwiftUI的表面,但您現在已經了解了如何使用一些新的Xcode工具來布局和預覽視圖,以及如何使用@State變量來更新UI。更不用說那個驚人的Alert了!

您現在已做好充分準備,深入了解Apple的豐富資源 - 其教程和WWDC會議。教程tutorials和WWDC會議通過不同的示例項目。例如,Introducing SwiftUI#204)為會議構建了一個列表應用程序 - 它比教程的Landmarks應用程序更簡單。 SwiftUI Essentials#216)向您展示了如何使用Form容器視圖輕松獲取iOS表單的外觀。

為了簡化本教程,我沒有為RGB顏色創建數據模型。但大多數應用程序將其數據建模為結構體或類。如果需要SwiftUI來跟蹤模型實例中的更改,則必須通過實現發布更改事件的didChange屬性來符合BindableObject。看看Apple的示例項目,特別是Data Flow Through SwiftUI#226

為了使自己更容易進入SwiftUI,您可以將SwiftUI視圖添加到現有應用程序,或者在新的SwiftUI應用程序中使用現有視圖 - 觀看Integrating SwiftUI#231)以查看這樣做的快捷方式。

另外,瀏覽SwiftUI documentation以查看其他可用內容 - 有很多內容!

后記

本篇主要講述了SwiftUI相關,感興趣的給個贊或者關注~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容