swiftUI

746 阅读15分钟

前言

苹果不仅引出了新语言 Swift 还引出了新的布局框架 SwiftUI,如果说 Swift 能吸引一些前后端人来开发,那么 SwiftUI 则可以让前端人员过渡开发移动端

至于为什么这么说,将 swiftSwiftUIJavaScriptflex盒式布局 对比一下就知道了,会发现苹果还是很有想法的

另外 SwiftUI 学习的过程也是很简单的,如果有 flex盒式布局 布局经验,那么上手会更加简单

测试demo ---- 可以下载下来跟着文章走

SwiftUI 学习地址

SF Symbos

SF Symbos 是苹果为 SwiftUI 推出的一个 icon网址,在 SwiftUI 中可以直接使用对应的名字,则可以直接使用对应的icon

如下所示,便是此 app 的页面效果了,可以直接复制使用

image.png

SwiftUI 中使用很简单,举个例子

//这就就可以加载出 icon 了
Image(systemName: "pencil.circle")

SwiftUI

SwiftUI 是一个使用 盒式布局 的一个 UI框架 ,之前用过 UIStackView的应该熟悉,其被称为万能布局,Storyboard 或者 UIKit 中的一个框架(当然现在还不能完全代替,毕竟光是一个版本兼容问题就不行)

其中也引出了 state 状态机 等概念,后面会介绍

本篇以实战的方式介绍布局修改状态动画绘制图像,其他的不多介绍(例如:list细节怎么用)

常见的 View 控件

SwiftUI 中常见的控件很多,在 UIKit 中有的不少名字都很像,例如:

Text、Button、TextField、ScrollView、List、Image、NavigationView、TabView、HStack、VStack、Spacer、Divider

Text:就是常见的 UILable,放置文本的地方

Button:按钮,带有点击事件的文本

ScrollView: 滚动视图

List:UITableView表格视图,可复用,可配合 ForEach使用

Image: UIImageView图片视图

TextField: UITextField 输入框

NavigationView: UIViewController 导航控制器视图(SwiftUI中没有刻意的控制器概念,视图即控制器)

TabView: UITabbarController 分页视图

HStack: 横向放置的盒式布局

VStack: 纵向放置的盒式布局

ZStack: 层层向上叠加放置的布局,类似于 UI 默认的摆放方式

Spacer: 用于填充盒式布局剩余空间,多个刻意平分剩余空间

Divider: 分割线

注意ForEach方式可以遍历创建多个View,配合ScrollView可以创建类似List的效果(平时使用的List其实是List + ForEach),且使用起来方便

SwiftUI 布局简介

创建 SwiftUI 项目后,会默认给出下面结构

//这里是我们默认写UI视图的地方,在body中为主视图
//body结果返回一个View(实际使用中间放置多个View也没问题,会被默认放到一个 VStack 中)
struct ContentView: View {
    var body: some View {
        Text("我才18岁!")
    }
}

//这是是快速浏览使用的,会有类似模拟器的一页效果显示(实际显示也不完整,例如:导航)
//不可操作
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

可以点击 comand + 鼠标左键对视图进行操作

其中 Show SwiftUI inspector可以更改视图属性

Embed in HStack 则将当前视图放置到 HStack中

其他可以自行测试

image.png

实战SwiftUI

下面我用 TabViewNavigationViewList,配合 SF Symbols 做一个有分页有导航有list的一个页面

大致效果如下图所示(想看动态的没有),可以下载项目运行

image.png

下面先看首页效果的制作

首页、TabView、NavigationView、状态机

首页包括了 TabView、NavigationView、List,先看上面的 body,其中 TabView 为分页视图,两个 NavigationView 可以理解为两个tab的控制器页面

下面也简单介绍了状态机,其意思很简单,就是保存了视图动态,可以用其更新 UI

struct ContentView: View {
    //@State为状态机,当页面状态需要更新时,可以通过此属性设置的参数来更新View状态
    //注意:不设置@State,刚加载View时也能显示内容,之后,却无法监听参数变更以更新UI
    @State var isActiveLeft = false
    @State var isActiveRight = false
    
    var body: some View {
        TabView {
            NavigationView {
                //id可以设置里面指定item属性,如果是个字符串可以直接\.self,表示对象本身
                //如果集合内对象里面有id属性,可设置成\.id,id用来优化List的性能
                List(Symbols, id:\.self ) {
                    ListRow(symbol: $0)
                }
                .navigationBarTitle("pencil") //导航标题,默认是国外的样式
                .navigationBarItems(leading: leftBarItem, trailing: rightBarItem)
                //.navigationBarHidden(true)
                //.navigationBarTitle("pencil", displayMode: .inline) //这就是国内默认使用样式了
            }.tabItem {
                Image(systemName: "pencil.circle")
                Text("pencil")
            }
            
            NavigationView {
                Folder()
            }.tabItem {
                Image(systemName: "folder.circle")
                Text("folder")
            }
        }
    }
    
    //导航的左侧按钮
    var leftBarItem: some View {
        //toggle为Bool的自动置反功能
        Button(action: {self.isActiveLeft.toggle()}) {
            Image(systemName: "person")
            Text("person")
        }
        //添加一个弹窗,其是父级View中的,而不是上面的Button
        //通过 $isActiveLeft 监听状态,button就可以更改了
        .sheet(isPresented: $isActiveLeft) {
            VStack {
                Text("hello person !")
            }
        }
    }
    导航的右侧按钮
    //d
    var rightBarItem: some View {
        Button(action: {self.isActiveRight.toggle()}) {
            Image(systemName: "power")
            Text("power")
        }
        .sheet(isPresented: $isActiveRight) {
            VStack {
                Text("no power !")
            }
        }
    }
}

tabItem 则是在给视图设置,点击的标签,为下面的位置

image.png

navigationView 设置的则在顶部,如下所示

image.png

navigationView 里面包裹的中间视图,就是我们的 list 本身,就不截图介绍了

sheet 这个 sheet 是 View 内的方法,只要是 View 都可以使用,可以通过状态机触发,显示效果就是 popup窗口,实现效果如下所示

image.png

状态机:

状态机保存、更新UI状态的机器,即通过其可以可以随意更新UI视图

状态机的使用: 用@State 修饰的变量会保存到状态机内,当其修改该属性后,对应的UI视图会重新渲染更新

问题:不用状态机(@State)也渲染出来了,那么是否可以不使用 @State 修饰我的变量

回答:可以,当UI组件刚创建时,会从属性读取内容,并给指定节点赋值,无论在没在状态机都可以显示出来,如果视图已经创建完毕,如果想更新指定组件内容,那么需要将对应属性保存到状态机(设置 @State),那样就可以继续修改更新视图

List、Row、ForEach、NavigationView、NavigationLink

List 中使用了 Symbols 参数,而 Symbols是用SF Symbols中的字符串声明的字符串图标名数组,为String类型

//list的第一个属性,是对象数组,属性用于给 Row 赋值使用
//第二个属性,id可以设置里面指定item属性,如果是个字符串可以直接\.self,表示对象本身
//如果集合内对象里面有id属性,可设置成\.id,id用来优化List的性能
//最后一个是尾随闭包,在里面返回一个 Row(这里面的Row是自定义的View)
//往ListRow里面传递了一个参数, $0为隐藏参数,表示闭包的第一个参数
List(Symbols, id:\.self ) {
    ListRow(symbol: $0)
}
//相当于下面的代码,也就是说,前面的id等相关设置都是ForEach与外部视图组合的
List {
    ForEach(Symbols, id:\.self ) {
        ListRow(symbol: $0)
    }
}

下面就是自定义的 ListRow,其使用了 NavigationLink 内嵌视图,其为 NavigationView中跳转必备,其作用就是点击此 item 会跳转到指定页面,这里的页面是 PencilDetail,且往里面传递了一个参数

注意:这里的item只要点击一定会跳转,如果想控制是否会跳转,后面会介绍

struct ListRow: View {
    var symbol: String
    var body: some View {
        NavigationLink(destination: PencilDetail(symbol: symbol)) {
            HStack {
                Image(systemName: symbol)
                    .frame(width: 40, height: 40)
                Divider()
                Spacer()
                Text(symbol)
                    .foregroundColor(.red)
                    .opacity(0.8)
                Spacer()
            }
        }
    }
}

详情页、NavigationView、盒式布局、ForEach、ObservableObject(视图与model绑定)

下面就是点击 Listrow 跳转的详情页了,在这里,我们介绍一下盒式布局ObservableObject

TabBar问题:此时进入详情页,会发现 tabbar 仍然存在,这是无法避免的,如果想没有 tabbar 只需要将 NavigationView 放到 TabView外层即可

代码如下所示

struct PencilDetail: View {
    @ObservedObject var detailModel = PencilDetailModel()
    
    var symbol: String
    var body: some View {
        ScrollView(.vertical, showsIndicators: true, content: {
            VStack(alignment: .center, spacing: 0.0) {
                HStack {
                    Image(systemName: symbol)
                        .frame(width: 30, height: 30)
                    Divider()
                    Text("Hi!")
                    Spacer()
                }
                
                VStack(alignment: .center, spacing:0.0) {
                    HStack {
                        Text("Hello, world!")
                        Spacer()
                        Text("Hello, world!")
                    }
                    Text("Hello, world!")
                }
                //id可以设置里面指定item属性,如果是个字符串可以直接\.self,表示对象本身
                //如果集合内对象里面有id唯一属性,可设置成\.id,id用来优化List的性能
                Text("通过ForEach将集合生成若干个视图")
                ForEach(detailModel.list, id: \.self) { num in
                    Text(num)
                }
            }
        })
        //设置导航
        .navigationBarTitle("PencilDetail", displayMode: .inline)
    }
}

效果如下所示:

image.png

如上所示,由于是 NavigationView跳转过来的,因此可以直接使用 navigation的属性,直接设置导航相关

ScrollView(.vertical, showsIndicators: true, content: {
    //......
}).navigationBarTitle("PencilDetail", displayMode: .inline)
盒式布局

盒式布局又称 flex布局,在ios中有 UIStackView 也是 盒式布局,且在 react-native中被广泛使用(其View最终映射到原生View上)

盒式布局很简单,通过水平布局 HStack垂直布局 VStack,来进行放置,放置时,就像是一个盒子一样,从左往右,从上往下挨着排列

注意盒式布局一般通过各种嵌套,来完成看似不可能的效果,虽然万能但也不是全能,有些布局还是需要其他布局方式来处理的

参考 PencilDetail部分效果图,来理解 盒式布局

image.png

下面代码是效果图的内容,相信看了会眼前一亮

ScrollView(.vertical, showsIndicators: true, content: {
    //最外部的垂直布局,布局内的盒子,都是从上往下罗列
    VStack(alignment: .center, spacing: 0.0) {
        //第一个视图,为一个水平盒子,里面水平布置了几个视图
        //第一行的 Hi!
        //默认居左放置,Spacer()为填充剩余空间,否则默认局中显示
        HStack {
            Image(systemName: symbol)
                .frame(width: 30, height: 30) //通过设置padding来调整间距
            Divider()  //分割线
            Text("Hi!")
            Spacer()
        }

        //第二行与第三行
        VStack(alignment: .center, spacing:0.0) {
            //第二行
            //默认局中显示,因此,中间方式spacer填充剩余空间
            //则显示效果为两侧对称
            HStack {
                Text("Hello, world!")
                Spacer()
                Text("Hello, world!")
            }
            //第三行,默认局中
            Text("Hello, world!")
        }
    }
})
ForEach生成List

ForEach单出来讲,就是因为他可以代替 List或者是 UITableView,设置的id就是服用的,List的一些类似的构造方法,相信也是将其封装进去的,因此可放心使用

ForEach可以理解为遍历一个数组,将数组内的元素铺到当前视图上,会根据情况进行复用

//id可以设置里面指定item属性,如果是个字符串可以直接\.self,表示对象本身
//如果集合内对象里面有id唯一属性,可设置成\.id,id用来优化List的性能
Text("通过ForEach将集合生成若干个视图")
ForEach(detailModel.list, id: \.self) { num in
    Text(num)
}

效果如下所示( VStack 默认垂直方向罗列元素,元素水平居中)

image.png

ObservableObject

就上面那样,发现还没有昨晚首页想要的结果,中间引出了 PencilDetailModel 类,这个类是给详情页提供数据的,总所周知,详情页很多情况数据是需要网络请求的,因此会异步请求数据,并更新内容

正常思路 :我们可能会在创建数据mode 的时候传入一个闭包,通过闭包回调更新数据,这样也是可以完成

推荐思路:上面思路如果对比一下 ObservableObject 会发现,还是差那么一点意思,只需要对象设置观察,然后那边赋值后,这边会自动更新状态机内容,也就是combine,因此使用很方便

ObservableObject使用如下所示

SwiftUI这边,界面需设置观察,观察内容为整个模型对象

struct PencilDetail: View {
    @ObservedObject var detailModel = PencilDetailModel()
    var body: some View {
        //使用 detialMode.list
        ForEach(detailModel.list, id: \.self) { num in
            Text(num)
        }
    }
}

数据源这边需要遵循 ObservableObject协议,然后向外反馈的参数使用 @Publish属性即可

SwiftUI 使用的是此模型的 list 的属性,因此我们只要在 list 前面加上 @Publish关键字即可,这样当 list更新时,内容会反馈到外面监听的SwiftUI中,可以理解为这一步操作被加入到了状态机中,此时更新了状态机

class PencilDetailModel: ObservableObject {
    @Published var list: [String] = []
    
    init() {
        //直接在这里获取网络数据即可,这里是模拟网络异步请求
        DispatchQueue.global().async {
            sleep(2)
            DispatchQueue.main.async {
                self.list = ["哈哈",
                        "我是网络请求数据",
                        "我是模拟的网络请求数据",
                        "我的参数使用@Published修饰,遵循ObservableObject协议",
                        "一旦发生改变则会更新使用该参数的UI",
                        "UI监听该对象时需要使用@ObservedObject来修饰"]
            }
        }
    }
}

我的页面、控制Navigation跳转

动态控制Navigation跳转

如下所示,设置固定跳转代码,和动态控制跳转的代码

var leftBarItem: some View {
    //直接进入,固定跳转方式,只要点击了里面设置的UI节点就会跳转
    NavigationLink(destination: DrawPathDetail()) {
        Text("绘制路径")
    }
}

//如下所示,NavigationLink通过 isActive参数,来控制入口的进入
//isActive参数通过 State绑定的参数改变可以控制是否跳转,无须按钮点击
//可以将 NavigationLink放在没有节点的地方,直接通过点击事件更新 isActive参数控制跳转即可
var rightBarItem: some View {
    //这里演示一下怎么动态控制进入下一页,通过按钮控制状态进入
    HStack {
        NavigationLink(destination: AnimationDetail(), isActive: $isEnterAnimation){}
        Button {
            self.isEnterAnimation = true
        } label: {
            Text("绘制转场")
        }
    }
}
SwiftUI实战演练

这里在我的页面使用 SwiftUI 进行了一小波实战演练,做了个人信息摘要和订单列表入口 UI

效果如下所示:

image.png

代码如下所示

ScrollView {
    //个人信息
    //大方向水平布局
    HStack(alignment: .top, spacing: nil, content: {
        //左侧按钮,默认显示即可
        Image(systemName: "video")
            .resizable() //能让图片填充满,contentModel木有了
            .frame(width: 60, height: 60, alignment: .center)
            .cornerRadius(4)
            .padding(.leading, 10)
        //中间为垂直布局,默认居中,所以要设置 .leading居左
        VStack(alignment: .leading, spacing: nil, content: {
            //设置连个 spacer则是让中间两个部分垂直方向局中显示
            Spacer()
            Text("昵称:干饭人开动了!")
                .font(.headline)
                .fontWeight(.bold)
            Text("账号:111111")
                .font(.system(size: 12)) //也可以通过这种方式设置字体,一般这么设置
            Spacer()
        })
        //填充左右侧剩余空间
        Spacer()
        //用于跳转的右侧按钮,居上显示
        HStack(alignment: .top, spacing: nil, content: {
            Button(action: {
                print("可以准备跳转设置界面")
            }, label: {
                Image(systemName: "sunset")
                    .resizable()
                    .frame(width: 30, height: 30, alignment: .center)
            })
        })
    })

    //订单入口
    //分为外则一个背景,内侧上下两部分,一个标题全部,一个订单类型
    //外部框
    VStack {
        //顶部一行,订单和全部,左右靠边,中间填充即可
        HStack {
            Text("全部订单")
                .font(.headline)
            Spacer()
            NavigationLink(destination: FoldDetail(type: 0), label: {
                Text("全部")
                    .font(.subheadline)
                    .foregroundColor(.gray)
            })
        }.padding([.leading, .trailing], 10)

        //下面的下面这么写,实际可以用前面讲的一个ForEach解决,下面这么写,村纯粹一开始的懒人写法
        //注意Spacer的使用,用于平分剩余空间
        HStack {
            NavigationLink(destination: FoldDetail(type: 1), label: {
                VStack {
                    Image(systemName: "cloud.sun")
                    Text("待付款")
                        .font(.subheadline)
                        .foregroundColor(.gray)
                        .padding(.top, 1)
                }
            })
            Spacer()
            NavigationLink(destination: FoldDetail(type: 2), label: {
                VStack {
                    Image(systemName: "cloud.sun")
                    Text("待发货")
                        .font(.subheadline)
                        .foregroundColor(.gray)
                        .padding(.top, 1)
                }
            })
            Spacer()
            NavigationLink(destination: FoldDetail(type: 3), label: {
                VStack {
                    Image(systemName: "cloud.sun")
                    Text("待收货")
                        .font(.subheadline)
                        .foregroundColor(.gray)
                        .padding(.top, 1)
                }
            })
            Spacer()

            NavigationLink(destination: FoldDetail(type: 4), label: {
                VStack {
                    Image(systemName: "cloud.sun")
                    Text("待评价")
                        .font(.subheadline)
                        .foregroundColor(.gray)
                        .padding(.top, 1)
                }
            })
            Spacer()
            NavigationLink(destination: FoldDetail(type: 5), label: {
                VStack {
                    Image(systemName: "cloud.sun")
                    Text("退款/售后")
                        .font(.subheadline)
                        .foregroundColor(.gray)
                        .padding(.top, 1)
                }
            })
        }.padding([.leading, .trailing], 10)
        .padding(.top, 1)
    }.padding(.top, 20)
}

绘制路径

绘制路径是使用的 Path,绘制方式与各个端很相似,也是那么几步

移动画笔到某个点、从当前点到另外一个点(直线或者曲线)、绘制线条、填充内部颜色

绘制线条是可以直接使用 UIColor 颜色,也可以使用 LinearGradient 绘制渐变色

三角形
//每个path的绘制的坐标相对于自己的View容器
//绘制图形框
Path { path in
    //移动到某个点
    path.move(to: CGPoint(x: 100, y: 10))
    //连线到第二个点
    path.addLine(to: CGPoint(x: 40, y: 110))
    //连线到第三个点
    path.addLine(to: CGPoint(x: 160, y: 110))
    //连线封闭路径(首位线段相连)
    path.closeSubpath() //用线段连接起点和终点,形成闭合图形
}.stroke(LinearGradient(gradient: Gradient(colors: [.red, .green]), 
    startPoint: UnitPoint(x: 0.5, y: 0), endPoint: UnitPoint(x: 0.5, y: 1)))
//stroke绘制线段

//绘制填充图形
Path { path in
    path.move(to: CGPoint(x: 100, y: 10))
    path.addLine(to: CGPoint(x: 40, y: 110))
    path.addLine(to: CGPoint(x: 160, y: 110))
    path.closeSubpath()
}.fill(LinearGradient(gradient: Gradient(colors: [.red, .green]), 
    startPoint: UnitPoint(x: 0.5, y: 0), endPoint: UnitPoint(x: 0.5, y: 1)))
//fill填充绘制封闭图形,还可以设置颜色

三角形效果如下所示,分别是绘制线条和填充图形

image.png

圆形

如下所示,圆形的绘制更简单了,有专门的方法可以绘制,也可以点进去看看,可以绘制扇形

//绘制圆形框
Path { path in
    path.addRoundedRect(in: CGRect(x: 0, y: 0, width: 120, height: 120), 
        cornerSize: CGSize(width: 60, height: 60))
}
.stroke(Color.blue) //线条绘制可以设置线条颜色

//绘制圆形
Path { path in
    path.addRoundedRect(in: CGRect(x: 0, y: 0, width: 120, height: 120), 
        cornerSize: CGSize(width: 60, height: 60))
}.fill(Color.blue)

image.png

自定义图形

自定义图形与普通图形其实也差不多,只不过绘制的过程中根据自己的需要,可以绘制支线,曲线,斜线等不规则图形,绘制线段还好,绘制曲线使用的是贝塞尔曲线,需要使用控制点来控制曲线,分为一个控制点和两个控制点,使用很简单,想深刻理解,可以百度查查

Path { path in
    path.move(to: CGPoint(x: 20, y: 40))
    path.addLine(to: CGPoint(x: 20, y: 100))
    //贝塞尔曲线,一个控制点
    path.addQuadCurve(to: CGPoint(x: 100, y: 100), control: CGPoint(x: 60, y: 100))
    path.addLine(to: CGPoint(x: 100, y: 40))
    //连个控制点的贝塞尔曲线
    path.addCurve(to: CGPoint(x: 20, y: 40), control1: CGPoint(x: 60, y: 0), 
        control2: CGPoint(x: 60, y: 80))
}.stroke(Color.red, lineWidth: 3)

image.png

跟随容器大小变化的图形

需要使用到 GeometryReader,通过其,可以获取到当前容器的宽和高的信息Path绘制的起点又是相对于当前容器的,因此可以随意控制Path的绘制了

如下所示,定义容器 View 大小,并且是使用 GeometryReader 来自定义 Path

//定义外部视图大小
VStack {
    flexView()
        .frame(width: 20, height: 20)
    flexView()
        .frame(width: 40, height: 40)
    flexView()
        .frame(width: 100, height: 100)
    Text("跟随容器大小缩放的图形")
}

//通过geometry Reader可以获取当前视图容器的信息
GeometryReader { geometry in
    Path { path in
        //获取宽高来控制图形大小
        let width = geometry.size.width
        let height = geometry.size.height

        path.move(to: CGPoint(x: width/2, y: 0))
        path.addLine(to: CGPoint(x: 0, y: height/2))
        path.addLine(to: CGPoint(x: width/2, y: height))
        path.addLine(to: CGPoint(x: width, y: height/2))
        path.closeSubpath()
    }.stroke(Color.yellow)
}

image.png

动画与转场

在开发的过程中,为了用户体验,一定会用到一些动画效果,下面就介绍下常见的动画

:想看效果,可以下载下来代码运行一下

View动画

常见的动画有平移、旋转、缩放、透明等,正常不设置动画操作,就是突然变化完毕,很突兀,因此,可以使用 Viewanimation 属性,来开启动画效果

设置了animation属性后,我们通过改变View的显示效果(旋转、透明、大小等), 则会自动产生动画

animation属性有一些枚举,分别是设置动画过渡曲线的,有慢进满出、线性变化、弹性过渡等,可以自己尝试一下

如下所示,分别设置旋转、透明、缩放效果

Button {
    isRoate.toggle()
} label: {
    Text("旋转")
}.padding(.top, 10)
.rotationEffect(.degrees(isRoate ? 90 : 0))
.animation(.easeInOut)//慢入慢出过渡

Button("透明") {
    isOpacity.toggle()
}.opacity(isOpacity ? 0.1 : 1)
.animation(.linear) //线性过渡

Button("大小") {
    isScale.toggle()
}.scaleEffect(isScale ? 3 : 1)
.animation(.spring()) //弹性过渡
转场动画

转场动画也是动画的一种,一般使用形容一个新的场景出现或者消失的过渡动画,例如:push到另一个页面,控制一个弹窗出现

SwiftUI 控制一个 View显隐,这里通过 if + 状态属性来控制一些效果的显示

如下所示,当 isShowTransition 状态参数设置为 true 的时候,就会显示里面的视图,为false释放里面的视图

我们通过 WithAnimation设置 isShowTransition状态属性为 true,则会自动开启一个过渡动画,当然只能设置动画的过渡曲线、时间,而过渡的转场效果取决与 新场景自身的动画设置,这里默认动画效果是 Opacity透明度变化或者是溶解效果

如下所示,是基本的转场动画效果

 Button("切换转场动画") {
    //可以设置动画时长和过渡曲线来开启动画
    withAnimation(.easeInOut(duration: 2)) {
        isShowTransition.toggle() //通过其控制动画
    }
}.padding(.top, 10)

//新场景
if (isShowTransition) {
    //由于flex布局问题,通过动画切换的会发现整体动画很好玩
    //虽然别人可以控制动画过渡时间和变换曲线,变化效果取决子自身
    //例如:缩放、显隐、平移或者混合等
    //不设置默认效果似乎是溶解,或者opacity过渡
    HStack {
        Text("左侧0")
        Spacer()
        Text("中间0")
        Spacer()
        Text("右侧0")
    }
    .padding(.top, 10)
}

下面介绍下其他的动画效果,为了对比代代买,我们把之前的动画代码也加进去了,方便对比

分别是默认 溶解(Opacity过渡)、slide(左侧移入右侧移出)、move(一侧移入,同侧移出)、asymmetric(同时设置移入和移出效果)

Button("切换转场动画") {
    //可以设置动画时长和过渡曲线来开启动画
    withAnimation(.easeInOut(duration: 2)) {
        isShowTransition.toggle() //通过其控制动画
    }
}.padding(.top, 10)

//新场景
if (isShowTransition) {
    //默认动画
    //不设置默认效果似乎是溶解,或者opacity过渡
    HStack {
        Text("左侧0")
        Spacer()
        Text("中间0")
        Spacer()
        Text("右侧0")
    }
    .padding(.top, 10)

    //下面是AnyTransition.slide左侧移入,右侧出
    HStack {
        Text("左侧1")
        Spacer()
        Text("中间1")
        Spacer()
        Text("右侧1")
    }
    .padding(.top, 10)
    .transition(.slide)

    //右侧移入右侧,进出只能有用一个效果
    HStack {
        Text("左侧2")
        Spacer()
        Text("中间2")
        Spacer()
        Text("右侧2")
    }
    .padding(.top, 10)
    .transition(.move(edge: .trailing))


    //右侧移入右侧出,asymmetric来定义两个,但似乎写出来效果不是很好,也不通用
    HStack {
        Text("左侧3")
        Spacer()
        Text("中间3")
        Spacer()
        Text("右侧3")
    }
    .padding(.top, 10)
    .transition(
        .asymmetric(insertion: AnyTransition.move(edge: .trailing),
                    removal: AnyTransition.move(edge: .leading)))
}

看了上面的代码,可以看到同时定义不同的移入和移出效果,写法上有点臃肿,我们通过扩展的方式,给 AnyTransition添加一个新的效果

使用方式如下所示,下面添加了不止一个动画效果,即一个View多个效果,可以理解为动画组

//通过扩展自定义转场动画,asymmetric定义,转入和转出动画
//右侧渐变划入,从大变小从显示变成隐藏
extension AnyTransition {
    //想用类名调用得用 static修饰
    static var customTransition: AnyTransition {
        //组合动画改成右侧移入,左侧移出
        let inAnimation = AnyTransition.move(edge: .trailing).combined(with: .opacity)
        let outAnimation = AnyTransition.scale.combined(with: .opacity)
        return .asymmetric(insertion: inAnimation, removal: outAnimation)
    }
}

//新场景
if (isShowTransition) {
    //左侧进入右侧出去,通过扩展来解决通用和使用效果问题
    HStack {
        Text("左侧4")
        Spacer()
        Text("中间4")
        Spacer()
        Text("右侧4")
    }
    .padding(.top, 10)
    .transition(.customTransition) //右侧移入右侧出
}
组合动画(多个View)

这里讲的组合动画,是使用多个不同的View,设置不同的动画效果,从而形成一个整体效果

同一个View 同时拥有几个动画效果(例如:透明、平移),只要开启了animation,设置好其他参数,会自动形成一个动画组,上面转场动画案例里面有相关代码

如下所示,通过 ForEach 创建多个View,并且不同View设置了不同的动画,这里只是创建时加了一个延迟,还给每个动画设置了不同的过渡效果,这就是一个组合动画

ps: 注意下面是创建时,如果直接设置效果,则会直接显示最终效果,如果还需要自己的额外效果,需要在创建完毕后,在执行其他动画,下面的延迟动画,是借助了外面的HStack的动画产生的效果

if (isShowCustom) {
    //组合多个动画效果,创建十个view,分别给动画设置延迟,这样动画过渡的时候会有一个缓慢移入效果
    //侧边慢慢移入
    HStack {
        ForEach(1...8, id: \.self) { (num) in
            Text("\(num)")
                .animation(.easeOut.delay(Double(num) * 0.5))
        }
    }.padding(.top, 10)
    .transition(.slide)


    //sping弹性效果移入
    HStack {
        ForEach(1...8, id: \.self) { (num) in
            Text("\(num)")
                .animation(.spring(dampingFraction: 0.5).delay(Double(num) * 0.5))
        }
    }.padding(.top, 10)
    .transition(.slide)
}

注意: 动画的具体效果可以参考代码呀

SwiftUI 与 UIKit

在使用SwiftUI的过程中,UIKit 框架的 View们,仍能混合使用,可以将 UIKit 框架下的自定义View写好,在 SwiftUI 中,只需要像调用自己控件一样使用即可,完全是无缝衔接,因此无需担心

最后

到这里,相信已经开始入门了,里面有些 UI 比较生疏,效果不满意,可以点进去,或者百度搜索、参考官网都行

快来试一试吧!