如何在swiftUI中正确使用MVVM?

How to use MVVM correctly in swiftUI?

我正在学习 SwiftUI 并尝试实现 MVVM 架构。这个想法很简单,我尝试将照片添加到一个列表中,该列表会根据所选的工作日进行更改。照片保存在本地

但是当我使用MVVM架构时,整个情况变得复杂了。特别是如果您想分别保存工作日的每个时间表。因为每个工作日都是一个单独的数组。我确信有一种更简单的方法来执行下面的代码。但是我没弄明白。

模特:

import Foundation

struct Activity: Identifiable, Codable {
  var id = UUID()
  var image: String
  var name: String
    
}

视图模型:

import Foundation
import UIKit

class Activities: ObservableObject {
    
    //MARK:- PROPERTIES:
    
    var indoorActivities = [Activity]()
    var outdoorActivities = [Activity]()
    let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path
    
    //Use it as example in RowView preview
    var exampleAct: [Activity] {
        return [indoorActivities[0], indoorActivities[1]]
    }
    
    @Published  var sundayActivities = [Activity]() {
        didSet {
            print("Change hapeend to sundayAcitivty")
            let encoder = JSONEncoder()
            if let data = try? encoder.encode(sundayActivities) {
                UserDefaults.standard.set(data, forKey: "sunday")
                
            }
        }
    }
    @Published var mondayActivities = [Activity]() {
        didSet {
            let encoder = JSONEncoder()
            if let data = try? encoder.encode(mondayActivities) {
                UserDefaults.standard.set(data, forKey: "monday")
                
            }
        }
    }
    @Published var tuesdayActivities = [Activity]() {
        didSet {
            let encoder = JSONEncoder()
            if let data = try? encoder.encode(tuesdayActivities) {
                UserDefaults.standard.set(data, forKey: "tuesday")
                
            }
        }
    }
    @Published var wednesdayActivities = [Activity]() {
        didSet {
            let encoder = JSONEncoder()
            if let data = try? encoder.encode(wednesdayActivities) {
                UserDefaults.standard.set(data, forKey: "wednesday")
                
            }
        }
    }
    @Published var thursdayActivities = [Activity]() {
        didSet {
            let encoder = JSONEncoder()
            if let data = try? encoder.encode(thursdayActivities) {
                UserDefaults.standard.set(data, forKey: "thursday")
                
            }
        }
    }
    @Published var fridayActivities = [Activity]() {
        didSet {
            let encoder = JSONEncoder()
            if let data = try? encoder.encode(fridayActivities) {
                UserDefaults.standard.set(data, forKey: "friday")
                
            }
        }
    }
    @Published var saturdayActivities = [Activity]() {
        didSet {
            let encoder = JSONEncoder()
            if let data = try? encoder.encode(saturdayActivities) {
                UserDefaults.standard.set(data, forKey: "saturday")
                
            }
        }
    }
    
   //MARK:- INIT:
    
    init() {
        if let data = UserDefaults.standard.data(forKey: "sunday") {
            let decoder = JSONDecoder()
            if let sunday = try? decoder.decode([Activity].self, from: data) {
                self.sundayActivities = sunday

            }
        } else {
            self.sundayActivities = []
        }
        
        if let data = UserDefaults.standard.data(forKey: "monday") {
            let decoder = JSONDecoder()
            if let monday = try? decoder.decode([Activity].self, from: data) {
                self.mondayActivities = monday
            }
        } else {
            self.mondayActivities = []
        }
        
        if let data = UserDefaults.standard.data(forKey: "tuesday") {
            let decoder = JSONDecoder()
            if let monday = try? decoder.decode([Activity].self, from: data) {
                self.tuesdayActivities = monday
            }
        } else {
            self.tuesdayActivities = []
        }
        
        if let data = UserDefaults.standard.data(forKey: "wednesday") {
            let decoder = JSONDecoder()
            if let monday = try? decoder.decode([Activity].self, from: data) {
                self.wednesdayActivities = monday
            }
        } else {
            self.wednesdayActivities = []
        }
        
        if let data = UserDefaults.standard.data(forKey: "thursday") {
            let decoder = JSONDecoder()
            if let monday = try? decoder.decode([Activity].self, from: data) {
                self.thursdayActivities = monday
            }
        } else {
            self.thursdayActivities = []
        }
        
        if let data = UserDefaults.standard.data(forKey: "friday") {
            let decoder = JSONDecoder()
            if let monday = try? decoder.decode([Activity].self, from: data) {
                self.fridayActivities = monday
            }
        } else {
            self.fridayActivities = []
        }
        
        if let data = UserDefaults.standard.data(forKey: "saturday") {
            let decoder = JSONDecoder()
            if let monday = try? decoder.decode([Activity].self, from: data) {
                self.saturdayActivities = monday
            }
        } else {
            self.saturdayActivities = []
        }
        
        
        
        if let urls = Bundle.main.urls(forResourcesWithExtension: "jpg", subdirectory: "/activities/indoorActivities") {
            
            for url in urls {
                
                let path = "//activities/indoorActivities" + "/\(url.lastPathComponent)"
                let name = url.deletingPathExtension().lastPathComponent
                let activity = Activity(image: path, name: name)
                indoorActivities.append(activity)
               
            }
        }
        
        //Get the documnet directory
        // attach the image path to the document direcotroy
        // assign it to image
        
        
        if let urls = Bundle.main.urls(forResourcesWithExtension: "jpg", subdirectory: "/activities/outdoorActivities") {
            
            for url in urls {
                
                let path = "//activities/outdoorActivities" + "/\(url.lastPathComponent)"
                let name = url.deletingPathExtension().lastPathComponent
                let activity = Activity(image: path, name: name)
                outdoorActivities.append(activity)
               
            }
        }
        
    }
    
    // Get UIImage from a url path
    class func getImage(image: String) -> UIImage {
        
        
        let bundle = Bundle.main.bundlePath
        if let uiImage = UIImage(contentsOfFile: bundle + image) {
            return uiImage
        }
        return UIImage(systemName: "circle.fill")!
    }
    
}

观看次数:

每日生活视图:

import SwiftUI

struct DailyLifeView: View {
    
    @EnvironmentObject var activities: Activities
    @State private var weekDay = "Sun"
    @State private var showActivites = false
    @State private var showDeleteAllButton = false
     var weekDays = ["Sun", "Mon", "Tue", "Wed", "Thur", "Fri", "Sat"]
  
   
    var body: some View {
        
        NavigationView {
            ZStack {
                VStack (spacing: 20){
                    Text("Choose the daily activities for your child")
                        .font(.headline)
                        .fontWeight(.bold)
                        .foregroundColor(.secondary)
                    Picker("Weeks", selection: $weekDay) {
                        ForEach(weekDays, id: \.self) {
                            Text([=12=])
                        }
                    }
                    .pickerStyle(SegmentedPickerStyle())
                    
                    if showDeleteAllButton {
                        Button(action: {
                            withAnimation {
                                deleteAll()
                            }
                            
                            
                        }, label: {
                            
                            HStack {
                                Text("Delete All")
                                    
                                Image(systemName: "trash")
                            }
                            .font(.headline)
                            .foregroundColor(.white)
                        })
                        .padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20))
                        .background(Color.red)
                        .cornerRadius(10)
                        .transition(.scale)
                        
                    }
                  
                    
                    
                    
                    List {
                        switch weekDay {
                        case "Sun":
                            RowView(activities: $activities.sundayActivities)
                                
                        case "Mon":
                            RowView(activities: $activities.mondayActivities)
                        case "Tue":
                            RowView(activities: $activities.tuesdayActivities)
                        case "Wed":
                            RowView(activities: $activities.wednesdayActivities)
                        case "Thur":
                            RowView(activities: $activities.thursdayActivities)
                        case "Fri":
                            RowView(activities: $activities.fridayActivities)
                        case "Sat":
                            RowView(activities: $activities.saturdayActivities)
                        default:
                            RowView(activities: $activities.sundayActivities)
                        }
                    }
                    .listStyle(InsetListStyle())
                    
                    
                    Spacer()
                    
                }
                .padding()
                VStack{
                    Spacer()
                    HStack{
                        Spacer()
                        Button(action: {showActivites.toggle()}, label: {
                            Text("+")
                                .font(.system(.largeTitle))
                                .frame(width: 77, height: 70)
                                .foregroundColor(Color.white)
                                .padding(.bottom, 7)
                            
                        })
                        .background(Color(hex: "64B5F6"))
                        .cornerRadius(38.5)
                        .padding()
                        .shadow(color: Color.black.opacity(0.3),
                                radius: 3,
                                x: 3,
                                y: 3)
                        .sheet(isPresented: $showActivites, content: {
                            ActivitiesList(weekDay: self.weekDay)
                        })
                    }
                }
            }
            .navigationViewStyle(DefaultNavigationViewStyle())
            .navigationBarTitle("Daily Life")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarItems(
                trailing: Button(action: {
                    withAnimation {
                        showDeleteAllButton.toggle()

                    }
                    
                }, label: {
                    Text(showDeleteAllButton ? "Done" : "Edit")
                })
            
            )
        }
    }
    func deleteAll() {
        switch weekDay {
        case "Sun":
            activities.sundayActivities.removeAll()
        case "Mon":
            activities.mondayActivities.removeAll()
        case "Tue":
            activities.tuesdayActivities.removeAll()
        case "Wed":
            activities.wednesdayActivities.removeAll()
        case "Thur":
            activities.thursdayActivities.removeAll()
        case "Fri":
            activities.fridayActivities.removeAll()
        case "Sat":
            activities.saturdayActivities.removeAll()
        default:
            activities.sundayActivities.removeAll()
        }
        
        
    }
}

活动列表:

import SwiftUI



struct ActivitiesList: View {
    
    @Environment(\.presentationMode) var presentationMode
    @EnvironmentObject var activities: Activities
    var weekDays = ["Sun", "Mon", "Tue", "Wed", "Thur", "Fri", "Sat"]
    var weekDay: String = "Sun"
    let columns = [
        GridItem(.adaptive(minimum: 100), spacing: 20)
    ]
    
    
    var body: some View {
        ScrollView {
            LazyVGrid(
                columns: columns,
                spacing: 30
//                pinnedViews: [.sectionHeaders]
            ) {
                Section(
                    header: Text("INDOOR ACTIVITIES")
                        .font(.title)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                
                ) {
                    ForEach(activities.indoorActivities) { activity in
                        Button(action: {
                            self.selectWeekDay(activity: activity)
                            print(activity.name)
                            self.presentationMode.wrappedValue.dismiss()
                            
                        }, label: {
                            Image(uiImage: Activities.getImage(image: activity.image))
                                .resizable()
                                .scaledToFit()
                                .cornerRadius(10)
//
                        })
                        
                            
                    }
                }
                Section(
                    header: Text("OUTDOOR ACTIVITIES")
                        .font(.title)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                
                ) {
                    ForEach(activities.outdoorActivities) { activity in
                        Button(action: {
                            self.activities.sundayActivities.append(activity)
                            print(activity.name)
                            self.presentationMode.wrappedValue.dismiss()
                        }, label: {
                            Image(uiImage: Activities.getImage(image: activity.image))
                                .resizable()
                                .scaledToFit()
                                .cornerRadius(10)
                              
                        })
                    }
                }
            }
            .padding()
        }
        .background(Color(hex: "90CAF9"))
        
       
    }
    
    //MARK: - FUNCTIONS:
    
 
    
    func selectWeekDay(activity: Activity) {
        switch weekDay {
        case "Sun":
            self.activities.sundayActivities.append(activity)
        case "Mon":
            self.activities.mondayActivities.append(activity)
        case "Tue":
            self.activities.tuesdayActivities.append(activity)
        case "Wed":
            self.activities.wednesdayActivities.append(activity)
        case "Thur":
            self.activities.thursdayActivities.append(activity)
        case "Fri":
            self.activities.fridayActivities.append(activity)
        case "Sat":
            self.activities.saturdayActivities.append(activity)


        default:
            self.activities.sundayActivities.append(activity)

        }
    }
}

行视图:

import SwiftUI
import AVFoundation

struct RowView: View {
    
    @Binding var activities: [Activity]
    
    var body: some View {
        ForEach(activities) { activity in
            Button(action: {
                let Synth = AVSpeechSynthesizer()
                let utterance = AVSpeechUtterance(string: activity.name)
                Synth.speak(utterance)
                
                
                
            }, label: {
                HStack {
                    Image(uiImage: Activities.getImage(image: activity.image))
                        .resizable()
                        .scaledToFit()
                        .cornerRadius(10)
                        

                    Spacer()
                     
                    Text(activity.name)
                        .font(.title3)
                        .fontWeight(.bold)
                        .foregroundColor(.white)
                }
                .padding()
                .background(Color(hex: "90CAF9"))
                .frame(height: 100)
                .cornerRadius(20)
                .shadow(
                    color: Color.black.opacity(0.3), radius: 3, x: 3, y: 3)
                
               
            })
           
        }
        .onDelete(perform: removeItems)
        
        
       
        
    }
    
    func getImage(image: String) -> UIImage {
        if let uiImage = UIImage(contentsOfFile: image) {
            return uiImage
        }
        return UIImage(systemName: "circle.fill")!
    }
    
     func removeItems(at offsets: IndexSet) {

        activities.remove(atOffsets: offsets)

    }
    
     
}

抱歉附加了很长的代码,但除非您看到视图的完整代码,否则我无法解释这个问题。

所以 MVVM 的第一条规则是,您应该始终尽可能多地从视图中分离逻辑。您应该在视图中拥有的唯一逻辑是处理您的视图本身的逻辑,仅此而已。其他所有内容都应保留在 ViewModel 本身中。

查看

struct YourView: View {
     
     @ObservedObject yourViewModel = YourViewModel()
     
     var body: some View {
          Text("Hello, \(yourViewModel.firstName)")
     }
}

查看模型

class YourViewModel: ObservableObject {
     @Published firstName = "John"
}

这是 MVVM 架构的基本结构。在您的视图中,您将始终引用 viewModel yourViewModel 及其属性。您还可以将它们作为绑定访问,例如 $yourViewModel.firstName 尽管在本示例中甚至无法编译,但您应该明白这一点。您还应该寻求扩展您的视图模型并清理一些代码。首先,只要你有可重用的代码,就创建一个函数。

示例函数

将这些函数添加到您的视图模型中。

func setUserDefaultActivity(activity: [Activity], activityKey: String) {
    let encoder = JSONEncoder()
        if let data = try? encoder.encode(activity) {
            UserDefaults.standard.set(data, forKey: activityKey)  
        }
}

func getUserDefaultActivity(activityKey: String) -> [Activity] {
    if let data = UserDefaults.standard.data(forKey: activityKey) {
        let decoder = JSONDecoder()
        if let activity = try? decoder.decode([Activity].self, from: data) {
             return activity
        }
    } else {
        return []
    }
}

用法示例

您可以在任何地方使用这些创建的函数。

//Setting your Defaults
@Published  var sundayActivities = [Activity]() {
        didSet {
            setUserDefaultActivity(sundayActivities, "sunday")
        }
    }

//Retrieving your Defaults
init() {
    var activityKeys = ["sunday", "monday", "tuesday" /* ... etc */ ]
    for key in activityKeys {
         switch key {
              case "sunday":
                  sundayActivities = getUserDefaultActivity(key)
              case "monday":
                  mondayActivities = getUserDefaultActivity(key)
              case "tuesday":
                  tuesdayActivities = getUserDefaultActivity(key)
              default:
                  //Continue to add all the days, have a default to cover all cases. 
         }
    }
}

有无数其他方法可以完成同样的任务,使它更简洁,但我不想给你太多选择。这里的主要收获是您可以清理相当多的代码并使事情更易于管理。请注意,我在我的示例中用 20 行与您的 100 多行完成了相同的事情。这是随着时间和实践而来的,所以不要太心烦意乱,继续学习,它会来的。关于您的选择器,将与视图相关的所有数据放入您的 ViewModel 中,包括您的数组。视图中唯一的内容应该是您对 ViewModel 本身的引用。请注意,在上面的 View Example 中,我只引用了 yourViewModel 并且数据保存在 yourViewModel class 中。这就是你需要的分离。此资源可能有助于更多地了解选择器以及它如何与您的视图模型啮合。

很遗憾,您的问题还不是很清楚。另一条建议,尤其是关于 Stack Overflow 的建议,是学会提出更好的问题。 10 次中有 9 次,如果你找不到答案,那是因为你没有问正确的问题。我向你保证,开发人员的大部分工作就是提出正确的问题。祝你好运!