【SwiftUI 2.0】它好用嗎?能吃嗎?

因為APPLEWWDC19發表了SwiftUI這個語法簡潔的Framework,個人發想APPLE應該是在學Google的Flutter吧?想讓寫APP的門檻降低,不過秉持APPLE一貫的傳統,當然這個東西只能寫自家系統的東西,像macOS / watchOS / tvOS,不像Google想以Flutter一統江湖。

作業環境

項目 版本
CPU Apple M1
macOS Big Sur 11.4
Xcode 12.5

基本使用

建立專案

  • 首先,因為SwiftUI支援iOS 13+ / macOS 10.15+以上,所以呢請使用Xcode 11以上來做測試。
  • 這裡新建一個名叫『SwiftUI_First』的專案,Interface選擇『SwiftUI』, LifeCycle選擇『SwiftUI App』,當然如果要混搭風的話,LifeCycle可以選擇『UIKit App Delegate』。
  • 在這裡跟以前最顯眼的不同,就是它有預覽的功能,而且要macOS 10.15以上才支援,其主要是因為它使用了PreviewProvider這個Protocol,當然預覽可以不只一支,不過要在模擬器有安裝的才可以 (風扇一直轉…XD)。
Xcode -> Editor -> Create Preview

簡單的例子

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        
        ZStack(alignment: .center, content: {           // ZStack => 有階層的StackView (z軸上的先後次序) / 內容物對齊置中
            
            RoundedRectangle(cornerRadius: 25.0)        // RoundedRectangle => 可以看成是長方形的UIView
                .stroke(lineWidth: 3.0)                 // 畫外框線 (如果沒有的話,會變成實心的)
            
            Text("Hello World !!!")                     // Text => 可以看成是UILabel
        })
        .padding(.horizontal)                           // .padding(.horizontal) => 置中對齊
        .foregroundColor(.red)                          // .foregroundColor(.red) => 前景顏色
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().preferredColorScheme(.light).previewDevice("iPhone 11")
    }
}

進階例子

要做一個長得像這樣的APP

符號表

let emojis = [
    "🏳️", "🏴", "🏴‍☠️", "🏁", "🚩",
    "🏳️‍🌈", "🏳️‍⚧️", "🇺🇳", "🇹🇹", "🇹🇷",
    "🇹🇨", "🇹🇲", "🇧🇹", "🇨🇫", "🇨🇳",
    "🇩🇰", "🇪🇨", "🇪🇷", "🇵🇬", "🇧🇷",
    "🇧🇧", "🇵🇾", "🇧🇭", "🇧🇸", "🇵🇦",
]

新增一個CardView

  • 新增一個中間有字的View
// MARK: - CardView
struct CardView: View {
    
    var body: some View {
        
        ZStack {
            RoundedRectangle(cornerRadius: 20.0)
                .fill()
                .foregroundColor(.red)
            Text("🏴")
                .font(.largeTitle)
        }
    }
}

加上點擊功能

// MARK: - CardView
struct CardView: View {
    
    @State private var isFaceUp = true      // 要加上@State(屬性包裝器)才可以改值 => struct => mutating

    var body: some View {
        
        let rectangle = RoundedRectangle(cornerRadius: 20.0)
        let text = Text("🏴")
        
        ZStack {
            
            /// 正面 => 顯示文字 / 背面 => 沒有文字
            if (!isFaceUp) {
                rectangle.fill()
            } else {
                rectangle.fill().foregroundColor(.white)
                rectangle.strokeBorder(lineWidth: 3.0, antialiased: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
                text.font(.largeTitle)
            }

        }.onTapGesture {                    // 點擊後 => isFaceUp = false
            isFaceUp.toggle()
        }
    }
}

讓Text能動態換文字

  • 為了讓每張牌都不一樣,所以Text的文字要是可以更換的,所以加上『var content: String』。
import SwiftUI

struct ContentView: View {
    
    let emojis = [
        "🏳️", "🏴", "🏴‍☠️", "🏁", "🚩",
        "🏳️‍🌈", "🏳️‍⚧️", "🇺🇳", "🇹🇹", "🇹🇷",
        "🇹🇨", "🇹🇲", "🇧🇹", "🇨🇫", "🇨🇳",
        "🇩🇰", "🇪🇨", "🇪🇷", "🇵🇬", "🇧🇷",
        "🇧🇧", "🇵🇾", "🇧🇭", "🇧🇸", "🇵🇦",
    ]
    
    var body: some View {
        CardView(content: emojis[5])
    }
}

// MARK: - CardView
struct CardView: View {

    var content: String                     // 加上一個String變數,讓Text能動態換文字
    
    @State private var isFaceUp = true      // 要加上@State(屬性包裝器)才可以改值 => struct => mutating
    
    var body: some View {
        
        let rectangle = RoundedRectangle(cornerRadius: 20.0)
        let text = Text(content)
        
        ZStack {
            
            /// 正面 => 顯示文字 / 背面 => 沒有文字
            if (!isFaceUp) {
                rectangle.fill()
            } else {
                rectangle.fill().foregroundColor(.white)
                rectangle.strokeBorder(lineWidth: 3.0, antialiased: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
                text.font(.largeTitle)
            }

        }.onTapGesture {                    // 點擊後 => isFaceUp = false
            isFaceUp.toggle()
        }
    }
}

重複使用CardView

  • 使用ForEach迴圈來重複使用CardView。
  • 要注意的是,要傳入參數 id來當成它的唯一值。
  • 可以試試看,如果把emojis內的文字都設定成同一個值,會有什麼反應?
import SwiftUI

struct ContentView: View {
    
    let emojis = [
        "🏳️", "🏴", "🏴‍☠️", "🏁", "🚩",
        "🏳️‍🌈", "🏳️‍⚧️", "🇺🇳", "🇹🇹", "🇹🇷",
        "🇹🇨", "🇹🇲", "🇧🇹", "🇨🇫", "🇨🇳",
        "🇩🇰", "🇪🇨", "🇪🇷", "🇵🇬", "🇧🇷",
        "🇧🇧", "🇵🇾", "🇧🇭", "🇧🇸", "🇵🇦",
    ]
    
    var body: some View {
        
        VStack {
            HStack {
                
                CardView(content: emojis[1])
                CardView(content: emojis[2])
                CardView(content: emojis[3])
                
                ForEach(emojis[4...6], id: \.self) { emoji in
                    CardView(content: emoji)
                }
            }
        }.foregroundColor(.red)
    }
}

把所有的CardView全部都排列上去

  • 把所有的CardView全部都排列上去,利用LazyVGrid組合一下。
import SwiftUI

struct ContentView: View {

    let emojis = [
        "🏳️", "🏴", "🏴‍☠️", "🏁", "🚩",
        "🏳️‍🌈", "🏳️‍⚧️", "🇺🇳", "🇹🇹", "🇹🇷",
        "🇹🇨", "🇹🇲", "🇧🇹", "🇨🇫", "🇨🇳",
        "🇩🇰", "🇪🇨", "🇪🇷", "🇵🇬", "🇧🇷",
        "🇧🇧", "🇵🇾", "🇧🇭", "🇧🇸", "🇵🇦",
    ]

    var body: some View {
        
        VStack {
            HStack {
                
                LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {               // LazyVGrid => 一列 => 三個 (100是測出來的)
                    
                    ForEach(emojis[0..<3], id: \.self) { emoji in
                        CardView(content: emoji).aspectRatio(2/3, contentMode: .fit)    // 長相比例:2:3 => 按比例填滿
                    }
                }
                    
            }.foregroundColor(.red)
        }
    }
}

超出範圍怎麼辦?

  • 超出範圍怎麼辦?利用ScrollView來處理這個問題。

import SwiftUI

struct ContentView: View {
    
    let emojis = [
        "🏳️", "🏴", "🏴‍☠️", "🏁", "🚩",
        "🏳️‍🌈", "🏳️‍⚧️", "🇺🇳", "🇹🇹", "🇹🇷",
        "🇹🇨", "🇹🇲", "🇧🇹", "🇨🇫", "🇨🇳",
        "🇩🇰", "🇪🇨", "🇪🇷", "🇵🇬", "🇧🇷",
        "🇧🇧", "🇵🇾", "🇧🇭", "🇧🇸", "🇵🇦",
    ]
    
    var body: some View {
        
        VStack {
            HStack {
                
                ScrollView {                                                                // 超出範圍怎麼辦? => ScrollView
                    LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {               // LazyVGrid => 一列 => 三個 (100是測出來的)
                        
                        ForEach(emojis[0..<emojis.count], id: \.self) { emoji in
                            CardView(content: emoji).aspectRatio(2/3, contentMode: .fit)    // 長相比例:2:3 => 按比例填滿
                        }
                    }

                }.foregroundColor(.red)
            }
        }
    }
}

加上新增 / 刪除的Button

  • 最後再加上兩個功能Button,其中ICON是使用先Apple提供的APP - SF Symbols 2來處理的。
import SwiftUI

struct ContentView: View {
    
    @State var emojiCount = 12                                                           // 要加上@State(屬性包裝器)才可以改值 => struct => mutating
    
    let emojis = [
        "🏳️", "🏴", "🏴‍☠️", "🏁", "🚩",
        "🏳️‍🌈", "🏳️‍⚧️", "🇺🇳", "🇹🇹", "🇹🇷",
        "🇹🇨", "🇹🇲", "🇧🇹", "🇨🇫", "🇨🇳",
        "🇩🇰", "🇪🇨", "🇪🇷", "🇵🇬", "🇧🇷",
        "🇧🇧", "🇵🇾", "🇧🇭", "🇧🇸", "🇵🇦",
    ]
    
    var body: some View {
        
        VStack {
            HStack {
                
                ScrollView {                                                               // 超出範圍怎麼辦? => ScrollView
                    LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) {               // LazyVGrid => 一列 => 四個 (80是測出來的)
                        
                        ForEach(emojis[0..<emojiCount], id: \.self) { emoji in
                            CardView(content: emoji).aspectRatio(2/3, contentMode: .fit)    // 長相比例:2:3 => 按比例填滿
                        }
                    }
                        
                }.foregroundColor(.red)
            }
            
            Spacer()                                                                        // 中間的空白
            
            HStack {
                removeButton                                                                // 移除用的Button
                Spacer()
                addButton                                                                   // 新增用的Button
            }
            .padding(.horizontal)
            .font(.largeTitle)
        }
    }

    /// 移除用的Button
    var removeButton: some View {
        
        /// 第一個的action:可以不寫 => label:
        Button {
            if (emojiCount > 1) { emojiCount -= 1 }                                         // 最少有一個
        } label: {
            VStack {
                Image(systemName: "minus.circle")                                           // SF Symbols 2 => 上面圖示的名字
            }
        }
    }
    
    /// 新增用的Button
    var addButton: some View {
            
        Button(action: {
            if (emojiCount < emojis.count) { emojiCount += 1 }                              // 最多就是emojis的量
        }, label: {
            VStack {
                Image(systemName: "plus.circle")                                            // SF Symbols 2 => 上面圖示的名字
            }
        })
    }
}

範例程式碼下載

後記

  • SwiftUI用起來的確是滿精簡的,不過在想法,給我的感覺上比較像CSS,也許有朝一日會用上吧?先有個基本的認識。