SwiftUI快速入門

SwiftUI介紹

SwiftUI is a modern way to declare user interfaces for any Apple platform. Create beautiful, dynamic apps faster than ever before.react

SwiftUI是一套代碼,能夠運行而且自動適配到全部的Apple設備上(iPhone,iPad,Mac,iWatch)

環境依賴:

  • Xcode 11 (Download from Apple)
  • iOS 13 / macOS 10.15 / tvOS 13 / watchOS 6
  • macOS Catalina in order to have SwiftUI render in the canvas (Download from Apple)

語法設計

隨着整個軟件開發領域的發展,近年來涌現的 React Native ,Weex,Flutter技術,逐漸證實了一個事實:在 UI 開發領域,描述性語言是最佳方式。 SwiftUI使用易於閱讀和編寫的聲明式 Swift 語法,建立了一個很是接近HTML 描述語言的 DSL(domain-specific language)。 蘋果一直在嘗試所見即所得的理念,早期 xib 到 stroyboard,可是發展的都不大順暢。ios

簡單對比一下其餘語法:git

SwiftUIgithub

VStack {
    Text("a1")
    Text("a2")
    Text("a3")
}
複製代碼

Flutterweb

Stack(
        children: <Widget>[
                 Text("a1"),
                 Text("a2"),
                 Text("a3"),
         ],
)
複製代碼

HTMLspring

<div>
  <label>a1</label>
  <label>a2</label>
  <label>a3</label>
</div>
複製代碼
  • HTML,界面描述能力最好,可是缺少編程能力
  • Flutter,具備很強的編程能力,函數調用裏的 ,、{}的符號的存在,佈局代碼觀感不大好。

Xcode 11新技能

拖放: 只需拖動畫布上的控件便可在用戶界面中排列組件。單擊打開檢查器能夠拖具體控件,也能夠拖控件的字體、顏色、圓角、對齊方式等屬性。代碼中能夠任意拖,可是畫布只能拖控件,不能拖屬性。畫布相關修改後,代碼中自動生成。編程

cmd+鼠標左鍵,呼出快速操做,不只在代碼中支持,預覽畫布中也支持,Inspect功能很強大。canvas

動態替換: 畫布右下角第一個按鈕。 Swift 編譯器和運行時徹底嵌入到 Xcode 中,所以咱們的應用將不斷構建並運行。咱們看到的設計畫布不只僅是用戶界面的模擬——它就是應用的實時效果。Xcode 能夠直接在你的實時應用中使用「動態替換」交換編輯過的代碼。

預覽: 右上角:Editor and Canvas,快捷鍵:shift + cmd + enter。 如今你能夠爲任何 SwiftUI 視圖建立一個或多個預覽,以獲取示例數據並配置幾乎全部用戶可能看到的內容,諸如大字體、本地化或黑暗模式。預覽還能夠顯示你的 UI 在任何設備和任何方向上的效果。 預覽 是 Apple 用來對標 RN 或者 Flutter 的 Hot Reloading 的開發工具。 Xcode會自動刷新,也能夠手動刷新,快捷鍵:option + cmd + Pswift

基礎控件

簡單排列Label、Button、Image:bash

struct ViewsTest: View {
    var body: some View {
        VStack {
            Text("Label")
                .foregroundColor(Color.orange)
                .font(Font.system(size: 17))
            Button(action: {
                print("click")
            }) {
                Text("Button")
            }
            Image("image_write")
                .onTapGesture {
                    print("image click")
            }
        }
    }
}
struct ViewsTest_Previews: PreviewProvider {
    static var previews: some View {
        ViewsTest()
    }
}
複製代碼

View協議的定義:

public protocol View {
    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    associatedtype Body : View
    /// Declares the content and behavior of this view.
    var body: Self.Body { get }
}
複製代碼

PreviewProvider協議定義:

public protocol PreviewProvider : _PreviewProvider {
    /// The type of the previews variable.
    associatedtype Previews : View
    /// Generates a collection of previews.
    static var previews: Self.Previews { get }

    /// Returns which platform to run the provider on.
    ///
    /// When `nil`, Xcode infers the platform based on the file the
    /// `PreviewProvider` is defined in. This should only be provided when the
    /// file is in targets that support multiple platforms.
    static var platform: PreviewPlatform? { get }
}
複製代碼

_PreviewProvider協議的定義就看不到源碼了。

some View 這種寫法使用了 Swift 5.1 的 Opaque return types 特性。它向編譯器做出保證,每次 body 獲得的必定是某一個肯定的,遵照 View 協議的類型,可是請編譯器「網開一面」,不要再細究具體的類型。相似於OC id。

UIKit與SwiftUI相同功能組件對比:

View Controllers

Views and Controls

佈局

先上代碼:

VStack(alignment: .center, spacing: 10) {
            Text("a")
            Text("b")
            HStack(alignment: .top, spacing: 20) {
                Text("c")
                Text("d")
            }
        }
複製代碼

a、b、(c、d)垂直排列,c、d水平排列。

HStack:用於將子視圖在水平線上依次排列 VStack:用於將子視圖在垂直線上依次排列 ZStack:用於將子視圖在垂直於屏幕線上依次排列,能夠實現覆蓋子視圖。相似在 UIKit中向一個 View添加不一樣的 SubView。 支持各類Stack的嵌套。

看下VStack的定義:

public struct VStack<Content> : View where Content : View {
    /// Creates an instance with the given `spacing` and Y axis `alignment`.
    ///
    /// - Parameters:
    ///     - alignment: the guide that will have the same horizontal screen
    ///       coordinate for all children.
    ///     - spacing: the distance between adjacent children, or nil if the
    ///       stack should choose a default distance for each pair of children.
    @inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
}
複製代碼

咱們發現返回值是Content協議類型,並使用@ViewBuilder進行標記。這裏使用了 Swift 5.1 的另外一個新特性:Funtion builders。

這個新特性的大概預覽:

// Original source code:
@TupleBuilder
func build() -> (Int, Int, Int) {
  1
  2
  3
}
// This code is interpreted exactly as if it were this code:
func build() -> (Int, Int, Int) {
  let _a = 1
  let _b = 2
  let _c = 3
  return TupleBuilder.buildBlock(_a, _b, _c)
}
複製代碼

ViewBuilder的定義爲:

@_functionBuilder public struct ViewBuilder {
    /// Builds an empty view from an block containing no statements, `{ }`.
    public static func buildBlock() -> EmptyView
    /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through
    /// unmodified.
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}
複製代碼

使用 @_functionBuilder 進行標記的類型 (這裏的 ViewBuilder),能夠被用來對其餘內容進行標記。

HStack調用等效僞代碼,不能實際編譯:

HStack(alignment: .center,spacing: 20) { viewBuilder -> Content in
    let text1 = Text("c")
    let text2 = Text("d")
    return viewBuilder.buildBlock(text1, text2)
}
複製代碼

Stack相似OC UIStackView,沒用過? 看這個:

控件優先級:

若是兩個控件須要設置哪一個優先展現,使用佈局優先級 .layoutpriority(1) ,數字越大,優先級越高

HStack(alignment: .lastTextBaseline, spacing: 10) { // alignment對齊方式,spacing控件間距
            Text("Hello").layoutPriority(100)
            Text("哈哈哈").layoutPriority(20).alignmentGuide(.lastTextBaseline) { d in
                d[.bottom] * 0.5
            }
            Text("World").layoutPriority(10)
        }
複製代碼

數據綁定

系統提供Combine 庫,類比 reactive-cocoa,rxSwift 庫。

@State @Binding @EnvironmentObject @ObservedObject 等一系列的Combine提供的功能,支持實現數據綁定,view自動更新數據。

Combine 的鏈式調用和 SwiftUI 組件裏的 ViewModifier 雖然形似但內核是不同的,Combine 自己就是數據流處理的模式;而 SwiftUI 裏鏈式調用只是爲了形式上返回單一對象。

@State

定義一個響應式狀態,它的變化會致使依賴它的視圖自動更新(單向)

示例:點擊play按鈕,label自動計數

struct DataFlowTest: View {
    @State var number : Int = 0
    var body: some View {
        VStack {
            Text("play number is : \(self.number)")
            Button(action: {
                self.number += 1
            }) {
                Text("Play")
            }
        }
    }
}
複製代碼

@Binding

視圖和數據的雙向綁定。 傳遞參數使用'$'符號

struct DataFlowTest: View {
    @State var number : Int = 0
    var body: some View {
        VStack {
            Text("play number is : \(self.number)")
            PlayButton(count: $number)
        }
    }
}
struct PlayButton : View {
    @Binding var count : Int    
    var body: some View {
        Button(action: {
            self.count += 1
        }) {
            Text("Play")
        }
    }
}
複製代碼

ObservableObject(以前版本用BindableObject)

自定義綁定類型不能使用@Binding,須要使用@ObservedObject(以前使用@ObjectBinding) 做用等價於 @Binding,可是支持使用一個外部對象。

Xcode 11 beta 5 : BindableObject protocol is become ObservableObject and willChange become objectWillChange.

class UserData: ObservableObject {
    let objectWillChange = PassthroughSubject<Void, Never>() // 注意:變量名必須是這個objectWillChange
    var userName = "no name" {
        willSet {
            objectWillChange.send()
        }
    }
}
複製代碼
struct DataFlowTest: View {
    @State var userData = UserData()
    var body: some View {
        VStack {
            Text("user name is : \(self.userData.userName)")
            PlayButton(userModel: userData)
        }
    }
}
struct PlayButton : View {
    @ObservedObject var userModel: UserData
    var body: some View {
        Button(action: {
            self.userModel.userName = "Tom"
        }) {
            Text("Play")
        }
    }
}
複製代碼

EnvironmentObject

用於實現單個model綁定多個view

使用步驟

1.使用environmentObject()綁定同一個對象userModel

struct ContentView: View {
    let userModel = UserData()
    var body: some View {
        NavigationView {
            List {
                Section() {
                    NavigationLink(destination: DataFlowModelTest().environmentObject(userModel)) {
                        Text("DataFlow model")
                    }
                    NavigationLink(destination: DataFlowModelTest2().environmentObject(userModel)) {
                        Text("DataFlow model 2")
                    }
                }
            }
        }
    }
}
複製代碼

2.使用@EnvironmentObject標記所要綁定的對象

這時候修改某個view中的UserData對象,其餘view的值自動變換

struct DataFlowModelTest: View {
    @EnvironmentObject var userModel : UserData
    var body: some View {
        VStack {
            Text("change name :\(userModel.userName)")
            Button(action: {
                self.userModel.userName = "tom"
            }) {
                Text("change name")
            }
        }
    }
}
複製代碼

動畫

在 SwiftUI 中,作動畫變的十分簡單。

直接在 View 上使用 .animation modifier

@inlinable public func animation(_ animation: Animation?) -> some View
複製代碼
@State var showDetail = false
 var body: some View {
    Button(action: {
        self.showDetail.toggle()
    }) {
        Image("image_write")
            .imageScale(.large)
            .rotationEffect(.degrees(showDetail ? 90 : 0))
            .animation(.linear(duration: 10))
            .animation(nil)
            .scaleEffect(showDetail ? 3 : 1)
            .padding()
            .animation(.spring())
    }
}

複製代碼

SwiftUI 作的事情等效因而把以前的全部 modifier 檢查一遍,而後找出全部知足 Animatable 協議的 view 上的數值變化,好比角度、位置、尺寸等,而後將這些變化打個包,建立一個事務 (Transaction) 並提交給底層渲染去作動畫。

使用 withAnimation {} 來控制某個 State,進而觸發動畫。

withAnimation() 接受一個指定所需動畫類型的參數。 好比,按鈕點擊後,字體顏色2s時間變化爲黃色:

@State var bgColor = Color.green
var body: some View {
    Button(action: {
       withAnimation(.easeInOut(duration: 2)) {
           self.bgColor = Color.orange
       }
    }) {
       Text("change color")
        .background(bgColor)
    }
}
複製代碼

這個方法至關於把一個 animation 設置到 View 數值變化的 Transaction 上,並提交給底層渲染去作動畫。

轉場動畫

@inlinable public func transition(_ t: AnyTransition) -> some View
複製代碼

view支持直接調用.transition,便可實現轉場動畫。系統提供offset、scale、opacity、slide、move等。

ShowDetailView()
         .transition(.slide)
複製代碼

自定義轉場動畫:

extension AnyTransition {
    static var customTransition: AnyTransition {
        let insertion = AnyTransition.move(edge: .trailing)
            .combined(with: .opacity)
        let removal = AnyTransition.scale
            .combined(with: .opacity)
        return .asymmetric(insertion: insertion, removal: removal) // insertion:入場動畫,removal:出場動畫
    }
}
        // 調用,跟系統的同樣
     ShowDetailView()
        .transition(.customTransition)

複製代碼

與UIKit交互

UIKit中加載SwiftUI的View

UIHostingController

Use the UIHostingController to bridge SwiftUI views into a UIKit view and view controller hierarchy.

UIHostingController 是一個 UIViewController 子類,它將負責接受一個 SwiftUI 的 View 描述並將其用 UIKit 進行渲染 (在 iOS 下的狀況)。UIHostingController 就是一個普通的 UIViewController,所以徹底能夠作到將 SwiftUI 建立的界面一點點集成到已有的 UIKit app 中,而並不須要從頭開始就是基於 SwiftUI 的構建。

eg:設置rootViewController爲swiftUI的view,須要使用UIHostingController構建

window.rootViewController = UIHostingController(rootView: ContentView())
複製代碼

SwiftUI中加載UIKit的View、ViewController

SwiftUI的View中加載UIKit的View

UIViewRepresentable

Use the UIViewRepresentable protocol to bridge UIKit views into SwiftUI, not view controllers.

好比SwiftUI中須要加載WebView,SwiftUI是沒有WebView控件的,而WebKit的WKWebView是繼承自UIView,因此須要使用UIViewRepresentable中轉。

struct WebViewTestRepresentable : UIViewRepresentable {
    typealias UIViewType = WKWebView
   
    func makeUIView(context: UIViewRepresentableContext<WebViewTestRepresentable>) -> WebViewTest.UIViewType {
        return WKWebView()
    }
   
    func updateUIView(_ uiView: WebViewTest.UIViewType, context: UIViewRepresentableContext<WebViewTestRepresentable>) {
        let req = URLRequest(url: URL(string: "https://www.apple.com")!)
        uiView.load(req)
    }
}

struct WebViewTestView_Previews : PreviewProvider {
    static var previews: some View {
        WebViewTestRepresentable()
    }
}
複製代碼

SwiftUI的view加載UIKit的UIViewController

UIViewControllerRepresentable

Create a structure that conforms to UIViewControllerRepresentable and implement the protocol requirements to include a UIViewController in your SwiftUI view hierarchy.

具體方法: 新建一個結構體,繼承自UIViewControllerRepresentable,實現Protocol:建立、更新UIViewController

struct UIKitVCRepresentable : UIViewControllerRepresentable {
    func makeUIViewController(context: UIViewControllerRepresentableContext<UIKitVCRepresentable>) -> UIViewController {
        return UIKitViewController() // 返回新構建的UIViewController子類
    }
    // vc展現的時候回調,相似oc裏viewWillAppear/viewDidAppear
    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<UIKitVCRepresentable>) {
    }
}
複製代碼

注意: 這種方式實現的是嵌套在SwiftUI的view中的UIViewController,相似OC中addController

最後

目前只支持蘋果生態內的全部os系統,but 最近出了個支持web開發的測試項目:SwiftWebUI Swift 是開源的,跨平臺運行也不是問題,之後可能會支持更多的平臺。

參考

developer.apple.com/documentati… developer.apple.com/documentati… developer.apple.com/videos/play… developer.apple.com/tutorials/s… fuckingswiftui.com/ onevcat.com/2019/06/swi… swift.gg/2019/09/12/…

本文demo:

github.com/342261733/S…

相關文章
相關標籤/搜索