Swift UI
Update 2023/2/20
SwiftUI Animations iOS 16 - Animate Anything with SwiftUI (oreilly.com)
[Stephen DeStefano](https://learning.oreilly.com/search/?query=author%3A"Stephen DeStefano"&sort=relevance&highlight=true)
Dev Preparation
Hot reload and preview
在代码下面加上这一段,就会在Xcode右侧显示出预览内容,而且有Hot Reload的功能。
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Xcode code indent
选中代码,然后Control+I 就能对齐空格
Declarative Programming vs Imperative Programming
命令式和声明式,并不是绝对的概念。
命令式(Imperative): How the program should achieve the result
例如用Python在数组中查找元素
for xxx in xx:
...
声明式(Declarative): What the program should do
而相对而言SQL语句
SELECT * FROM xx where xxx
就是声明式。
而SwiftUI自称是“声明式”的UI框架。
Stacks
VStack
让组件纵向排布
HStack
让组件横向排布
ZStack
组件包裹嵌套
每个Stack都会返回一个View,并且不能为空,必须包含内容。
Spacer()
会尽可能地扩张,扩张的逻辑有些类似于Qt中的expanding。如果在一个View中有两个相邻的Spacer,会平分空间。
Text
kerning
字母之间的距离
foregroundcolor
文字的颜色
lineLimit
限制文字的行数(超出的用省略号表示)
truncationMode
和lineLimit一起用,省略的内容在头部还是尾部还是中间
overlay
覆盖其他的Text/View
border
边框,可以设置线的颜色、宽度等
onTapGesture
点击后触发的事件
State and Modifiers
我们可以在body view里加上:
@State private var stateString: String = ""
然后用一个TextField(输入框)来更新状态:
TextField("Place holder text", text: $stateString)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(.init(top: 40, leading: 20, bottom: 100, trailing: 20))
显示的时候,可以改变任意组件中的值:
Text("Log the \(stateString) done today")
SecureField
用来输入密码的输入框
Image
resizable
让图片能够变换大小
Image(systemName)
可以用system自带的icon,如faceid,wifi等。
改变大小:用.font(.system(size: 50))
.blur(radius: )
模糊图片
ForEach(0..<N)
地砖铺设
List{
ForEach(0..<10) { _ in
HStack {
ForEach(0..<5) { _ in
Image(systemName: "faceid")
.font(.largeTitle)
.padding()
}
}
}
}
异步请求
这里就有些意思了。通常来说,对很多UI,我们都会做这样的处理:一边在后台下载某个图片,先展示一个静态的本地图片。等下载完成,替换本地图片。
如果这个流程是在Qt中进行,那么需要一个信号Emitter和一个槽函数Slot,来触发下载完成后的界面重新绘制。
如果是在JS/React中进行,那么只需要修改state,就可以触发重新绘制。
那么如果是在SwiftUI中呢?
其实,SwiftUI和React也非常类似,只要@State的变量发生了变化,那么View就会触发重绘。
我们来试试看:
struct ContentView: View {
@State private var img: UIImage? = nil
let staticImage = UIImage(systemName: "wifi")
@State private var stateString: String = ""
var body: some View {
VStack {
Image(uiImage: (self.img ?? staticImage!))
.resizable()
.onAppear(perform: imageDownloader)
.frame(width: CGFloat(400), height: CGFloat(400), alignment: .center)
.onTapGesture {
print("Image was tapped")
}
...
如果img为Nil,那么就使用staticImage(这里加了个!的原因是,UIImage返回的是UIImage?,有点类似Rust中的Optional的味道。因此需要加个!来强制unwrap)
这里再看看imageDownloader函数:
func imageDownloader() {
guard let imgUrl = URL(string: "https://picsum.photos/400/400?i=30") else {
print("Could not find images at this URL")
return
}
URLSession.shared.dataTask(with: imgUrl) {
(data, response, error) in
if let imageData = data,
let imageToDisplay = UIImage(data: imageData) {
self.img = imageToDisplay
} else {
print("error\(String(describing: error))")
}
}.resume()
}
这里的guard let又是什么呢?
这里就不得不提一下编程中的一个概念:Pyramids of doom
if firstName != "" {
if lastName != "" {
if address != "" {
// do great code
}
}
}
很显然,上面的代码是非常不直观的。可以改成下面的样式:
if firstName == "" { return }
if lastName == "" { return }
if address == "" { return }
// do great code
而SwiftUI中的guard let可以更加清晰地表现出这种提前return的逻辑
guard name.characters.count > 0 else {
throw InputError.NameIsEmpty
}
guard age > 18 else {
return false
}
guard #available(iOS 9, *) else {
return
}
func printName() {
guard let unwrappedName = name else {
print("You need to provide a name.")
return
}
print(unwrappedName)
}
回到我们刚才的例子,这里的guard let其实并没有什么效果(因为我的代码是参考Stephen大佬的课程的)但是他这里写的并不对,因为这里只是URL的构造函数,我们可以点进去看:
Returns `nil` if a `URL` cannot be formed with the string (for example, if the string contains characters that are illegal in a URL, or is an empty string).
只是在url包含非法字符时,才会触发这个错误然后early return。而"Cannot find image"却并不会真的走这个分支。
这里的sharedSession也是一个默认的单例,用于处理基础的请求,并不能做复杂的配置。
Projects
Animating Circles

要做出图中这个效果,我们可以注意观察: