SwiftUI基礎教程(3) 第二章:SwiftUI基礎元素實例

本文是SwiftUI基礎教程基礎教程的第二部分,第一部分參見SwiftUI基礎教程(1)
本文是SwiftUI基礎教程基礎教程的第二部分,第一部分參見SwiftUI基礎教程(2)

第二章:SwiftUI基礎元素實例

2.14 Combine組件基本應用1

import Foundation
import Combine

// SettingStore.swift

// Int 是值DisplayOrderType的基礎值
// CaseIterable 是使DisplayOrderType具有迭代能力
enum DisplayOrderType: Int, CaseIterable {
    case alphabetical = 0
    case favoriteFirst = 1
    case checkInFirst = 2
        
    // 枚舉在Swift中是一級對象,可以具有成員函數
    init(type: Int) {
        switch type {
        case 0: self = .alphabetical
        case 1: self = .favoriteFirst
        case 2: self = .checkInFirst
        default: self = .alphabetical
        }
    }
  
    var text: String {
        switch self {
        case .alphabetical: return "Alphabetical"
        case .favoriteFirst: return "Show Favorite First"
        case .checkInFirst: return "Show Check-in First"
        }
    }
    
    // 在排序時會自動調用
    func predicate() -> ((ATankInfoForm, ATankInfoForm) -> Bool) {
        switch self {
        case .alphabetical: return { $0.name < $1.name }
        case .favoriteFirst: return { $0.isFavorite && !$1.isFavorite }
        case .checkInFirst: return { $0.isCheckIn && !$1.isCheckIn }
        }
    }
}

// ObservableObject使SettingStore能夠在發生變化時被監聽到
final class SettingStore: ObservableObject {
    init() {
        // 這個是默認值,在程序中第一次能夠被執行
        UserDefaults.standard.register(defaults: [
            "view.preferences.showCheckInOnly" : false,
            "view.preferences.displayOrder" : 0,
            "view.preferences.maxPriceLevel" : 5
        ])
    }
    
    // 添加@Published的屬性能夠在變化時被其他對象感知
    @Published var showCheckInOnly: Bool = UserDefaults.standard.bool(forKey: "view.preferences.showCheckInOnly") {
        // 類似OC的KVO,在屬性被賦值時自動執行
        didSet {
            UserDefaults.standard.set(showCheckInOnly, forKey: "view.preferences.showCheckInOnly")
        }
    }
    
    @Published var displayOrder: DisplayOrderType = DisplayOrderType(type: UserDefaults.standard.integer(forKey: "view.preferences.displayOrder")) {
        didSet {
            UserDefaults.standard.set(displayOrder.rawValue, forKey: "view.preferences.displayOrder")
        }
    }
    
    @Published var maxPriceLevel: Int = UserDefaults.standard.integer(forKey: "view.preferences.maxPriceLevel") {
        didSet {
            UserDefaults.standard.set(maxPriceLevel, forKey: "view.preferences.maxPriceLevel")
        }
    }
}
struct SettingView: View {
    // 類似全局變量,可以在app範圍內訪問
    @Environment(\.presentationMode) var presentationMode
    
    @State private var selectedOrder = DisplayOrderType.alphabetical
    @State private var showCheckInOnly = false
    @State private var maxPriceLevel = 5
    
    @EnvironmentObject var settingStore: SettingStore
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("SORT PREFERENCE")) {
                    Picker(selection: $selectedOrder, label: Text("Display order")) {
                        ForEach(DisplayOrderType.allCases, id: \.self) {
                            orderType in
                            Text(orderType.text)
                        }
                    }
                }
                
                Section(header: Text("FILTER PREFERENCE")) {
                    Toggle(isOn: $showCheckInOnly) {
                        Text("Show Check-in Only")
                    }
                    
                    Stepper(onIncrement: {
                        self.maxPriceLevel += 1
                        
                        if self.maxPriceLevel > 5 {
                            self.maxPriceLevel = 5
                        }
                    }, onDecrement: {
                        self.maxPriceLevel -= 1
                        
                        if self.maxPriceLevel < 1 {
                            self.maxPriceLevel = 1
                        }
                    }) {
                        Text("Show \(String(repeating: "$", count: maxPriceLevel)) or below")
                    }
                }
            }
            
            
            .navigationBarTitle("Settings")
            
            .navigationBarItems(leading:
                
                Button(action: {
                    self.presentationMode.wrappedValue.dismiss()
                }, label: {
                    Text("Cancel")
                        .foregroundColor(.black)
                })
                
                , trailing:

                Button(action: {
                    self.settingStore.showCheckInOnly = self.showCheckInOnly
                    self.settingStore.displayOrder = self.selectedOrder
                    self.settingStore.maxPriceLevel = self.maxPriceLevel
                    self.presentationMode.wrappedValue.dismiss()
                    
                }, label: {
                    Text("Save")
                        .foregroundColor(.black)
                })
            )
            
        }
        .onAppear {
            self.selectedOrder = self.settingStore.displayOrder
            self.showCheckInOnly = self.settingStore.showCheckInOnly
            self.maxPriceLevel = self.settingStore.maxPriceLevel
        }
        
    }
}

struct SettingView_Previews: PreviewProvider {
    static var previews: some View {
        SettingView().environmentObject(SettingStore())
    }
}

struct SwiftUIForm: View {
    
    @State var tankInfos = [
        ATankInfoForm(name: "M1A2", image: "1", type:"美國", phone: "119", priceLevel: 3, isFavorite: true, isCheckIn: false),
        ATankInfoForm(name: "T90", image: "2", type:"俄羅斯", phone: "119", priceLevel: 2, isFavorite: false, isCheckIn: true),
        ATankInfoForm(name: "T72", image: "3", type:"俄羅斯", phone: "119", priceLevel: 2, isFavorite: false, isCheckIn: true),
        ATankInfoForm(name: "99A", image: "4", type:"俄羅斯", phone: "119", priceLevel: 2, isFavorite: false, isCheckIn: true),
        ATankInfoForm(name: "LieKeLieEr", image: "5", type:"德國", phone: "123", priceLevel: 5, isFavorite: true, isCheckIn: true),
        ATankInfoForm(name: "BaoEr", image: "6", type:"德國", phone: "123", priceLevel: 2, isFavorite: false, isCheckIn: true),
        ATankInfoForm(name: "09", image: "7", type:"日版", phone: "777", priceLevel: 1, isFavorite: true, isCheckIn: true),
        ATankInfoForm(name: "10", image: "8", type:"日本", phone: "911", priceLevel: 4, isFavorite: false, isCheckIn: false),
    ]
    
    @State private var selectedRestaurant: ATankInfoForm?
    @State private var showSettings: Bool = false
    
    @EnvironmentObject var settingStore: SettingStore
    
    var body: some View {
        NavigationView {
            List {
                ForEach(tankInfos.sorted(by: self.settingStore.displayOrder.predicate())) { aTankInfo in
                    if self.shouldShowItem(tankInfo: aTankInfo) {
                     
                        BasicImageRow(restaurant: aTankInfo)
                            .contextMenu {
                                
                                Button(action: {
                                    // mark the selected restaurant as check-in
                                    self.checkIn(item: aTankInfo)
                                }) {
                                    HStack {
                                        Text("Check-in")
                                        Image(systemName: "checkmark.seal.fill")
                                    }
                                }
                                
                                Button(action: {
                                    // delete the selected restaurant
                                    self.delete(item: aTankInfo)
                                }) {
                                    HStack {
                                        Text("Delete")
                                        Image(systemName: "trash")
                                    }
                                }
                                                 
                                Button(action: {
                                    // mark the selected restaurant as favorite
                                    self.setFavorite(item: aTankInfo)
                                    
                                }) {
                                    HStack {
                                        Text("Favorite")
                                        Image(systemName: "star")
                                    }
                                }
                            }
                            .onTapGesture {
                                self.selectedRestaurant = aTankInfo
                        }
                    }
                }
                .onDelete { (indexSet) in
                    self.tankInfos.remove(atOffsets: indexSet)
                }
            }
            
            .navigationBarTitle("Restaurant")
            .navigationBarItems(trailing:

                Button(action: {
                    self.showSettings = true
                }, label: {
                    Image(systemName: "gear").font(.title)
                        .foregroundColor(.black)
                })
            )
            .sheet(isPresented: $showSettings) {
                SettingView().environmentObject(self.settingStore)
            }
            
        }
        .navigationViewStyle(StackNavigationViewStyle())
       
    }
    
    private func delete(item restaurant: ATankInfoForm) {
        if let index = self.tankInfos.firstIndex(where: { $0.id == restaurant.id }) {
            self.tankInfos.remove(at: index)
        }
    }
    
    private func setFavorite(item restaurant: ATankInfoForm) {
        if let index = self.tankInfos.firstIndex(where: { $0.id == restaurant.id }) {
            self.tankInfos[index].isFavorite.toggle()
        }
    }
    
    private func checkIn(item restaurant: ATankInfoForm) {
        if let index = self.tankInfos.firstIndex(where: { $0.id == restaurant.id }) {
            self.tankInfos[index].isCheckIn.toggle()
        }
    }
    
    private func shouldShowItem(tankInfo: ATankInfoForm) -> Bool {
        return (!self.settingStore.showCheckInOnly || tankInfo.isCheckIn) && (tankInfo.priceLevel <= self.settingStore.maxPriceLevel)
    }
    

}

struct SwiftUIForm_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(SettingStore())
    }
}

struct BasicImageRow: View {
    var restaurant: ATankInfoForm
    
    var body: some View {
      
            HStack {
                Image(restaurant.image)
                    .resizable()
                    .frame(width: 60, height: 60)
                    .clipShape(Circle())
                    .padding(.trailing, 10)
                
                VStack(alignment: .leading) {
                    HStack {
                        Text(restaurant.name)
                            .font(.system(.body, design: .rounded))
                            .bold()
                        
                        Text(String(repeating: "$", count: restaurant.priceLevel))
                            .font(.subheadline)
                            .foregroundColor(.gray)

                    }
                    
                    Text(restaurant.type)
                        .font(.system(.subheadline, design: .rounded))
                        .bold()
                        .foregroundColor(.secondary)
                        .lineLimit(3)
                    
                    Text(restaurant.phone)
                        .font(.system(.subheadline, design: .rounded))
                        .foregroundColor(.secondary)
                }
                
                Spacer()
                    .layoutPriority(-100)
                
                if restaurant.isCheckIn {
                    Image(systemName: "checkmark.seal.fill")
                        .foregroundColor(.red)
                }
                
                if restaurant.isFavorite {
//                    Spacer()
                    
                    Image(systemName: "star.fill")
                    .foregroundColor(.yellow)
                }
            }
            
        
    }
}

struct ATankInfoForm: Identifiable {
    
    let id = UUID()
    var name: String
    var image: String
    var type: String
    var phone: String
    var priceLevel: Int
    var isFavorite: Bool = false
    var isCheckIn: Bool = false
}

2.15 Combine組件基本應用2

// 本代碼構建了一個用於註冊的UI,其中UI部分不做介紹,對於涉及Combine的做簡單介紹
struct SwiftUIRegister: View {
    // @ObservedObject 標明這個是一個可以被觀察的對象(別人修改它,這裏自動刷新UI)
    @ObservedObject private var userRegistrationViewModel = UserRegistrationViewModel()
    
    var body: some View {
        VStack {
            Text("Create an account")
                .font(.system(.largeTitle, design: .rounded))
                .bold()
                .padding(.bottom, 30)
            
            // $userRegistrationViewModel.username 標明把此處的值傳遞到其他的View
            FormField(fieldName: "Username", fieldValue: $userRegistrationViewModel.username)
            RequirementText(iconColor: userRegistrationViewModel.isUsernameLengthValid ? Color.secondary : Color(red: 251/255, green: 128/255, blue: 128/255), text: "A minimum of 4 characters", isStrikeThrough: userRegistrationViewModel.isUsernameLengthValid)
                .padding()
            
            FormField(fieldName: "Password", fieldValue: $userRegistrationViewModel.password, isSecure: true)
            VStack {
                RequirementText(iconName: "lock.open", iconColor: userRegistrationViewModel.isPasswordLengthValid ? Color.secondary : Color(red: 251/255, green: 128/255, blue: 128/255), text: "A minimum of 8 characters", isStrikeThrough: userRegistrationViewModel.isPasswordLengthValid)
                RequirementText(iconName: "lock.open", iconColor: userRegistrationViewModel.isPasswordCapitalLetter ? Color.secondary : Color(red: 251/255, green: 128/255, blue: 128/255), text: "One uppercase letter", isStrikeThrough: userRegistrationViewModel.isPasswordCapitalLetter)
            }
            .padding()
            
            FormField(fieldName: "Confirm Password", fieldValue: $userRegistrationViewModel.passwordConfirm, isSecure: true)
            RequirementText(iconColor: userRegistrationViewModel.isPasswordConfirmValid ? Color.secondary : Color(red: 251/255, green: 128/255, blue: 128/255), text: "Your confirm password should be the same as password", isStrikeThrough: userRegistrationViewModel.isPasswordConfirmValid)
                .padding()
                .padding(.bottom, 50)
            
            Button(action: {
                // Proceed to the next screen
            }) {
                Text("Sign Up")
                    .font(.system(.body, design: .rounded))
                    .foregroundColor(.white)
                    .bold()
                    .padding()
                    .frame(minWidth: 0, maxWidth: .infinity)
                    .background(LinearGradient(gradient: Gradient(colors: [Color(red: 251/255, green: 128/255, blue: 128/255), Color(red: 253/255, green: 193/255, blue: 104/255)]), startPoint: .leading, endPoint: .trailing))
                    .cornerRadius(10)
                    .padding(.horizontal)
                
            }
            
            HStack {
                Text("Already have an account?")
                    .font(.system(.body, design: .rounded))
                    .bold()
                
                Button(action: {
                    // Proceed to Sign in screen
                }) {
                    Text("Sign in")
                        .font(.system(.body, design: .rounded))
                        .bold()
                        .foregroundColor(Color(red: 251/255, green: 128/255, blue: 128/255))
                }
            }.padding(.top, 50)
            
            Spacer()
        }
        .padding()
    }
}

struct SwiftUIRegister_Previews: PreviewProvider {
    static var previews: some View {
        SwiftUIRegister()
    }
}

struct FormField: View {
    var fieldName = ""
    @Binding var fieldValue: String
    
    var isSecure = false
    
    var body: some View {
        
        VStack {
            if isSecure {
                SecureField(fieldName, text: $fieldValue)
                    .font(.system(size: 20, weight: .semibold, design: .rounded))
                    .padding(.horizontal)
                
            } else {
                TextField(fieldName, text: $fieldValue)                 .font(.system(size: 20, weight: .semibold, design: .rounded))
                    .padding(.horizontal)
            }

            Divider()
                .frame(height: 1)
                .background(Color(red: 240/255, green: 240/255, blue: 240/255))
                .padding(.horizontal)
            
        }
    }
}

struct RequirementText: View {
    
    var iconName = "xmark.square"
    var iconColor = Color(red: 251/255, green: 128/255, blue: 128/255)
    
    var text = ""
    var isStrikeThrough = false
    
    var body: some View {
        HStack {
            Image(systemName: iconName)
                .foregroundColor(iconColor)
            Text(text)
                .font(.system(.body, design: .rounded))
                .foregroundColor(.secondary)
                .strikethrough(isStrikeThrough)
            Spacer()
        }
    }
}
import Foundation
import Combine

class UserRegistrationViewModel: ObservableObject {
    //     @Published標明這個值的變化會被其他對象感知

    // Input
    @Published var username = ""
    @Published var password = ""
    @Published var passwordConfirm = ""
    
    // Output
    @Published var isUsernameLengthValid = false
    @Published var isPasswordLengthValid = false
    @Published var isPasswordCapitalLetter = false
    @Published var isPasswordConfirmValid = false
    
    private var cancellableSet: Set<AnyCancellable> = []
    
    init() {
        $username
            .receive(on: RunLoop.main) // 標明這個值的改變會被在主線程監聽
            .map { username in             // 接收到新的值之後要做的處理
                return username.count >= 4
            }
            .assign(to: \.isUsernameLengthValid, on: self)  // 根據收到的消息,對其他值進行修改,由於isUsernameLengthValid也是一個被訂閱對象,所以其改動會導致其他監聽被調用(此處是SwiftUIRegister被自動刷新)
            .store(in: &cancellableSet) // 爲了防止內存泄漏
        
        $password
            .receive(on: RunLoop.main)
            .map { password in
                return password.count >= 8
            }
            .assign(to: \.isPasswordLengthValid, on: self)
            .store(in: &cancellableSet)
        
        $password
            .receive(on: RunLoop.main)
            .map { password in
                let pattern = "[A-Z]"
                if let _ = password.range(of: pattern, options: .regularExpression) {
                    return true
                } else {
                    return false
                }
            }
            .assign(to: \.isPasswordCapitalLetter, on: self)
            .store(in: &cancellableSet)
        
        Publishers.CombineLatest($password, $passwordConfirm) // 同時監聽兩個值要怎麼辦
            .receive(on: RunLoop.main)
            .map { (password, passwordConfirm) in
                return !passwordConfirm.isEmpty && (passwordConfirm == password)
            }
            .assign(to: \.isPasswordConfirmValid, on: self)
            .store(in: &cancellableSet)
    }
}

        以上代碼的UI如下圖所示。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章