如何在 swiftUI 中正确使用 MVVM? [英] How to use MVVM correctly in swiftUI?

查看:35
本文介绍了如何在 swiftUI 中正确使用 MVVM?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm learning SwiftUI and tried to implement the MVVM architecture. The idea is simple, I tried to add photo to a list which it changes based on the weekday selected. The photos are locally saved.

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

However When I used the MVVM architecture, it complicated the whole situation. Specially if you want to save each schedule of the weekday separately. Because each weekday is a separate array. I'm sure there is a much easier way to do the code below. But I didn't figure it out.

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($0)
                        }
                    }
                    .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)

    }
    
     
}

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

Sorry for the long code attached, but I couldn't explain the issue unless you see the full code of the view.

推荐答案

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

So the first rule of MVVM is that you should ALWAYS separate as much logic from the view as humanly possible. The only logic you should have in the view, is the logic to handle your view itself, nothing more. Everything else should be kept in the ViewModel itself.

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 虽然在本示例中甚至无法编译,但您应该明白这一点.您还应该考虑扩展您的视图模型并清理一些代码.首先,只要你有可重用的代码,就创建一个函数.

This is the basic structure for an MVVM architechture. Inside of your view you'll always reference the viewModel, yourViewModel, and its properties. You can also access them as a binding, eg $yourViewModel.firstName although in this example that won't even compile, but you should get the point there. You should also be looking to expand your view model and clean up some code. Firstly, anytime you have reusable code, make a function.

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

Add these functions to your view model.

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 []
    }
}

示例用法

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

Example Usage

Everywhere you can use those created functions.

//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 类中.这就是你需要的分离.此资源可能有助于更多地了解选择器以及它如何与您的视图模型啮合.

There are a myriad of other ways to accomplish this same task that make it more concise but I don't want to overload you with options. The main takeaway here is that you can clean-up quite a bit of your code and make things much easier to manage. Notice that I accomplished the same thing in my examples in 20 lines vs your 100+ lines. This comes with time and practice, so don't be too distraught, continue to learn and it will come. Regarding your picker, put any and all data related to the views inside of your ViewModel, including your arrays. The only thing on the view should be your reference to your ViewModel itself. Notice in the View Example above, I'm only referencing yourViewModel and the data is kept in the yourViewModel class. That's the separation you need to have. This resource may help some more with understanding of the picker and how it meshes with your View Model.

SwiftUI Picker onChange 或等效物?

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

Unfortunately it's still not entirely clear what your problem is. Another bit of advice, particularly on Stack Overflow, is to learn to ask better questions. 9 of 10 times, if you can't find an answer, it's because you're not asking the right question. I promise you, most of a developer's job is to ask the right questions. Good luck!

这篇关于如何在 swiftUI 中正确使用 MVVM?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆