SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)

版本记录

版本号 时间
V1.0 2019.09.16 星期一

前言

今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)

开始

首先看下主要内容

学习构建一个使用动画和SwiftUI的启动画面,超越典型的静态启动画面,并在应用程序加载时让用户感兴趣。

下面看一下写作环境

Swift 5, iOS 13, Xcode 11

精彩的启动画面 - 开发人员有机会玩有趣的动画,因为应用程序疯狂地为其需要运行的关键数据执行API端点。 与静态无动画启动屏幕相反,启动画面可以在应用程序中发挥重要作用:在用户等待应用程序启动时保持用户的兴趣。

本教程将逐步指导您从没有启动画面的应用程序到具有炫酷闪屏的应用程序,这将是其他人的羡慕。 你还在等什么?

注意:本教程假设您对SwiftUI动画,状态和修饰符感到满意。 本教程不是介绍这些概念,而是专注于使用它们来复制一个很酷的动画。

在本教程中,您将增强一个名为Fuber的应用程序。 Fuber是一种按需乘车共享服务,允许乘客请求Segway司机将他们运送到城市环境中的不同位置。

Fuber发展迅速,目前在60多个国家为Segway代客提供服务,但由于使用Segway司机,它面临众多政府以及Segway工会的反对。

打开启动项目并浏览一下。

正如您在ContentView.swift中看到的,当前所有应用程序都会显示SplashScreen两秒钟,然后将其淡化以显示MapView

注意:在生产应用程序中,此循环的退出条件可能是API端点的握手成功,它为应用程序提供了继续所需的数据。

启动画面位于自己的模块中:SplashScreen.swift。 您可以看到它有一个带有“F ber”标签的Fuber-blue背景,等待您添加动画“U”

构建并运行启动项目。

几秒钟之后,你会看到一个不太令人兴奋的静态闪屏,它会转换到地图(Fuber的主屏幕)。

您将花费本教程的其余部分将这个无聊的静态启动画面转换为精美动画的屏幕,这将使您的用户希望主屏幕永远不会加载。 看看你将构建的内容:

注意:如果您正在运行macOS Catalina beta,您可以使用实时预览代替在模拟器上构建和运行。


Understanding the Composition of Views and Layers

新的和改进的SplashScreen将包含几个子视图,所有子视图都在ZStack中方便地组织:

  • 网格背景由较小的“Chimes”图像的图块组成,它与启动项目一起提供。
  • “F ber”文本,有动画FuberU的空间。
  • FuberU,代表'U'的圆形白色背景。
  • 一个矩形,代表FuberU中间的正方形。
  • 另一个矩形表示从FuberU中间到其外边缘的直线。
  • Spacer视图,以确保ZStack大小将覆盖整个屏幕。

结合起来,这些视图创建了Fuber'U'动画。

starter项目提供TextSpacer视图。 您将在以下部分中添加其余视图。

既然您已经知道如何构建这些图层,那么您可以开始创建和动画FuberU


Animating the Circle

使用动画时,最好关注您当前正在实施的动画。 打开ContentView.swift并注释掉.onAppear闭包。 它应该如下所示:

.onAppear {
//DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
//  withAnimation() {
//    self.showSplash = false
//  }
//}
}

通过这种方式,您不会被闪屏分散注意力,在X秒后淡出显示MapView。 别担心,当你完成并准备时,你会取消注释。

您现在可以专注于动画。 首先打开SplashScreen.swift,然后在SplashScreen的结束括号下面添加一个名为FuberU的新结构:

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) 根据Shape协议的要求实现path(in:)
  • 2) 使用路径绘制从0开始到360结束的弧,即一个完整的圆。
  • 3) 添加额外的属性,以便SwiftUI知道如何为你的形状设置动画。

为了看到你的新类型,你将设置一些变量和一个动画,然后在body上用一些修饰符声明它。

首先在SplashScreen结构中的body元素之前添加这些变量:

@State var percent = 0.0
let uLineWidth: CGFloat = 5

在启动和修改FuberU时,您将使用这些变量。

然后,在SplashScreenstruct结束括号后添加以下代码:

extension SplashScreen {
  var uAnimationDuration: Double { return 1.0 }
    
  func handleAnimations() {
    runAnimationPart1()
  }

  func runAnimationPart1() {
    withAnimation(.easeIn(duration: uAnimationDuration)) {
      percent = 1
    }
  }
}

handleAnimations()将成为初始屏幕复杂动画的所有不同部分的基础。 它基于magic numbers,你可以玩,并调整,以配合你以后的确切口味。

最后,在body中,在现有的TextSpacer元素之间添加以下代码。

FuberU(percent: percent)
 .stroke(Color.white, lineWidth: uLineWidth)
 .onAppear() {
   self.handleAnimations()
 }
 .frame(width: 45, height: 45, alignment: .center)

在这里,您将新圆圈(最终将代表Fuber'U'的一部分)添加到特定位置的堆栈。 此外,在视图出现时调用handleAnimations()

构建并运行您的应用:

你可以看到正在发生的事情,但这并不是你所期望的。 你的代码确实画了一个圆圈,但只有一次,圆圈的边界太薄了。 你希望它填满整个圈子。 你马上解决这些问题。


Improving the Circle Animation

首先在runAnimationPart1()之后添加此代码:

func restartAnimation() {
  let deadline: DispatchTime = .now() + uAnimationDuration
  DispatchQueue.main.asyncAfter(deadline: deadline) {
    self.percent = 0
    self.handleAnimations()
  }
}

要调用此方法,请在handleAnimations()的末尾添加以下行:

restartAnimation()

此代码通过等待其持续时间重置percent然后再次调用它来循环动画。

现在圆圈动画重复出现,您可以向FuberU添加修饰符,使其看起来与您想要的完全一样。 首先,在body之前添加这些新变量:

@State var uScale: CGFloat = 1
let uZoomFactor: CGFloat = 1.4

现在,在FuberU上的stroke(_:lineWidth :)onAppear()修饰符之间添加以下三个新修饰符:

.rotationEffect(.degrees(-90))
.aspectRatio(1, contentMode: .fit)
.padding(20)

最后,在frame(width:height:alignment :)之前添加一个scaleEffect(_:anchor :)

.scaleEffect(uScale * uZoomFactor)

您的FuberU声明现在看起来像这样:

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)

此代码使线条更宽,添加了一个旋转,以便绘图从顶部开始并添加缩放效果,以便圆圈在动画时生长。

在将percent更新为1后立即通过在动画块内的runAnimationPart1()中添加以下行来完成此部分:

uScale = 5

使用此代码,您将uScale状态从1更改为5

构建并运行您的应用:

现在圆圈的行为与您预期的一样 - 您的应用程序从0360度绘制一个完整的白色圆圈,在此过程中会有所增长。

你可能会注意到圆圈在第一个绘制周期中的尺寸只会增加。 那是因为你从未重新初始化uScale。 别担心,您将在动画的下一步中解决这个问题。

注意:尝试使用FuberU修饰符 - 删除一些,添加新的,更改值等等。 当您观察视图更改时,您将更好地了解每个修改符的作用。


Adding the Square

随着Fuber'U'的动画完成,是时候添加square了。

首先,在body之前添加这些新的状态和属性:

@State var squareColor = Color.white
@State var squareScale: CGFloat = 1

let uSquareLength: CGFloat = 12

ZStackFuberU之后,为中心方块添加一个Rectangle视图:

Rectangle()
  .fill(squareColor)
  .scaleEffect(squareScale * uZoomFactor)
  .frame(width: uSquareLength, height: uSquareLength, alignment: .center)
  .onAppear() {
      self.squareColor = self.fuberBlue
  }

您添加了一个正方形,其大小和填充颜色将在整个动画中发生变化。

构建并运行您的应用:

如您所见,圆圈以预期大小显示在正方形后面,但没有动画。 您仍需要添加所有准备工作,然后按正确的顺序处理动画。

接下来,线!


Adding the Line

现在,你需要添加一行,使你的'U'看起来更像字母'U',而不像一个圆圈,上面有一个正方形。

body之前添加以下属性和状态:

@State var lineScale: CGFloat = 1

let lineWidth:  CGFloat = 4
let lineHeight: CGFloat = 28

然后在Spacer之前的ZStack末尾添加一个Rectangle视图。

Rectangle()
  .fill(fuberBlue)
  .scaleEffect(lineScale, anchor: .bottom)
  .frame(width: lineWidth, height: lineHeight, alignment: .center)
  .offset(x: 0, y: -22)

构建并运行您的应用:

现在您拥有了Fuber'U'的所有元素,您可以使动画更复杂一些。 你准备好迎接挑战了吗?


Completing the U Animation

你想制作的'U'动画有三个阶段:

  • 圆圈在绘制时放大。
  • 圆圈迅速缩小成正方形。
  • 方形消失了。

在扩展现有的handleAnimations()时,您将使用这三个阶段。 首先在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 }

这些神奇的数字是反复试验的结果。 随意尝试它们,看看你是否感觉它们改进了动画,或者只是为了让你更容易理解它们是如何工作的。

uScale = 5之后,再添加一行到runAnimationPart1()的末尾:

lineScale = 1

将以下代码添加到runAnimationPart1()的末尾,紧跟动画块的右括号后:

//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
}   

在这里,您使用带有截止时间的异步调用来在第一个动画运行后运行代码。 请注意,您有一些文本动画占位符,你很快就会解决这些问题。

现在是动画第二部分的时候了。 在runAnimationPart1()的结束括号后添加:

func runAnimationPart2() {
  let deadline: DispatchTime = .now() + uAnimationDuration + 
    uAnimationDelay + minAnimationInterval
  DispatchQueue.main.asyncAfter(deadline: deadline) {
    self.squareColor = Color.white
    self.squareScale = 1
  }
}   

确保在handleAnimations()中的runAnimationPart1()之后立即添加对新函数的调用:

runAnimationPart2()

现在,在runAnimationPart2()之后添加动画的第三部分:

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
  }
  }
}

请注意,代码中包含TODO,以显示本教程后面将为文本设置动画的确切位置。

现在,在runAnimationPart2()之后立即在handleAnimations()中添加新动画:

runAnimationPart3()

要完成此阶段,请使用以下新实现替换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()
    }
}

请注意,您根据为特定步骤定义的magic numbers计划动画的每个步骤从某个点开始。

构建并运行您的应用程序,然后观察美观!

如果您查看完成的动画,您将看到文本开始透明和小,然后淡入,用弹簧放大,最后消失。 现在是时候把所有这些放到位了。


Animating the Text

“F ber”文本从一开始就存在,但它有点无聊,因为它没有与'U'一起动画。 要解决这个问题,您需要为Text添加两个新修饰符。 首先,在body之前添加两个新状态:

@State var textAlpha = 0.0
@State var textScale: CGFloat = 1

现在,是时候用实际动画替换这些占位符了。

替换//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
}

接下来,替换//TODO: Add code #3 for text here为:

self.textAlpha = 0

最后,替换//TODO: Add code #4 for text here为:

self.textScale = 1

现在,将Text替换为以下内容:

Text("F           BER")
  .font(.largeTitle)
  .foregroundColor(.white)
  .opacity(textAlpha)
  .offset(x: 20, y: 0)
  .scaleEffect(textScale)

构建并运行您的应用:

文本视图通过动画响应两个新状态变量的变化! 多么酷啊?

现在,剩下的就是添加背景,你的真棒启动画面动画将完成。 深吸一口气,跳进动画的最后一部分。


Animating the Background

您将首先添加ZStack的背景。 由于它是背景,它应该是堆栈背面的视图,因此它必须首先出现在代码中。 为此,添加一个新的Image视图作为SplashScreenZStack的第一个元素:

Image("Chimes")
  .resizable(resizingMode: .tile)
  .opacity(textAlpha)
  .scaleEffect(textScale)

这使用Chimes资源制作填满整个屏幕的图块。 请注意,您使用textAlphatextScale作为状态变量,因此只要这些状态变量发生更改,视图就会更改其不透明度和比例。 由于它们已经更改为“F ber”文本的动画,因此您无需执行任何其他操作即可激活它们。

构建并运行应用程序,您将看到背景动画以及文本:

你现在需要添加涟漪效果,当Fuber'U'收缩成正方形时,它会淡化背景。 您可以通过在背景视图正上方添加一个半透明圆圈,在所有其他视图下方来实现。 该圆圈将从Fuber'U'的中间动画,以覆盖整个屏幕并隐藏背景。 听起来很容易,对吧?

添加圆圈动画所需的这两个新状态变量:

@State var coverCircleScale: CGFloat = 1
@State var coverCircleAlpha = 0.0

然后在背景图像视图后面的ZStack中添加这个新视图:

Circle()
  .fill(fuberBlue) 
  .frame(width: 1, height: 1, alignment: .center)
  .scaleEffect(coverCircleScale)
  .opacity(coverCircleAlpha)

现在,您需要在恰当的时刻更改这些状态变量的值以启动动画。 将此子句添加到runAnimationPart2(),在self.squareScale = 1的下面:

withAnimation(.easeOut(duration: self.fadeAnimationDuration)) {
  self.coverCircleAlpha = 1
  self.coverCircleScale = 1000
}

最后,不要忘记在动画完成并准备重新启动时初始化圆的大小和不透明度。 在重新调用handleAnimations()之前将其添加到restartAnimation()

self.coverCircleAlpha = 0
self.coverCircleScale = 1

现在构建并运行您的应用程序! 你已经实现了你想要实现的完整,复杂,非常棒的动画。 给自己一个轻拍。 这不是微不足道的,但是你一直都是这样做的。

现在坐下来,放松一下,继续完成一些重要的记忆,特别是在真正的应用程序中。


Finishing Touches

你制作的动画非常酷,但是你现在实现它的方式,动画将在闪屏逐渐消失后不断重复。

这还没完成。 在启动屏幕消失后,您需要阻止动画重新启动,因为它继续超过该点是没有意义的。 用户无论如何都不会看到它,并且它使用不必要的资源。

要阻止动画显示超出其需要的时间,请向SplashScreen添加一个新的静态变量:

static var shouldAnimate = true

handleAnimations()中,使用if语句包装restartAnimation(),这样一旦新的Booleanfalse,就会阻止它重新开始。 它应该如下所示:

if SplashScreen.shouldAnimate {
  restartAnimation()
}

现在,返回ContentView.swift,取消注释您在开头注释掉的.onAppear闭包,并将shouldAnimate设置为false。 然后,只是为了好玩,还将第二个常量更改为10,这样您就有机会享受您创建的漂亮的闪屏动画。 它现在应该是这样的:

.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
      SplashScreen.shouldAnimate = false
      withAnimation() {
        self.showSplash = false
      }
    }
}

构建并运行您的应用:

你应该看到你的炫酷闪屏显示10秒,然后是应用程序的主地图视图。 关于它的最好的部分是,一旦启动屏幕消失,它就不再在后台动画,因此您的用户可以自由地体验应用程序的所有荣耀,而无需任何后台动画减慢它们的速度。

后记

本篇主要讲述了基于SwiftUI的闪屏页的创建,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容