版本記錄
版本號(hào) | 時(shí)間 |
---|---|
V1.0 | 2019.09.16 星期一 |
前言
今天翻閱蘋(píng)果的API文檔,發(fā)現(xiàn)多了一個(gè)框架SwiftUI,這里我們就一起來(lái)看一下這個(gè)框架。感興趣的看下面幾篇文章。
1. SwiftUI框架詳細(xì)解析 (一) —— 基本概覽(一)
開(kāi)始
首先看下主要內(nèi)容
學(xué)習(xí)構(gòu)建一個(gè)使用動(dòng)畫(huà)和SwiftUI的啟動(dòng)畫(huà)面,超越典型的靜態(tài)啟動(dòng)畫(huà)面,并在應(yīng)用程序加載時(shí)讓用戶感興趣。
下面看一下寫(xiě)作環(huán)境
Swift 5, iOS 13, Xcode 11
精彩的啟動(dòng)畫(huà)面 - 開(kāi)發(fā)人員有機(jī)會(huì)玩有趣的動(dòng)畫(huà),因?yàn)閼?yīng)用程序瘋狂地為其需要運(yùn)行的關(guān)鍵數(shù)據(jù)執(zhí)行API端點(diǎn)。 與靜態(tài)無(wú)動(dòng)畫(huà)啟動(dòng)屏幕相反,啟動(dòng)畫(huà)面可以在應(yīng)用程序中發(fā)揮重要作用:在用戶等待應(yīng)用程序啟動(dòng)時(shí)保持用戶的興趣。
本教程將逐步指導(dǎo)您從沒(méi)有啟動(dòng)畫(huà)面的應(yīng)用程序到具有炫酷閃屏的應(yīng)用程序,這將是其他人的羨慕。 你還在等什么?
注意:本教程假設(shè)您對(duì)
SwiftUI
動(dòng)畫(huà),狀態(tài)和修飾符感到滿意。 本教程不是介紹這些概念,而是專注于使用它們來(lái)復(fù)制一個(gè)很酷的動(dòng)畫(huà)。
在本教程中,您將增強(qiáng)一個(gè)名為Fuber
的應(yīng)用程序。 Fuber
是一種按需乘車(chē)共享服務(wù),允許乘客請(qǐng)求Segway司機(jī)將他們運(yùn)送到城市環(huán)境中的不同位置。
Fuber
發(fā)展迅速,目前在60多個(gè)國(guó)家為Segway
代客提供服務(wù),但由于使用Segway
司機(jī),它面臨眾多政府以及Segway
工會(huì)的反對(duì)。
打開(kāi)啟動(dòng)項(xiàng)目并瀏覽一下。
正如您在ContentView.swift
中看到的,當(dāng)前所有應(yīng)用程序都會(huì)顯示SplashScreen
兩秒鐘,然后將其淡化以顯示MapView
。
注意:在生產(chǎn)應(yīng)用程序中,此循環(huán)的退出條件可能是API端點(diǎn)的握手成功,它為應(yīng)用程序提供了繼續(xù)所需的數(shù)據(jù)。
啟動(dòng)畫(huà)面位于自己的模塊中:SplashScreen.swift
。 您可以看到它有一個(gè)帶有“F ber”
標(biāo)簽的Fuber-blue
背景,等待您添加動(dòng)畫(huà)“U”
。
構(gòu)建并運(yùn)行啟動(dòng)項(xiàng)目。
幾秒鐘之后,你會(huì)看到一個(gè)不太令人興奮的靜態(tài)閃屏,它會(huì)轉(zhuǎn)換到地圖(Fuber
的主屏幕)。
您將花費(fèi)本教程的其余部分將這個(gè)無(wú)聊的靜態(tài)啟動(dòng)畫(huà)面轉(zhuǎn)換為精美動(dòng)畫(huà)的屏幕,這將使您的用戶希望主屏幕永遠(yuǎn)不會(huì)加載。 看看你將構(gòu)建的內(nèi)容:
注意:如果您正在運(yùn)行
macOS Catalina beta
,您可以使用實(shí)時(shí)預(yù)覽代替在模擬器上構(gòu)建和運(yùn)行。
Understanding the Composition of Views and Layers
新的和改進(jìn)的SplashScreen
將包含幾個(gè)子視圖,所有子視圖都在ZStack
中方便地組織:
- 網(wǎng)格背景由較小的
“Chimes”
圖像的圖塊組成,它與啟動(dòng)項(xiàng)目一起提供。 -
“F ber”
文本,有動(dòng)畫(huà)FuberU
的空間。 -
FuberU
,代表'U'
的圓形白色背景。 - 一個(gè)矩形,代表
FuberU
中間的正方形。 - 另一個(gè)矩形表示從
FuberU
中間到其外邊緣的直線。 -
Spacer
視圖,以確保ZStack
大小將覆蓋整個(gè)屏幕。
結(jié)合起來(lái),這些視圖創(chuàng)建了Fuber'U'
動(dòng)畫(huà)。
starter
項(xiàng)目提供Text
和Spacer
視圖。 您將在以下部分中添加其余視圖。
既然您已經(jīng)知道如何構(gòu)建這些圖層,那么您可以開(kāi)始創(chuàng)建和動(dòng)畫(huà)FuberU
。
Animating the Circle
使用動(dòng)畫(huà)時(shí),最好關(guān)注您當(dāng)前正在實(shí)施的動(dòng)畫(huà)。 打開(kāi)ContentView.swift
并注釋掉.onAppear
閉包。 它應(yīng)該如下所示:
.onAppear {
//DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// withAnimation() {
// self.showSplash = false
// }
//}
}
通過(guò)這種方式,您不會(huì)被閃屏分散注意力,在X
秒后淡出顯示MapView
。 別擔(dān)心,當(dāng)你完成并準(zhǔn)備時(shí),你會(huì)取消注釋。
您現(xiàn)在可以專注于動(dòng)畫(huà)。 首先打開(kāi)SplashScreen.swift
,然后在SplashScreen
的結(jié)束括號(hào)下面添加一個(gè)名為FuberU
的新結(jié)構(gòu):
struct FuberU: Shape {
var percent: Double
// 1
func path(in rect: CGRect) -> Path {
let end = percent * 360
var p = Path()
// 2
p.addArc(center: CGPoint(x: rect.size.width/2, y: rect.size.width/2),
radius: rect.size.width/2,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: end),
clockwise: false)
return p
}
// 3
var animatableData: Double {
get { return percent }
set { percent = newValue }
}
}
以下是您使用此代碼所做的事情:
- 1) 根據(jù)
Shape
協(xié)議的要求實(shí)現(xiàn)path(in:)
。 - 2) 使用路徑繪制從0開(kāi)始到360結(jié)束的弧,即一個(gè)完整的圓。
- 3) 添加額外的屬性,以便
SwiftUI
知道如何為你的形狀設(shè)置動(dòng)畫(huà)。
為了看到你的新類(lèi)型,你將設(shè)置一些變量和一個(gè)動(dòng)畫(huà),然后在body
上用一些修飾符聲明它。
首先在SplashScreen
結(jié)構(gòu)中的body
元素之前添加這些變量:
@State var percent = 0.0
let uLineWidth: CGFloat = 5
在啟動(dòng)和修改FuberU
時(shí),您將使用這些變量。
然后,在SplashScreen
的struct
結(jié)束括號(hào)后添加以下代碼:
extension SplashScreen {
var uAnimationDuration: Double { return 1.0 }
func handleAnimations() {
runAnimationPart1()
}
func runAnimationPart1() {
withAnimation(.easeIn(duration: uAnimationDuration)) {
percent = 1
}
}
}
handleAnimations()
將成為初始屏幕復(fù)雜動(dòng)畫(huà)的所有不同部分的基礎(chǔ)。 它基于magic numbers
,你可以玩,并調(diào)整,以配合你以后的確切口味。
最后,在body
中,在現(xiàn)有的Text
和Spacer
元素之間添加以下代碼。
FuberU(percent: percent)
.stroke(Color.white, lineWidth: uLineWidth)
.onAppear() {
self.handleAnimations()
}
.frame(width: 45, height: 45, alignment: .center)
在這里,您將新圓圈(最終將代表Fuber'U'
的一部分)添加到特定位置的堆棧。 此外,在視圖出現(xiàn)時(shí)調(diào)用handleAnimations()
。
構(gòu)建并運(yùn)行您的應(yīng)用:
你可以看到正在發(fā)生的事情,但這并不是你所期望的。 你的代碼確實(shí)畫(huà)了一個(gè)圓圈,但只有一次,圓圈的邊界太薄了。 你希望它填滿整個(gè)圈子。 你馬上解決這些問(wèn)題。
Improving the Circle Animation
首先在runAnimationPart1()
之后添加此代碼:
func restartAnimation() {
let deadline: DispatchTime = .now() + uAnimationDuration
DispatchQueue.main.asyncAfter(deadline: deadline) {
self.percent = 0
self.handleAnimations()
}
}
要調(diào)用此方法,請(qǐng)?jiān)?code>handleAnimations()的末尾添加以下行:
restartAnimation()
此代碼通過(guò)等待其持續(xù)時(shí)間重置percent
然后再次調(diào)用它來(lái)循環(huán)動(dòng)畫(huà)。
現(xiàn)在圓圈動(dòng)畫(huà)重復(fù)出現(xiàn),您可以向FuberU
添加修飾符,使其看起來(lái)與您想要的完全一樣。 首先,在body
之前添加這些新變量:
@State var uScale: CGFloat = 1
let uZoomFactor: CGFloat = 1.4
現(xiàn)在,在FuberU
上的stroke(_:lineWidth :)
和onAppear()
修飾符之間添加以下三個(gè)新修飾符:
.rotationEffect(.degrees(-90))
.aspectRatio(1, contentMode: .fit)
.padding(20)
最后,在frame(width:height:alignment :)
之前添加一個(gè)scaleEffect(_:anchor :)
:
.scaleEffect(uScale * uZoomFactor)
您的FuberU
聲明現(xiàn)在看起來(lái)像這樣:
FuberU(percent: percent)
.stroke(Color.white, lineWidth: uLineWidth)
.rotationEffect(.degrees(-90))
.aspectRatio(1, contentMode: .fit)
.padding(20)
.onAppear() {
self.handleAnimations()
}
.scaleEffect(uScale * uZoomFactor)
.frame(width: 45, height: 45, alignment: .center)
此代碼使線條更寬,添加了一個(gè)旋轉(zhuǎn),以便繪圖從頂部開(kāi)始并添加縮放效果,以便圓圈在動(dòng)畫(huà)時(shí)生長(zhǎng)。
在將percent
更新為1
后立即通過(guò)在動(dòng)畫(huà)塊內(nèi)的runAnimationPart1()
中添加以下行來(lái)完成此部分:
uScale = 5
使用此代碼,您將uScale
狀態(tài)從1
更改為5
。
構(gòu)建并運(yùn)行您的應(yīng)用:
現(xiàn)在圓圈的行為與您預(yù)期的一樣 - 您的應(yīng)用程序從0
到360
度繪制一個(gè)完整的白色圓圈,在此過(guò)程中會(huì)有所增長(zhǎng)。
你可能會(huì)注意到圓圈在第一個(gè)繪制周期中的尺寸只會(huì)增加。 那是因?yàn)槟銖奈粗匦鲁跏蓟?code>uScale。 別擔(dān)心,您將在動(dòng)畫(huà)的下一步中解決這個(gè)問(wèn)題。
注意:嘗試使用
FuberU
修飾符 - 刪除一些,添加新的,更改值等等。 當(dāng)您觀察視圖更改時(shí),您將更好地了解每個(gè)修改符的作用。
Adding the Square
隨著Fuber'U'
的動(dòng)畫(huà)完成,是時(shí)候添加square
了。
首先,在body
之前添加這些新的狀態(tài)和屬性:
@State var squareColor = Color.white
@State var squareScale: CGFloat = 1
let uSquareLength: CGFloat = 12
在ZStack
的FuberU
之后,為中心方塊添加一個(gè)Rectangle
視圖:
Rectangle()
.fill(squareColor)
.scaleEffect(squareScale * uZoomFactor)
.frame(width: uSquareLength, height: uSquareLength, alignment: .center)
.onAppear() {
self.squareColor = self.fuberBlue
}
您添加了一個(gè)正方形,其大小和填充顏色將在整個(gè)動(dòng)畫(huà)中發(fā)生變化。
構(gòu)建并運(yùn)行您的應(yīng)用:
如您所見(jiàn),圓圈以預(yù)期大小顯示在正方形后面,但沒(méi)有動(dòng)畫(huà)。 您仍需要添加所有準(zhǔn)備工作,然后按正確的順序處理動(dòng)畫(huà)。
接下來(lái),線!
Adding the Line
現(xiàn)在,你需要添加一行,使你的'U'
看起來(lái)更像字母'U'
,而不像一個(gè)圓圈,上面有一個(gè)正方形。
在body
之前添加以下屬性和狀態(tài):
@State var lineScale: CGFloat = 1
let lineWidth: CGFloat = 4
let lineHeight: CGFloat = 28
然后在Spacer
之前的ZStack
末尾添加一個(gè)Rectangle
視圖。
Rectangle()
.fill(fuberBlue)
.scaleEffect(lineScale, anchor: .bottom)
.frame(width: lineWidth, height: lineHeight, alignment: .center)
.offset(x: 0, y: -22)
構(gòu)建并運(yùn)行您的應(yīng)用:
現(xiàn)在您擁有了Fuber'U'
的所有元素,您可以使動(dòng)畫(huà)更復(fù)雜一些。 你準(zhǔn)備好迎接挑戰(zhàn)了嗎?
Completing the U Animation
你想制作的'U'
動(dòng)畫(huà)有三個(gè)階段:
- 圓圈在繪制時(shí)放大。
- 圓圈迅速縮小成正方形。
- 方形消失了。
在擴(kuò)展現(xiàn)有的handleAnimations()
時(shí),您將使用這三個(gè)階段。 首先在uAnimationDuration
之后添加這些新屬性:
var uAnimationDelay: Double { return 0.2 }
var uExitAnimationDuration: Double{ return 0.3 }
var finalAnimationDuration: Double { return 0.4 }
var minAnimationInterval: Double { return 0.1 }
var fadeAnimationDuration: Double { return 0.4 }
這些神奇的數(shù)字是反復(fù)試驗(yàn)的結(jié)果。 隨意嘗試它們,看看你是否感覺(jué)它們改進(jìn)了動(dòng)畫(huà),或者只是為了讓你更容易理解它們是如何工作的。
在uScale = 5
之后,再添加一行到runAnimationPart1()
的末尾:
lineScale = 1
將以下代碼添加到runAnimationPart1()
的末尾,緊跟動(dòng)畫(huà)塊的右括號(hào)后:
//TODO: Add code #1 for text here
let deadline: DispatchTime = .now() + uAnimationDuration + uAnimationDelay
DispatchQueue.main.asyncAfter(deadline: deadline) {
withAnimation(.easeOut(duration: self.uExitAnimationDuration)) {
self.uScale = 0
self.lineScale = 0
}
withAnimation(.easeOut(duration: self.minAnimationInterval)) {
self.squareScale = 0
}
//TODO: Add code #2 for text here
}
在這里,您使用帶有截止時(shí)間的異步調(diào)用來(lái)在第一個(gè)動(dòng)畫(huà)運(yùn)行后運(yùn)行代碼。 請(qǐng)注意,您有一些文本動(dòng)畫(huà)占位符,你很快就會(huì)解決這些問(wèn)題。
現(xiàn)在是動(dòng)畫(huà)第二部分的時(shí)候了。 在runAnimationPart1()
的結(jié)束括號(hào)后添加:
func runAnimationPart2() {
let deadline: DispatchTime = .now() + uAnimationDuration +
uAnimationDelay + minAnimationInterval
DispatchQueue.main.asyncAfter(deadline: deadline) {
self.squareColor = Color.white
self.squareScale = 1
}
}
確保在handleAnimations()
中的runAnimationPart1()
之后立即添加對(duì)新函數(shù)的調(diào)用:
runAnimationPart2()
現(xiàn)在,在runAnimationPart2()
之后添加動(dòng)畫(huà)的第三部分:
func runAnimationPart3() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2 * uAnimationDuration) {
withAnimation(.easeIn(duration: self.finalAnimationDuration)) {
//TODO: Add code #3 for text here
self.squareColor = self.fuberBlue
}
}
}
請(qǐng)注意,代碼中包含TODO
,以顯示本教程后面將為文本設(shè)置動(dòng)畫(huà)的確切位置。
現(xiàn)在,在runAnimationPart2()
之后立即在handleAnimations()
中添加新動(dòng)畫(huà):
runAnimationPart3()
要完成此階段,請(qǐng)使用以下新實(shí)現(xiàn)替換restartAnimation()
:
func restartAnimation() {
let deadline: DispatchTime = .now() + 2 * uAnimationDuration +
finalAnimationDuration
DispatchQueue.main.asyncAfter(deadline: deadline) {
self.percent = 0
//TODO: Add code #4 for text here
self.handleAnimations()
}
}
請(qǐng)注意,您根據(jù)為特定步驟定義的magic numbers
計(jì)劃動(dòng)畫(huà)的每個(gè)步驟從某個(gè)點(diǎn)開(kāi)始。
構(gòu)建并運(yùn)行您的應(yīng)用程序,然后觀察美觀!
如果您查看完成的動(dòng)畫(huà),您將看到文本開(kāi)始透明和小,然后淡入,用彈簧放大,最后消失。 現(xiàn)在是時(shí)候把所有這些放到位了。
Animating the Text
“F ber”
文本從一開(kāi)始就存在,但它有點(diǎn)無(wú)聊,因?yàn)樗鼪](méi)有與'U'
一起動(dòng)畫(huà)。 要解決這個(gè)問(wèn)題,您需要為Text
添加兩個(gè)新修飾符。 首先,在body
之前添加兩個(gè)新?tīng)顟B(tài):
@State var textAlpha = 0.0
@State var textScale: CGFloat = 1
現(xiàn)在,是時(shí)候用實(shí)際動(dòng)畫(huà)替換這些占位符了。
替換//TODO: Add code #1 for text here:
為
withAnimation(Animation.easeIn(duration: uAnimationDuration).delay(0.5)) {
textAlpha = 1.0
}
其次,替換//TODO: Add code #2 for text here
為:
withAnimation(Animation.spring()) {
self.textScale = self.uZoomFactor
}
接下來(lái),替換//TODO: Add code #3 for text here
為:
self.textAlpha = 0
最后,替換//TODO: Add code #4 for text here
為:
self.textScale = 1
現(xiàn)在,將Text
替換為以下內(nèi)容:
Text("F BER")
.font(.largeTitle)
.foregroundColor(.white)
.opacity(textAlpha)
.offset(x: 20, y: 0)
.scaleEffect(textScale)
構(gòu)建并運(yùn)行您的應(yīng)用:
文本視圖通過(guò)動(dòng)畫(huà)響應(yīng)兩個(gè)新?tīng)顟B(tài)變量的變化! 多么酷啊?
現(xiàn)在,剩下的就是添加背景,你的真棒啟動(dòng)畫(huà)面動(dòng)畫(huà)將完成。 深吸一口氣,跳進(jìn)動(dòng)畫(huà)的最后一部分。
Animating the Background
您將首先添加ZStack
的背景。 由于它是背景,它應(yīng)該是堆棧背面的視圖,因此它必須首先出現(xiàn)在代碼中。 為此,添加一個(gè)新的Image
視圖作為SplashScreen
的ZStack
的第一個(gè)元素:
Image("Chimes")
.resizable(resizingMode: .tile)
.opacity(textAlpha)
.scaleEffect(textScale)
這使用Chimes
資源制作填滿整個(gè)屏幕的圖塊。 請(qǐng)注意,您使用textAlpha
和textScale
作為狀態(tài)變量,因此只要這些狀態(tài)變量發(fā)生更改,視圖就會(huì)更改其不透明度和比例。 由于它們已經(jīng)更改為“F ber”
文本的動(dòng)畫(huà),因此您無(wú)需執(zhí)行任何其他操作即可激活它們。
構(gòu)建并運(yùn)行應(yīng)用程序,您將看到背景動(dòng)畫(huà)以及文本:
你現(xiàn)在需要添加漣漪效果,當(dāng)Fuber'U'
收縮成正方形時(shí),它會(huì)淡化背景。 您可以通過(guò)在背景視圖正上方添加一個(gè)半透明圓圈,在所有其他視圖下方來(lái)實(shí)現(xiàn)。 該圓圈將從Fuber'U'
的中間動(dòng)畫(huà),以覆蓋整個(gè)屏幕并隱藏背景。 聽(tīng)起來(lái)很容易,對(duì)吧?
添加圓圈動(dòng)畫(huà)所需的這兩個(gè)新?tīng)顟B(tài)變量:
@State var coverCircleScale: CGFloat = 1
@State var coverCircleAlpha = 0.0
然后在背景圖像視圖后面的ZStack
中添加這個(gè)新視圖:
Circle()
.fill(fuberBlue)
.frame(width: 1, height: 1, alignment: .center)
.scaleEffect(coverCircleScale)
.opacity(coverCircleAlpha)
現(xiàn)在,您需要在恰當(dāng)?shù)臅r(shí)刻更改這些狀態(tài)變量的值以啟動(dòng)動(dòng)畫(huà)。 將此子句添加到runAnimationPart2()
,在self.squareScale = 1
的下面:
withAnimation(.easeOut(duration: self.fadeAnimationDuration)) {
self.coverCircleAlpha = 1
self.coverCircleScale = 1000
}
最后,不要忘記在動(dòng)畫(huà)完成并準(zhǔn)備重新啟動(dòng)時(shí)初始化圓的大小和不透明度。 在重新調(diào)用handleAnimations()
之前將其添加到restartAnimation()
:
self.coverCircleAlpha = 0
self.coverCircleScale = 1
現(xiàn)在構(gòu)建并運(yùn)行您的應(yīng)用程序! 你已經(jīng)實(shí)現(xiàn)了你想要實(shí)現(xiàn)的完整,復(fù)雜,非常棒的動(dòng)畫(huà)。 給自己一個(gè)輕拍。 這不是微不足道的,但是你一直都是這樣做的。
現(xiàn)在坐下來(lái),放松一下,繼續(xù)完成一些重要的記憶,特別是在真正的應(yīng)用程序中。
Finishing Touches
你制作的動(dòng)畫(huà)非???,但是你現(xiàn)在實(shí)現(xiàn)它的方式,動(dòng)畫(huà)將在閃屏逐漸消失后不斷重復(fù)。
這還沒(méi)完成。 在啟動(dòng)屏幕消失后,您需要阻止動(dòng)畫(huà)重新啟動(dòng),因?yàn)樗^續(xù)超過(guò)該點(diǎn)是沒(méi)有意義的。 用戶無(wú)論如何都不會(huì)看到它,并且它使用不必要的資源。
要阻止動(dòng)畫(huà)顯示超出其需要的時(shí)間,請(qǐng)向SplashScreen
添加一個(gè)新的靜態(tài)變量:
static var shouldAnimate = true
在handleAnimations()
中,使用if
語(yǔ)句包裝restartAnimation()
,這樣一旦新的Boolean
為false
,就會(huì)阻止它重新開(kāi)始。 它應(yīng)該如下所示:
if SplashScreen.shouldAnimate {
restartAnimation()
}
現(xiàn)在,返回ContentView.swift
,取消注釋您在開(kāi)頭注釋掉的.onAppear
閉包,并將shouldAnimate
設(shè)置為false
。 然后,只是為了好玩,還將第二個(gè)常量更改為10
,這樣您就有機(jī)會(huì)享受您創(chuàng)建的漂亮的閃屏動(dòng)畫(huà)。 它現(xiàn)在應(yīng)該是這樣的:
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
SplashScreen.shouldAnimate = false
withAnimation() {
self.showSplash = false
}
}
}
構(gòu)建并運(yùn)行您的應(yīng)用:
你應(yīng)該看到你的炫酷閃屏顯示10
秒,然后是應(yīng)用程序的主地圖視圖。 關(guān)于它的最好的部分是,一旦啟動(dòng)屏幕消失,它就不再在后臺(tái)動(dòng)畫(huà),因此您的用戶可以自由地體驗(yàn)應(yīng)用程序的所有榮耀,而無(wú)需任何后臺(tái)動(dòng)畫(huà)減慢它們的速度。
后記
本篇主要講述了基于SwiftUI的閃屏頁(yè)的創(chuàng)建,感興趣的給個(gè)贊或者關(guān)注~~~