用SwiftUI打造一个精美App

SwiftUI中的一切都是视图。spring

文章来源:Building Custom Views with SwiftUIswift

更多SwiftUI文章:安全

手把手教你用SwiftUI写程序markdown

SwiftUI布局基础

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
//            .edgesIgnoringSafeArea(.all)
    }
}
复制代码

这段代码包含三个视图:app

1.png

  • 视图等级底部的文本(图中Hello World)ide

  • 内容视图(和文本的布局一致,即图中Hello World四周白线内)函数

  • 根视图(屏幕Size - 安全区域)oop

    若是想将根视图扩展到安全区,可使用edgesIgnoringSafeArea(.all)修饰器布局

固然,文本和文本的内容视图,咱们一般当作同一个来操做post

2.png

在SwiftUI中,不能给子视图强制规定一个尺寸,而是应该有父视图决定

布局步骤

  • 父视图提供给子视图一个Size
  • 子视图决定自身的Size(子视图也许不能彻底使用父视图的Size)
  • 父视图将子视图放在其坐标系中
  • SwiftUI会让视图坐标像最接近的像素值取整

例一:查看一段代码的布局:

var body: some View {
        Text("Avocado Toast")
            .padding(10)
            .background(Color.green)
    }
复制代码

在设置backgroundpadding修饰器时,会在Text视图和根视图中间插入对应的背景视图和边距视图

1.gif

例二:图片的原尺寸为20x20,咱们但愿1.5倍尺寸展现图片

struct ContentView: View {
    var body: some View {
        Image("20x20_avoado")
    }
}
复制代码

作法:

.frame(width: 30, height: 30)
复制代码

效果:图片尺寸不会发生变化,可是在图片周围会插入一个30x30尺寸的Frame视图

3.png

在SwiftUI中frame并非一个重要的布局元素,它其实只是一个View。

例三:

// 子视图必须平等竞争一个空间
HStack {
            Text("Delicious")
            Image("20x20_avocado")
            Text("Avocado Toast")
        }
        .lineLimit(1)
复制代码

2.1.gif

  • 设置文字底基线对齐

    4.png

  • 设置图片的底基线

    5.png

例四:让不一样容器中的视图对齐

6.png

  • 自定义对齐方式

    extension VerticalAlignment {
        private enum MidStarAndTitle : AlignmentID {
            // 告诉SwiftUI如何计算默认值
            static func defaultValue(in d: ViewDimensions) -> CGFloat {
                return d[.bottom]
            }
        }
        static let midStarAndTitle = VerticalAlignment(MidStarAndTitle.self)
    }
    复制代码
  • 设置文字的基线

    7.png

SwiftUI绘图

SwiftUI默认提供了多种样式的图形,好比圆形,胶囊和椭圆

8.png

  • 实现渐变色

    9.png

    • 角渐变色

      10.png

    • 使用角渐变色填充圆

      11.png

    • 使用渐变色填充圆环

      12.png

  • 实现复杂图形绘制

    3.gif

    完整代码可参见:官方Demo

    整体步骤主要包括:

    1. 建立单个楔形的数据模型
    class Ring: ObservableObject {
        /// A single wedge within a chart ring.
        struct Wedge: Equatable {
            /// 弧度值(全部楔形的弧度值之合最大为2π,即360°)
            var width: Double
            /// 横轴深度比例 [0,1]. (用来计算楔形的长度)
            var depth: Double
            /// 颜色值
            var hue: Double
            }
    }
    复制代码
    1. 绘制单个子图形
    struct WedgeShape: Shape {
      func path(in rect: CGRect) -> Path {
        		// WedgeGeometry是用来计算绘制信息的类,详细代码见Demo。
            let points = WedgeGeometry(wedge, in: rect)
    
            var path = Path()
            path.addArc(center: points.center, radius: points.innerRadius,
                startAngle: .radians(wedge.start), endAngle: .radians(wedge.end),
                clockwise: false)
            path.addLine(to: points[.bottomTrailing])
            path.addArc(center: points.center, radius: points.outerRadius,
                startAngle: .radians(wedge.end), endAngle: .radians(wedge.start),
                clockwise: true)
            path.closeSubpath()
            return path
        }  
      // ···
    }
    复制代码
    1. 用ZStack组装全部的楔形
    let wedges = ZStack {
                ForEach(ring.wedgeIDs, id: \.self) { wedgeID in
                    WedgeView(wedge: self.ring.wedges[wedgeID]!)
    
                    // use a custom transition for insertions and deletions.
                    .transition(.scaleAndFade)
    
                    // remove wedges when they're tapped.
                    .onTapGesture {
                        withAnimation(.spring()) {
                            self.ring.removeWedge(id: wedgeID)
                        }
                    }
                }
    
                // 若是不加这个Spacer(),会使Mac程序,在没添加任何楔形时,APP尺寸为0。
                Spacer()
            }
    复制代码

    为了更好的理解这个工程,你还须要一些知识:

    如何使用Animatable自定义复杂的动画

    假如咱们想实现一个简单的SwiftUI动画,好比点击Button按钮渐变消失,咱们能够这样来实现:

    @State private var hidden = false
    
        var body: some View {
            Button("Tap Me") {
                self.hidden = true
            }
            .opacity(hidden ? 0 : 1)
            .animation(.easeInOut(duration: 2))
       }
    复制代码

    在这个例子中,咱们使用@State修饰变量hidden,当hidden的值发生变化时,SwiftUI会自动为咱们处理渐变更画。而SwiftUI可以为咱们自动执行动画的前提是:SwiftUI已经知道要若是展现该动画效果,那什么状况下,SwiftUI不知道要如何展现动画呢?

    好比:咱们经过下面代码绘制出了多角形。

    Shape(sides: 3)
    复制代码

    该方法支持传入不一样的值,生成不一样的多边形。若是咱们但愿从三边形变成四边型,那么能够写以下代码:

    Shape(sides: isSquare ? 4 : 3)
        .stroke(Color.blue, lineWidth: 3)
        .animation(.easeInOut(duration: duration))
    复制代码

    可是运行代码后发现,这段代码并没有动画过渡效果。这是由于在执行动画的过程当中,SwiftUI会从起始状态到终止状态分红不一样的阶段来绘制,像opacity从0-1,可能会分红0,0.1,0.2,0.3,···,0.9,1.0,SwiftUI依次进行绘制,从而展现过渡状态。

    同理,从三角形变到四边形,SwiftUI也须要绘制中间状态,但SwiftUI并不知道该如何绘制3.5边形。这时就须要咱们本身来告诉SwiftUI该如何绘制了。

    • animatableDataAnimatable协议中,惟一须要实现的方法,经过这个方法来告诉SwiftUI须要监听哪些属性的变化。

      // 表明须要监听的值为Float类型
      var animatableData: Float {
              get { //··· }
              set { //··· }
      }
      复制代码
    • 然而并非全部类型的属性都可以被SwiftUI所监听,只有遵循VectorArithmetic协议的对象AnimatablePair, CGFloat, Double, EmptyAnimatableData and Float才能被SwiftUI监听。

    • 要实现Demo中楔形图片变换的效果,须要监听的值有startenddepthhue这四个值。animatableData属性的返回值也应该包含这四个值。

      extension Ring.Wedge: Animatable {
          typealias AnimatableData = AnimatablePair<AnimatablePair<Double, Double>, AnimatablePair<Double, Double>>
      
          var animatableData: AnimatableData {
              get {
                  .init(.init(start, end), .init(depth, hue))
              }
              set {
                  start = newValue.first.first
                  end = newValue.first.second
                  depth = newValue.second.first
                  hue = newValue.second.second
              }
          }
      }
      复制代码
    • 接下来在这四个属性发生变化时,SwiftUI会经过函数func path(in rect: CGRect) -> Path {}从新绘制,这样就能够展现动画的过渡效果了

    一些琐碎的知识点

    • 使用drawingGroup()提升复杂UI的渲染效率

      以往每建立一个楔形,都是一个单独的View,当楔形数量很是多时,再加上每一个View都在执行动画,很是耗费性能。在SwiftUI中,能够经过drawingGroup()将相同类型的View经过Metal绘制在一张画布上,从而减小渲染耗费的性能,避免卡顿。

    • 使用Equatable防止视图的新值和旧值相同时,更新子视图

      struct Wedge: Equatable { 
      	// ···
      }
      复制代码
    • 使用PassthroughSubject通知SwiftUI值发生变化

      • PassthroughSubject

        • 使用PassthroughSubject通知绑定的属性的视图,属性发生变化,须要从新绘制。
        let objectWillChange = PassthroughSubject<Void, Never>()
        
        private(set) var wedgeIDs = [Int]() {
                willSet {
                    objectWillChange.send()
                }
            }
        复制代码
        • contentView中监听了Ring模型
        @EnvironmentObject var ring: Ring
        复制代码

        所以在Ring模型的wedgeIDs发生变化时,会发出通知告知contentView使用其绘制的地方,须要从新绘制

      • CurrentValueSubject

        咱们经常使用的@Published属性包装器,实际上就是一种CurrentValueSubject

      简单来讲:PassthroughSubject用于表示事件。CurrentValueSubject用于表示状态。用现实世界的案例进行类比。

      PassthroughSubject = 门铃按钮,当有人按门时,只有在你在家时才会收到通知。CurrentValueSubject = 电灯开关,当你在外面时,有人打开了您家中的灯。你回到家,你知道有人打开了它们。

    参考:

    stackoverflow.com/questions/6…

    swiftui-lab.com/swiftui-ani…

相关文章
相关标签/搜索