SwiftUI 實戰:從 0 到 1 研發一個 App


心得感悟

起初看到 WWDC 上的演示 SwiftUI 時,我就覺得 SwiftUI 有種陌生的熟悉感(聲明式語法),所以體驗下,看看有沒有什么啟發。

先說下整體項目完成下來的感受:

  • 用 Swift + SwiftUI 開發 iOS 項目效率很高,本人之前沒有接觸過 Swift 語言,這次是從 0 開始學 swift 語言以及 swiftUI 框架的,每天花 2 個小時斷斷續續大致花了 3 天時間掌握了基本的 Swift 語法,而 SwiftUI 框架的掌握是按照官方的視頻學習的(贊下 Apple 的文檔、教學視頻的完備性);
  • 從我一個前端開發工程師的視角看來,SwiftUI 使用的 “DSL”、狀態管理和前端的 React 很相似,不少概念是相通的,比如 State,Props。
  • SwiftUI + Xcode11 + macOS Catalina 讓研發效率大大提升,預覽模式做到了所見即所得,注意這里是預覽模式而不是模擬器模式
  • 借助於預覽模式,UI 也可以做到很好的單頁面的單元測試;這一點比較有意思,獨立的 View 可以獨立預覽,然后可以注入數據,實時預覽調試。

執行環境

  • macOS Mojave: 10.14.5
  • xcode: Version 11.0 beta 6 (11M392q)

項目信息

github: https://github.com/young-cowboy/swiftui-app-habits

App 效果預覽

2 個關鍵點

這里提 2 個關鍵點“尾部閉包語法”以及“狀態管理”,因為理解他們就基本能掌握 SwiftUI 來開發一個簡單的項目。尾部閉包語法能讓我們理解 SwiftUI 是如何通過聲明式語法來創建 View 的,“數據傳遞”能讓我們了解怎么實現數據流功能。

尾部閉包語法

Swift 有一個很重要的概念叫做“尾部閉包語法”,這種語法讓 Swift DSL 的書寫變得更“聲明式”,我們先了解下 Swift 閉包定義:

{(parameters) -> return type in
  // 代碼
}

這樣來看是不是和 JavaScript 的閉包有一定的相似,但是閉包有很多種語法形式,這里我們舉個例子說明下:

import Foundation

func wrapClosure (closure: (String, String) -> Void) {
    let name = "KK"
    let age = "18"

    closure(name, age)
}

定義一個 function,參數只有一個,並且這個參數為一個函數,然后在方法體內部調用這個函數,傳入參數;

func foo (name: String, age: String) {
    print("My name is \(name) and \(age) years old")
}

wrapClosure(closure: foo) // My name is KK and 18 years old

通常我們會這樣執行,定義個函數來處理傳參。但是利用閉包的語法,有更簡潔的方式,如下就一個閉包的語法了

wrapClosure(closure: { (name: String, age: String) in
    print("My name is \(name) and \(age) years old")
})

還可以利用 Swift 的類型推斷能力,不寫閉包參數的類型,如下

wrapClosure(closure: { name, age in
    print("My name is \(name) and \(age) years old")
})

還可以利用快捷參數名來獲取參數,如下

wrapClosure(closure: {
    print("My name is \($0) and \($1) years old")
})

最后還有更簡潔的方式,這樣是 SwiftUI 利用到的一個特性,所以SwiftUI 的 DSL 看起來很有聲明式的感覺,這個特性叫 “尾部閉包語法”,這次方法執行連括號都可以不寫了。

如果一個閉包是以一個函數的最后一個參數傳遞的,那么它就可以在函數的圓括號以外內聯。

wrapClosure {
    print("My name is \($0) and \($1) years old")
}

到此應該能看出為什么 SwiftUI 能用聲明式的語法來創建各種 View 協議的視圖了。

數據傳遞

在做面向用戶的功能時,一個很主要的點是數據傳遞,不同的設計理念解決特點場景的問題。這里簡單介紹下 SwiftUI 的數據怎樣傳遞,這里有一個概念property wrapper,我理解是裝飾器 decorate,介紹三個裝飾器 @State@Binding@ObservedObject@Published

  • @State 裝飾過的屬性發生了變化,SwiftUI 會根據新的屬性值重新創建視圖
  • @Binding 修飾器修飾后,屬性變成了一個引用類型,傳遞變成了引用傳遞,這樣父子視圖的狀態就能關聯起
  • @ObservedObject 修飾一個復雜類型數據,可以被多個 View 所使用,用 @Published 修飾對象里屬性,表示需要被監聽;

更多的細節可以參考:https://mecid.github.io/2019/06/12/understanding-property-wrappers-in-swiftui/

頁面結構設計

一共有 4 個頁面 HabitListView, AddButtonView, HabitDetailView, AddItemView

數據結構設計

項目涉及到三個數據結構: HabitItem(習慣項), HabitIconArray(習慣圖標), HabitColor(習慣主題)

HabitColor 主題色,保存了習慣的主題

public let UserColorArray = [
  Color(red:75 / 255, green:166 / 255, blue: 239 / 255),
  Color(red:161 / 255, green:206 / 255, blue: 97 / 255),
  Color(red:248 / 255, green:214 / 255, blue: 80 / 255),
  Color(red:243 / 255, green:176 / 255, blue: 74 / 255),
  Color(red:238 / 255, green:140 / 255, blue: 111 / 255),
  Color(red:237 / 255, green:113 / 255, blue: 165 / 255),
  Color(red:207 / 255, green:102 / 255, blue: 247 / 255),
  Color(red:77 / 255, green:110 / 255, blue: 247 / 255),
  Color(red:236 / 255, green:107 / 255, blue: 102 / 255)
]

HabitIconArray 習慣的圖標庫

public let IconNameArray: [String] = [
    "alarm",
    "book",
    "pencil",
    "desktopcomputer",
    "gamecontroller",
    "sportscourt",
    "lightbulb"
]

HabitItem 用來保存習慣的詳細信息,這里實現 ObservableObject 協議,用來告訴 SwiftUI 這個對象需要監聽,用 Published property wrapper 包裝了 checkList 屬性,表示這個屬性是要監聽的,因為它可能需要傳遞給子 View

class HabitItem: Identifiable, ObservableObject {
    var name: String = ""
    var iconName: String = "clock"
    var theme: Color = UserColor.color1.value
    var uuid: Int = 0

    @Published var checkList: [Bool] = [false, false, false, false, false, false, false]
    
    init () {
        
    }
    
    init(name: String, iconName: String, theme: Color) {
        self.name = name
        self.iconName = iconName
        self.theme = theme
        self.uuid = generatteID()
    }
}

UI 設置

具體代碼參考倉庫代碼,這里講解一個流程,新建一個“習慣”項,在 MainView 里

MainView

  ...
  @State var sheetVisible: Bool = false
  @State var sheetType: String = "add"
  ...
  ...
  AddButtonView() {
      self.sheetType = "add"
      self.sheetVisible = true
  }
  ...

AddButtonView 利用尾部閉包語法內聯了一個閉包用來相應事件

AddButtonView 里定義了 onPressed 屬性

struct AddButtonView: View {
    var onPressed: () -> Void
    
    var body: some View {
        Button(action: {
            self.onPressed()
        }) {
            HStack {
                Image(systemName: "plus.circle.fill")
                    .resizable()
                    .frame(width: 60, height: 60)
                    .foregroundColor(Color.blue)
            }
        }
    }
}

AddItemView 里的新增按鈕點擊后相應事件,把選中的數據傳遞 onSumit 屬性回調里

struct AddItemView: View {
    @State var newItemTitle = ""
    @State var selectIconIndex: Int = 0
    @State var selectColorIndex: Int = 0
    
    var onSumit: (HabitItem) -> Void
    var onDissmis: () -> Void
    
    var body: some View {
         VStack {
            ...
            ...
            VStack {
                Button(action: {
                    if self.newItemTitle != "" {
                        let iconName = IconNameArray[self.selectIconIndex];
                        let theme = UserColorArray[self.selectColorIndex];

                        self.onSumit(HabitItem(name: self.newItemTitle, iconName: iconName, theme: theme))
                    }
                }) {
                    Text("新增").frame(minWidth: 0, maxWidth: .infinity)
                }
                ...
              }
            ...
            }
          ...
        }
    }
}

在 MainView 里,利用閉包處理回調事件新增選項

AddItemView(onSumit: { item in
    self.habitItemList.insert(item, at: 0)
    self.sheetVisible = false
  }, onDissmis: { self.sheetVisible = false })

剩下的功能大同小異,可以把代碼拉下來本地運行看看效果,github: https://github.com/young-cowboy/swiftui-app-habits

謝謝


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM