SwiftUI一日速成

Posted 颐和园

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SwiftUI一日速成相关的知识,希望对你有一定的参考价值。

Embed 功能

SwiftUI 可以将一个或多个 View 外部再包裹一层 View 。在某个 SwiftUI View 上右键,Show Code Actions (快捷键 command+shift+左键),选择 Embed in HStack/VStack/ZStack/Group/Button/List(Xcode 13)。

VStack

背景色

修改背景色用 background 修饰符:

VStack 
 	...

.background(Color(red: 0xf2/255.0, green: 0xf2/255.0, blue: 0xf2/255.0))

Rectangle

或者用一个嵌入一个 Rectangle:

VStack 
    Rectangle()
        .fill(Color.green)
        .frame(width: 200, height: 200)
        .padding()
        .border(Color.red)

圆角

此外,记得圆角一定要放在背景色之后设置,否则无效

        .background(Color(red: 0xf2/255.0, green: 0xf2/255.0, blue: 0xf2/255.0))
        .cornerRadius(10)

描绘边框

通过 overlay 修饰符,可以在 View 上描绘(覆盖)一层几何图形,比如圆角矩形(RoundedRectangle)、矩形(Rectangle)、圆形(Circle)等。

Image(...)
  .overlay(RoundedRectangle(cornerSize: CGSize(width: 20,height: 20)).stroke(Color.gray,lineWidth: 4))

填充安全区

如果要让举行填充整个屏幕空间(包括安全区),可以:

Rectangle()
  .fill(Color.black.opacity(0.6))
  .edgesIgnoringSafeArea(.all)

分割线

Divider()
  .background(Color.purple) // 默认颜色为灰色
  .scaleEffect(CGSize(width: 1, height: 10)) // 高度放大10倍
  .padding(Edge.Set.init(arrayLiteral: .top, .bottom), 20) // 上下内边距设置20,

不能超过 10 个子 View 的限制

其实所有的容器(Container)都有这个限制,包括:

  • VStack
  • HStack
  • ZStack
  • Group
  • List

解决的办法就是在容器中嵌套容器,当然每一层仍然不能超过10个,比如在 VStack 中嵌套 10 个 Group,然后在每个 Group 中又嵌套 10 个 View。

布局

padding

SwiftUI 的布局基于 padding。左留白:

.padding(.leading, 10)

两边留白:

.padding(.horizontal) // 或者 .padding([.leading, .bottom]),或者 .padding(.vertical,11)

四边留白:

.padding(10.0) // 或者 .padding(),默认好像是 8?

offset 和 padding

设置图片的坐标向上偏移:

Image(...).offset(x: 0, y: -130).padding(.bottom, -130)

offset 偏移的是图片的内容,并不会移动图片的 frame。这会导致虽然图片显示内容上移后,下方留下 130 像素的空白区域。要消除这个区域,我们使用了一个 padding 修饰符,让下边距扣减 130。

所以在实际开发中,offset 和 padding 需要组合使用。

Image

常用 Modifier:

  • cornerRadius(3.0) 圆角
  • resizable() 允许修改原图大小,这个修饰符必须放在其它修改 frame(包括 frame 和纵横比)之前,否则其它修饰符无效
  • apsectRatio(contentMode: ) 修改 content mode。scaleToFit/scaleToFill 是这个方法的便利方法
  • frame(minWitdh: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) 宽高自适应 0~∞,等同于占满全屏
  • frame(width:40, height: 40) 设置固定大小
  • clipped() 修改 frame 之后还必须调用这个修饰符,否则图片的内容又可能超出 frame
  • tapAction self.zoomed.toggle() 触摸事件处理
  • clipShape(Circle()) 裁剪成圆形
  • overlay(Circle().stroke(Color.white,lineWidth: 4)) 覆盖(描绘)白边
  • shadow(radius: 10) 阴影
  • listRowInsets() 如果 Image 位于 List 中,Image 四边会自动带有 padding,如果需要去掉这个 padding,需要应用 listRowInsets(EdgeInsets())

字体图标

Image 类可以使用系统内置的系统图标,这些系统图标可以通过一个 SF Symbols 的 App (MacOS) 来查找。由于是字体图标,你可以修改字体的大小和颜色,比如:

Image(systemName: "person.crop.circle").imageScale(.large).color(.gray)

NavigationView 和 navigationBarTitle

相当于 UIKit 中的 Navigation View Controller。用于将某个 View 包裹在一个 Navigation View Controller并提供了 Navigation Bar。

NavigationView() 
	List() ... .navigationBarTitle(Text("一个 Title"), displayMode: .inline)

上面的代码在 List 的外面套了一个 NavigationViewController,同时定义了一个导航栏,使用传统样式的标题(文字居中)。

**注意,navigation bar 并不是在 NavigationView 上进行设置,而是在它所包裹的 View 上设置。**这类似于在 ViewController 上设置 navigation Bar 而不是在 NavigationViewController 上设置。从这里也可以看出,SwiftUI View 就相当于 UIKit 的 UIView。

在 iPad 上的 NavigationView

这是 NavigationView 的一个坑,默认情况下 NavigationView 在 iPad 上是以 split view 的方式显示的,因此它和 iPhone 上看起来不一样!

要改变这点,需要手动设置 navigationViewStyle :

NavigationView 
  ...
.navigationViewStyle(StackNavigationViewStyle())

NavigationLink 导航组件

跳转按钮,允许跳转到另外一个页面:

NavigationLink(destination:Text("下一页")) 
	Image(...)

NavigationLink() 的第一个参数 destination 是一个SwiftUI View,指向要跳转到的那个页面,第二个参数 label 是一个 block,这个 block 返回的也是一个SwiftUI View,用来定义按钮的外观,可以是一个 Text ,也可以是一个 Image。

NavigationLink 的另一种用途是使用它来作为一种导航,就像 UIKit 中的 Segue,它不一定有 UI(你看不见它),但是你可以通过它,让其它 UI 控件也能实现页面的导航。

首先你需要一个 @State 属性,来控制某个页面的显示与隐藏:

 @State var pushActive = false

然后你需要一个“不可见”的 NavigationLink 来充当这个导航 Segue:

NavigationLink(
 	destination: MechanicalCheckView(viewModel: MechanicalCheckViewModel()),
 		isActive: self.$pushActive
 	) 
 		EmptyView()
	.hidden()

这里,除了 destintaion 和 label 参数外,还多了一个 isActive 参数,用来绑定 pushActive 属性(使用 $ 关键字)。当 pushActive 被改变时,UI 会显示完全不同的结果(绑定了刷新动作)。当 pushActive 默认值为 false 时,此导航不会发生,页面显示之前的页面。一旦将 pushActive 修改为 true,NavigationLink 的导航会生效,也就是显示第二个页面。同时,这个 NavigationLink 不需要显示,因此它的 label 是一个空白视图(EmptyView 不会占用屏幕空间)。同时用 hidden() 进行了隐藏。

什么时候发生 push 导航?那是由另一个控件(比如普通 button)来触发的,触发导航的方式很简单,修改 pushActive = true 即可:

Button(action: 
 	pushActive = true
, label: 
 	...
)

这就是 NavigatoinLink 的真实用途,它并不仅仅是用于在页面上显示一个button,而是用来代替 UIKit 的 segue 组件。

dismiss 返回

struct DestinationView: View 
    // 声明属性presentationMode
    @Environment(\\.presentationMode) var presentationMode: Binding<PresentationMode>
    var body: some View 
        Text("Destination View")
            .onTapGesture 
                // 返回
                self.presentationMode.wrappedValue.dismiss()
            
    

@Environment将全局变量绑定到本地属性。

ScrollView

ScrollView 控制水平布局还是垂直布局是方式是在内部使用 HStack 或 VStack:

ScrollView(.horizontal, showsIndicators: false)
  HStack(spacing: 15
    ForEach(...)
      ...
    
  

这里 .horizontal 不是控制水平布局的,仅仅是指定允许手指滑动的方向,showsIndicators 指定是否显示滚动条。

自定义导航栏/工具栏

自定义导航栏其实就是自定义工具栏,因为你可以先隐藏导航栏,然后在设置一个工具栏代替它:

content
 	.navigationBarBackButtonHidden(true)
 	.navigationBarTitleDisplayMode(.inline)
 	.toolbar(content: buildToolbar)

接下来看具体怎么实现。

实现 ViewModifier

新建 swift 文件 Toolbar.swift。首先继承 ViewModifier:

struct FvtToolbar: ViewModifier 
    var title: String
    var rightTitle: String
    var rightAction: ()->()
 	  init(_ title:String, _ rightTitle:String, _ rightAction:@escaping ()->())
        self.title = title
        self.rightTitle = rightTitle
        self.rightAction = rightAction
    

3 个属性分别是:

  • title 工具栏中间的标题
  • rightTitle 工具栏右按钮的标题
  • rightAction 工具栏右按钮的点击事件处理回调

init 方法负责初始化它们。

作为一个 ViewModifier,最重要的方法就是 body 方法:

    func body(content: Content) -> some View 
        content
            .navigationBarBackButtonHidden(true)
            .navigationBarTitleDisplayMode(.inline)
            .toolbar(content: buildToolbar)
    

content 参数实际上就是工具栏所在的 View ,将由 modifier 方法传入。我们先隐藏了 back button 和导航栏原来的标题,然后用 toolbar(content:) 方法设置一个工具栏。其中 buildToolbar 是一个特殊 ToolbarContentBuilder 结构体的实例。我们无需真的去定义一个 ToolbarContentBuilder,而是只需定义一个能够返回 ToolbarContent 的方法即可:

    @ToolbarContentBuilder
    func buildToolbar() -> some ToolbarContent 
        ToolbarItem(placement: .principal) 
            Text(title)
                .font(.system(size: 17, weight: .bold, design: .default))
        
        ToolbarItem(placement: .navigationBarTrailing) 
            Button(action: 
                rightAction()
            ) 
                Text(rightTitle)
                    .font(.system(size: 17, weight: .regular, design: .default))
            
        
    

@ToolbarContentBuilder 注解将方法包装成 toolbar 方法要求传入的 ToolbarContentBuilder 结构。而这个方法对方法名和参数都没有要求,但对返回值,要求是 ToolbarContent,即一个 ToolbarItem 的集合,同时这个集合可以通过所谓的“多表达式闭包“得到。所谓的”多表达式闭包“,如同 body 方法,一个闭包中会包含多个 swift 表达式,swift 将这些表达式包装在一个集合中作为闭包的返回值。

扩展装饰器方法

为了便于使用,我们可以为 View 扩展出一个装饰器方法:

extension View 
    func fvtToolbar(_ title:String, _ rightTitle:String, _ rightAction:@escaping ()->()) -> some View 
        return modifier(FvtToolbar(title, rightTitle, rightAction))
    

fvtToolbar 方法接受 3 个参数,分别是 title、rightTitle 和 rightAction。返回一个 View。方法的返回值必须用 modifier 方法包裹,以支持 View 的链式调用。此外,modifier 会将自己的 content 传递给所包裹的对象。被包裹的对象必须是一个 ViewModifier

,比如FvtToolbar,它就实现了 ViewModifier (一个协议)。这样 ,modifier 会调用它的 body 方法,从而实现对目标视图的修改(比如将导航栏用我们提供的 Toolbar 代替)。

调用扩展的装饰器

在视图的 body 属性的最外层根视图使用该装饰器:

        .fvtToolbar("MECHANICAL_CHECK_TITLE".localize,
                    "NAVIGATION_BUTTON_CANCEL".localize)
            ...
        

这样,视图会呈现一个由文字标题栏和取消按钮构成的 toolbar。

EditButton

SwfitUI 内置了编辑按钮:

EditButton().padding()

SegmentedControl

SegmentedControl 是一个容器,里面包裹了多个 Text:

SegmentedControl(selection: $profile.prefersSeason) 
  ForEach(User.Season.allCases.identified(by: \\.self))
    season in
    Text(season.rawValue).tag(season)
  

$profile.prefersSeason 进行了双向绑定,其中 profile 是一个视图状态(@State)。ForEach 循环总是需要Sequence对象有一个唯一 id。

Identified(by:)

SwiftUI 的 List 在循环一个数组时,要求数组元素要么实现了 Identifiable 协议(其实就一个 id 属性),要么调用 identified(by:) 方法:

List(landmarkds.indetified(by: \\.id))()

\\.id\\.self是swift 的 keypath 语法。

绑定视图的刷新 - @State

视图状态绑定了刷新操作,当视图中的 @State 属性被修改,自动触发页面的刷新(调用 body 块)。

@State var zoomed = false

@State 还带来了另外一个效果,就是改变结构体成员的可变性。结构体跟类不同,普通成员函数中,无法对自身结构体的属性进行赋值操作,比如如下代码:

    func present(isPresented: Bool) -> some View 
        isShow = isPresented
        return self
    

编译器报错:Cannot assign to property: ‘self’ is immutable。以往的解决办法是在函数前用 mutating 修饰:

 		mutating func present(isPresented: Bool) -> some View 
      ...

但现在不用了,直接在 isShow 前面加一个 @State:

@State var isShow: Bool = false

@Published 属性包裹器

类似于 @State,@Published 允许你将被修饰的属性绑定指定视图的刷新操作。不同的是 @State 的绑定动作是自动的,默认就是绑定到 @State 属性所在的 View,但 @Published 需要你手动绑定到指定的 View。换句话说,@Published 属性可以用于任何 View ,但 @State 只能用于当前 View。

如果要让某个类中的属性能够绑定视图刷新操作,这个类必须实现 ObservableObject 协议:

class Bag: ObservableObject 
	var items = [String]()

如果你想让 items 属性能够绑定视图,则用 @Published 修饰它:

@Published var items = [String]()

这个语法糖会自动添加 willSet 属性监听器方法。

这样,Bag 就变成了一个 ObservableObject 对象。你可以在任意 View 中绑定它。只需在这个 View 中使用 @ObservedObject 声明一个 Bag 属性:

struct ContentView: View 
	@ObservedObject var bag = Bag()
	var body: some View 
		...
	

注意 @ObservedObject 关键字的使用,它将 bag 对象包装成外部可访问和修改的。这点和 @State 不同,@State 的对象一般是 private 的。这样,当你修改 bag 中的 items 值时,会触发 ContentView 更新。

View 的生命周期

类似于 UIViewController 的 viewDidAppear/viewDidDisappear, SwiftUI View 也有对应的生命周期方法:

				.onAppear 
//            viewModel.startSNRCheck()
            viewModel.showRemovingStep = true
        
        .onDisappear 
//            viewModel.startSNRCheck()
            viewModel.showRemovingStep = true
        

还有一个特殊的 onReceive 方法:

        .onReceive(viewModel.$isTestSucceeded)  testResult in
            isTestSucceeded = testResult
        

这个方法主动监听某个 Observable 对象的 published 属性,如果值发生变化,调用指定的块,块参数为新值。

SwiftUI 的动画

withAnimation

当某个 @State 属性被改变时,页面改变,同时让这种改变以动画方式进行,那么可以将该属性的修改代码包裹在 withAnimation 块中:

withAnimation(.basic(duration:1))
	self.zoomed.toggle()

Preview 可能看不到动画效果,需要在模拟器里面执行。withAnimation 的动画会影响整个 View,因为 @State 的刷新会导致整个 body 刷新。

注意,调试动画时不要使用 Canvas(可能看不出效果),最好在模拟器上调试。

transition

此外还有另一种 SwiftUI 动画 Transition ,则是单独对某个 View 执行动画:

Text("...").transition(.move(edge.trailing))

.move 指定该动画是平移,并且视图将从(from)屏幕的右侧(trailing)滑入。注意 .move() 的参数实际是 from,to 就是当前位置。

swiftUI 中的动画是可取消动画,不需要等上一个动画彻底完成即可执行下一个动画。

变形恢复

可以让 View 恢复到变形(缩放、平移、旋转)之前 :

.transition(.identity)

旋转和缩放

旋转使用修饰符 rotationEffect,缩放使用修饰符 scaleEffect:

Image(...)
  .rotationEffect(.degrees(showDetaiol ? 90 : ))
  .scaleEffect(showDetail ? 1.5 : 1)

如果在切换 showDetail 状态时使用了 withAnimation 块,则旋转和缩放将以动画方式执行:

withAnimation(.spring())
  self.showDetail.toggle()

animation

transition 是几何变形,但要让这个变形能够以动画方式进行,需要用到隐式动画 animation。在 SwiftUI 中,withAnimation 叫做显式动画,animation 叫做隐式动画。animation 修饰符和 transition 修饰符一样,也是作用于特定的 View(而不是 body 中的所有 view),它支持多种动画特效,比如弹簧动画:

.opacity(isShow ? 1 : 0)
.animation(.spring())

当然也可以匀速进行并指定动画时长 duration:

.animation(.linear(duration: 2))

GeometryReader

此外需要注意,隐式动画会影响到所有动画属性,包括位置,也就是说,隐式动画一创建时会默认位置从左上角开始(0,0)。因此为了让它不影响到我们的位置,我们需要用到 GeometryReader:

var body: some View 
	GeometryReader  geometry in
		VStack
    	.opacity(isShow ? 1 : 0)
 			.animation(.spring())
 			.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
	

这样它在动画开始时将 frame 设置到布局时的原始位置(宽高、中心都等于 geometry)而不是左上角(0,0)。

animation 的位置

animation 修饰符的位置非常重要,它让之前的动画特效生效。如果你把它放到一个 scaleEffect 之前执行,那么 animation 没有任何意义:

.animation(.easeIn(duration: 2))  
.scaleEffect( isPresented ? 1: 0)

因为在 animation 之前没有任何特效设置语句。你必须把 scaleEffect 放到前面执行:

.scaleEffect( isPresented ? 1: 0)
.animation(.easeIn(duration: 2))

animatino 取消

animation 还有一种特殊用法 animation(nil) ,表示取消之前的动画,例如:

Image(...)
  .rotationEffect(.degrees(showDetaiol ? 90 : ))
  .animation(nil)
  .scaleEffect(showDetail ? 1.5 : 1)
  .animation(.spring())

此时,旋转动画将被清除(注意,仅仅是不以动画方式执行,但旋转仍然有效,只不过是瞬间旋转)。但在 animation(nil) 之后的缩放动画和弹簧动画仍然执行。

不对称动画

不对称动画属于 transition 动画中的一种,但是它的进入和退出动画是非对称的。对于对称的过渡动画(转场动画,transition 动画),它的入场方式和出场方式是对称的,比如从左边进入,从左边退出。一般退出动画和进入动画是做相反运动,在这种情况下,我们只需指定入场方式即可,出场方式 SwiftUI 会自动根据入场方式计算。但对于非对称动画,Swift UI 无法通过入场方式推断出场方式,因此你必须同时指定入场动画和出场动画:

Image(...)
  .transition(
    .asymmetric(
      insertion: .move(edge:.trailing),
      removal: .scale
    )
  )

这里,Image 将从屏幕右侧滑入,但退出时则是逐渐缩小至不可见。

组合动画

combined 修饰符可以将两个动画组合成一个,形成一种1+1的效果:

Image(...)
  .transition(
    AnyTransition.move(edge:.trailing).combined(with:.opacity)
  )

从右边滑入+渐入效果。

AnyTransition

transition 修饰符中用了一个参数,比如我们用过的 move,scale,opacity 等,它们代表了不同的 transition 动画特效,但无一例外,统统都是 AnyTransition 类型。我们实际上可以自己扩展 AnyTransition 类型,从而定义自己的动画特效:

extension AnyTransition 
  static var moveAndScale: AnyTransition 
  	AnyTransition.move(edge: .trailing).combined(with: .opacity)
  
  static var myTransition: AnyTransition 
    AnyTransition.asymmetric(
      insertion: .move(edge:trailing),
      removal: .scale()以上是关于SwiftUI一日速成的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI一日速成

SwiftUI一日速成

#欧卡日记#第一日

X分钟速成Y (其中Y=Python)

细!小白初入网络安全必经之路,速成网络安全基础

极*Java速成教程 -