SwiftUI - 常用Views / Modifies(三)

前言

今天没有前言,直接进入主题吧...😁

1、Form

Form表单,新建一个表单,发现和List并无太大差别,只是界面背景置灰了,显示内容会自动有个margin

Form{
       Section(header: Text("第一组"), footer: Text("Footer")) {
              Text("form")
              Text("form")
              Text("form")
       }
       Section() {
                Text("form")
                Text("form")
                Text("form")
       }
}

样式看起来和List差不多。但Form真正比较实用的地方类似于设置功能界面。

@State var pick = 0
@State var toggle1 = false
@State var toggle2 = true

NavigationView {
            Form {
                Section(header: Text("自动下载"), footer: Text("自动下载在其他的设备上新购买的项目(含免费项目)。")) {
                    Toggle("App", isOn: $toggle1)
                    Toggle("App更新", isOn: $toggle1)
                }
                Section(header: Text("蜂窝数据"), footer: Text("仅允许 200 MB以下的 App 使用蜂窝数据自动下载。")) {
                    Toggle("自动下载", isOn: $toggle1)
                    Picker("App 下载", selection: $pick) {
                        Text("始终允许").tag(0)
                        Text("超过 200 MB时询问").tag(1)
                        Text("始终询问").tag(2)
                    }
                }
                Section(footer: Text("在App Store中自动播放 App 预览视频。")) {
                    Picker("视频自动播放", selection: $pick) {
                        Text("开").tag(0)
                        Text("仅限 Wi-Fi").tag(1)
                        Text("关").tag(2)
                    }
                }
                Section(footer: Text("允许 App 询问产品使用反馈,以帮助开发者和其他用户了解您的想法。")) {
                    Toggle("App 内评分及评论", isOn: $toggle2)
                }
                Section(header: Text("沙盒账户"), footer: Text("此账户将仅用于本地开发时,测试 App 内购买项目。您当前的 App Store 账户将用于 TestFlight App。")) {
                    Button("Apple ID:LogicEducation@qq.com") {}
                }
                Section(header: Text("隐私")) {
                    Button("App Store 与 Arcade 隐私") {}
                    Button("个性化推荐") {}
                }
            }.navigationTitle("App Store").navigationBarTitleDisplayMode(.inline)
        }

50行代码就能实现设置内功能界面,代码量少,但功能一个不落。


Form

查看Form的视图层级,发现它内部也是一个UITableViewWrapperView,也是对UITableView的包装,难怪和List那么相似。你可以试着将Form改成List,就会发现毫无变化。

2、ScrollView

ScrollView在使用上和List没什么差别,而且在底层cell的复用机制上也和UITableView一样。

struct customView: View {
    var text: String
    var body: some View {
        Text(text)
    }
    init(_ text: String) {
        print("create", text)
        self.text = text
    }
}
ScrollView{
     VStack {
          ForEach(0..<100){
              customView("Cell \($0)").font(.title)
          }
    }
 }

如上代码,运行后打印了100条,说明没有用到复用机制,当我们去掉当前容器VStack层后只打印18条,说明是VStack强制让子视图一次性加载。
ScrollViewReader SwiftUI提供了这么一个代理来帮助我们实现比如点击按钮滑动到列表底部这样的功能:

ScrollViewReader { proxy in
            Button("gotoBottom"){
                // 99 底部cell的ID
                proxy.scrollTo(99)
            }
            ScrollView{
                VStack {
                    //id 添加维一标识
                    ForEach(0..<100){
                        customView("Cell \($0)").font(.title).id($0)
                    }
                }
            }
        }
3、Grid

表格,在使用类似于之前的UICollectionView的效果。

LazyVGrid(columns: [GridItem(),GridItem()],spacing: 10){
            ForEach(0..<100,id: \.self) {
                Text("\($0)").frame(height: 100).frame(maxWidth: .infinity).background(.orange)
            }.padding(10).font(.title2)
        }

即可实现两列列表,但是此时不能上下滑动,我们需要将Grid放入ScrollView中才能滑动,这两者在使用上基本形影不离。
如果是宽度不规则的瀑布流样式,可以改变宽度范围来控制列数:

ScrollView{
        LazyVGrid(columns: [GridItem(.adaptive(minimum: 150,maximum: 200))], alignment: .leading,spacing: 10){
             ForEach(0..<100,id: \.self) {
                   Text("\($0)").frame(height: 100).frame(width:CGFloat(arc4random() % 10)*10 + 100).background(.orange)
             }.padding(10).font(.title2)
      } 
 }
Grid使用
4、VideoPlayer

视频播放器需要引入AVKit框架,可直接使用VideoPlayer结构体构造简单播放器。

import AVKit

let url = "https://video.yinyuetai.com/e65a400cf89941439cf1175f69737800.mp4"
    var body: some View {
        VStack{
            VideoPlayer(player: AVPlayer(url: URL.init(string: url)!),videoOverlay: {
                Text("play").font(.largeTitle).bold().foregroundColor(.white)
            }).ignoresSafeArea().frame(width: .infinity,height: 260)
            Spacer()
        }
    }

设置一定的尺寸就能满足简单的播放器需求。如果想实现另外的一些复杂功能,用法基本上和UIKit里是一样的。

5、Alert / AlertController

Alert提示弹框,样式还是和之前一样的,写法有所不同。

@State var show = false

Button ("show Alert") {
            show.toggle()
        }.alert(isPresented: $show) {
            Alert.init(title: Text("title"), message: Text("message"), primaryButton: .default(Text("cancle"),action: {
                print("cancle")
            }), secondaryButton: .destructive(Text("sure"),action: {
                print("sure")
            }))
        }

即可实现两个按钮的提示框,并监听按钮点击事件用以处理下一步操作。
当需要添加多个按钮,类似于UIAlertController的alert样式的弹出框,就需要像下面这样添加:

Button ("show Alert") {
            show.toggle()
        }.alert("title" ,isPresented: $show) {
            Button("btn1"){}
            Button("btn2"){}
            Button("btn3"){}
        }

此种写法,当不给添加按钮时候,默认样式是一个带ok按钮的提示弹窗。
那如果想实现底部弹出类似Sheet的样式,就需要用到.confirmationDialogActionSheet已经废弃不用。

Button ("show Alert") {
            show.toggle()
        }.confirmationDialog("sheet", isPresented: $show) {
            Button("btn1"){}
            Button("btn2"){}
            Button("btn3"){}
        }

底部弹出会自带一个cancle按钮。

6、显式动画
Button("animation"){
            withAnimation {
                scaleAmount += 44
            }
        }.font(.title).padding().background(.orange)
            .cornerRadius(20)
            .rotation3DEffect(.degrees(scaleAmount), axis: (x: 1, y: 0, z: 0))

创建一个按钮,在3D视角上,点击时就沿x轴旋转一定的角度。

Button("animation"){
            withAnimation(.easeInOut) {
                scaleAmount.toggle()
            }
        }
        .font(.title).padding().background(scaleAmount ? .white : .orange)
        //背景变换
        .animation(.easeInOut.repeatForever(autoreverses:true), value: scaleAmount)
        .cornerRadius(20)
        //缩放
        .scaleEffect(scaleAmount ? 1.1 : 1)
        .animation(.easeInOut.repeatForever(), value: scaleAmount)
        //边框发散
        .overlay{
                RoundedRectangle(cornerRadius: 20)
                .stroke(.red)
                .scaleEffect(scaleAmount ? 2 : 1)
                .opacity(scaleAmount ? 0 : 1)
                .animation(.easeInOut(duration: 1).repeatForever(autoreverses: false), value: scaleAmount)
            }

复杂动画,就是后面的动画不会影响前面的动画。
下面再写个关于改变位置的动画效果:

@State var isVstack:Bool = false
@Namespace var nameSpace

Group {
            if isVstack{
                VStack{
                    Image(uiImage: UIImage.init(named: "2222")!).resizable().frame(width: 150, height: 150).matchedGeometryEffect(id: "image", in: nameSpace)
                    Text("Hello Lcr").font(.largeTitle).matchedGeometryEffect(id: "text", in: nameSpace)
                }
            }else{
                HStack{
                    Text("Hello Lcr").font(.largeTitle).matchedGeometryEffect(id: "text", in: nameSpace)
                    Image(uiImage: UIImage.init(named: "2222")!).resizable().frame(width: 150, height: 150).matchedGeometryEffect(id: "image", in: nameSpace)
                }
            }
        }.onTapGesture {
            withAnimation {
                isVstack.toggle()
            }
        }

如果不加上.matchedGeometryEffect,效果就只是更改位置而没有动画效果。

动画效果图

7、手势

一些基本的手势如点击(.onTapGesture)长按(.onLongPressGesture)等使用比较简单,大家可以自己试试,另外的旋转缩放拖动稍微复杂点,需要搭配.gesture()来使用,下面写个例子来看看缩放效果:

@State var currentAmount: CGFloat = 0
@State var finalAmount: CGFloat = 1

Text("Hello Lcr").bold().font(.title).padding().background(.green).scaleEffect(currentAmount + finalAmount)
            .gesture(
                MagnificationGesture()
                    .onChanged({ amount in
                        print(amount)
                        currentAmount = amount - 1
                    }).onEnded({ amount in
                        finalAmount += currentAmount
                        currentAmount = 0
                    })
            
            )

SwiftUI里点击事件的响应也是和UIKit中一样的,子视图比父视图响应的优先级更高,我们可以利用.highPriorityGesture来将响应者更改,可以理解为高优先级的手势:

.highPriorityGesture(
            TapGesture().onEnded({ _ in
                print("VStack tap")
            })
        )

这样就能使响应优先级反转。
需求一:父子视图同时响应点击事件,那么可将highPriorityGesture替换成simultaneousGesture即可。
需求二:长按3秒之后可拖动,我们就需要借助于.sequenced序列来对两种手势进行组合:

let combined = pressGesture.sequenced{before: dragGesture}

SwiftUI手势使用中,我们需要利用.onChanged.onEnded等来获取手势的状态来实现一些功能上的操作。以及利用withAnimation{}来结合手势去完成一些动画效果。

最后我们来实现一个比较有意思的例子:

let words = Array("Lcr is a gentleman")
    @State var enabled = false
    @State var dragAmount = CGSize.zero
    var body: some View {
        HStack(spacing: 0) {
            ForEach(0..<words.count) {num in
                Text(String(words[num])).bold().font(.title).foregroundColor(.white)
                    .padding(2)
                    .background(enabled ? .orange : .red)
                    .offset(dragAmount)
                    .animation(Animation.default.delay(Double(num) / 20), value: dragAmount)
            }
        }.gesture(
            DragGesture()
                .onChanged {
                    dragAmount = $0.translation
                }
                .onEnded {_ in
                    dragAmount = .zero
                    enabled.toggle()
                }
        )
    }
手势动画
8、ToolBar

给界面顶部导航条添加按钮,之前用的是.navigationBarItems(),现在推荐使用.toolbar{}:

NavigationView {
            Text("Hello Lcr").font(.title)
                .navigationTitle("navi")
                //方式一 已废弃
                //.navigationBarItems(trailing: EditButton())
                //方式二
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button("Edit"){}
                    }
                }
        }

.toolbar方式可以很轻松自定义一些自己想要的按钮效果,文字图片都行。然后可以切换placement不同样式,查看按钮位置的不一样,以及多个ToolbarItem按钮之间的优先级(布局上的)。

9、fullScreenCover

.sheet()展示效果类似于representedViewController,不能铺满全屏,如果需要铺满,可以使用.fullScreenCover()即可:

@State var show = false

VStack {
            Text("Hello Lcr").fullScreenCover(isPresented: $show) {
                Text("Detail")
            }
            Button("show"){
                show.toggle()
            }
        }

也可用sheet替代fullScreenCover看看不一样的效果。

我们通过三篇文章简单介绍SwiftUI中一些常用的Views和Modifies,暂时也有个印象,项目中用一用,敲一敲就更熟悉了。

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

推荐阅读更多精彩内容