用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…

相關文章
相關標籤/搜索