1

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.

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.

The model :

import Foundation

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

Viewmodel :

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")!
    }
    
}

Views :

DailyLifeView :

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()
        }
        
        
    }
}

ActivitiesList :

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)

        }
    }
}

RowView:

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.

xTwisteDx
  • 2,152
  • 1
  • 9
  • 25
Usefz89
  • 53
  • 5
  • 1
    Ok, first, welcome to Stack Overflow. The first thing that stands out to me is that the code appears much more complex than it really is. Make use of functions to remove all of that code duplication, which in turn will make it much easier to read. I'll attempt to answer in a way that makes a bit more sense. However it's going to be lengthy so bear with me a moment. What exactly is the issue that you're having? Is it in the View Model? The View? What are you trying to do, simplify it for me. – xTwisteDx May 14 '21 at 18:27
  • @xTwisteDx Thanks for your reply, The issue I have is as you said, the complexity and the repeated code to do a simple task. I'll be appreciated if you show me how to implement the same task using MVVM architecture without all the repeated code I'm using because of the seven arrays I have, which represent the weekdays. – Usefz89 May 14 '21 at 18:36
  • @xTwisteDx What I'm trying to do is to select a day of a week from a segmented picker which I can add a Photo to it ( Which is a structure type of the photo name and path). So each weekday will show the "Activity" or the photo added to it even if the app has been shut down and reopen again. I hope I explained clear enough the object of the app or the view that I attached on the post. Thanks for your time. – Usefz89 May 14 '21 at 19:00

1 Answers1

6

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.

View

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

View Model

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

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.

Example Functions

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. 
         }
    }
}

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 or equivalent?

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!

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
xTwisteDx
  • 2,152
  • 1
  • 9
  • 25
  • Thanks again for the reply. For the view it's clear thanks to your answer what type of code suppose to be inside. However the confusion I still have is which code and where to add between the viewmodel and the model itself. For example, I added the arrays variables of the weekdays inside the viewmodel instead of the model in order to add and get the defaultKey. But what I understand before that all the data has to be in the model.. For the example functions, I will add them to the code, also I will check the link for the Picker. Again thanks for your effort It was really helpful. – Usefz89 May 14 '21 at 19:52