1

My App is working, but there is a lot of repetative code.

I can't figure out how to refactor the GeometryReader also how could I change the code using a ForEach to comply with MVVM Design Pattern. Last should this be put into a vertical ScrollView?

Any direction would be great to learn how to write cleaner Swift code.

import SwiftUI

struct ContentView: View {
    
    let colorFriendship = LinearGradient(colors: [Color("ColorFriendshipLight"), Color("ColorFriendshipDark")], startPoint: .topLeading, endPoint: .bottomTrailing)
    let colorWealth = LinearGradient(colors: [Color("ColorWealthLight"), Color("ColorWealthDark")], startPoint: .topLeading, endPoint: .bottomTrailing)
    let colorEducation = LinearGradient(colors: [Color("ColorEducationLight"), Color("ColorEducationDark")], startPoint: .topLeading, endPoint: .bottomTrailing)
    let colorCareer = LinearGradient(colors: [Color("ColorCareerLight"), Color("ColorCareerDark")], startPoint: .topLeading, endPoint: .bottomTrailing)
    let colorFamily = LinearGradient(colors: [Color("ColorFamilyLight"), Color("ColorFamilyDark")], startPoint: .topLeading, endPoint: .bottomTrailing)
    let colorHealth = LinearGradient(colors: [Color("ColorHealthLight"), Color("ColorHealthDark")], startPoint: .topLeading, endPoint: .bottomTrailing)
    let colorSpirituality = LinearGradient(colors: [Color("ColorSpiritualityLight"), Color("ColorSpiritualityDark")], startPoint: .topLeading, endPoint: .bottomTrailing)
    let colorCompose = LinearGradient(colors: [Color("ColorComposeLight"), Color("ColorComposeDark")], startPoint: .topLeading, endPoint: .bottomTrailing)
    
    // Shadow Icons
    private var radius = 7
    private var xOffset = 6
    private var yOffset = 6
    
    var body: some View {
        
        VStack {
            NavigationView {
                VStack {
                    HStack {
                        NavigationLink(destination: FriendshipListView()) {
                            VStack(alignment: .center) {
                                GeometryReader { geo in
                                    Image("menu-icon-friendship")
                                        .resizable()
                                        .scaledToFit()
                                        .frame(
                                            width: geo.size.width,
                                            height: geo.size.height)
                                        .shadow(color: Color("ColorCareerDark"), radius: CGFloat(radius), x: CGFloat(xOffset), y: CGFloat(yOffset))
                                }
                                Text("Friendships")
                                    .titleStyle()
                            }
                            .frame(maxWidth: .infinity, maxHeight: 110)
                            .padding(20)
                            .background(colorFriendship)
                            .cornerRadius(20)
                        }
                        NavigationLink(destination: WealthListView()) {
                            VStack(alignment: .center) {
                                GeometryReader { geo in
                                    Image("menu-icon-wealth")
                                        .resizable()
                                        .scaledToFit()
                                        .frame(
                                            width: geo.size.width,
                                            height: geo.size.height)
                                        .shadow(color: Color("ColorWealthDark"), radius: CGFloat(radius), x: CGFloat(xOffset), y: CGFloat(yOffset))
                                }
                                Text("Wealth")
                                    .titleStyle()
                            }
                            .frame(maxWidth: .infinity, maxHeight: 110)
                            .padding(20)
                            .background(colorWealth)
                            .cornerRadius(20)
                        }
                    }
                    HStack {
                        NavigationLink(destination: EducationListView()) {
                            VStack(alignment: .center) {
                                GeometryReader { geo in
                                    Image("menu-icon-education")
                                        .resizable()
                                        .scaledToFit()
                                        .frame(
                                            width: geo.size.width,
                                            height: geo.size.height)
                                        .shadow(color: Color("ColorEducationDark"), radius: CGFloat(radius), x: CGFloat(xOffset), y: CGFloat(yOffset))
                                        
                                }
                                Text("Education")
                                    .titleStyle()
                            }
                            .frame(maxWidth: .infinity, maxHeight: 110)
                            .padding(30)
                            .background(colorEducation)
                            .cornerRadius(20)
                        }
                        NavigationLink(destination: CareerListView()) {
                            VStack(alignment: .center) {
                                GeometryReader { geo in
                                    Image("menu-icon-career")
                                        .resizable()
                                        .scaledToFit()
                                        .frame(
                                            width: geo.size.width,
                                            height: geo.size.height)
                                        .shadow(color: Color("ColorCareerDark"), radius: CGFloat(radius), x: CGFloat(xOffset), y: CGFloat(yOffset))
                                }
                                Text("Career")
                                    .titleStyle()
                            }
                            .frame(maxWidth: .infinity, maxHeight: 110)
                            .padding(30)
                            .background(colorCareer)
                            .cornerRadius(20)
                        }
                    }
                    HStack {
                        NavigationLink(destination: FamilyListView()) {
                            VStack(alignment: .center) {
                                GeometryReader { geo in
                                    Image("menu-icon-family")
                                        .resizable()
                                        .scaledToFit()
                                        .frame(
                                            width: geo.size.width,
                                            height: geo.size.height)
                                        .shadow(color: Color("ColorFamilyDark"), radius: CGFloat(radius), x: CGFloat(xOffset), y: CGFloat(yOffset))
                                }
                                Text("Family")
                                    .titleStyle()
                            }
                            .frame(maxWidth: .infinity, maxHeight: 110)
                            .padding(30)
                            .background(colorFamily)
                            .cornerRadius(20)
                        }
                        NavigationLink(destination: HealthListView()) {
                            VStack(alignment: .center) {
                                GeometryReader { geo in
                                    Image("menu-icon-health")
                                        .resizable()
                                        .scaledToFit()
                                        .frame(
                                            width: geo.size.width,
                                            height: geo.size.height)
                                        .shadow(color: Color("ColorHealthDark"), radius: CGFloat(radius), x: CGFloat(xOffset), y: CGFloat(yOffset))
                                }
                                Text("Health")
                                    .titleStyle()
                            }
                            .frame(maxWidth: .infinity, maxHeight: 110)
                            .padding(30)
                            .background(colorHealth)
                            .cornerRadius(20)
                        }
                    }
                    HStack {
                        NavigationLink(destination: SpiritualityListView()) {
                            VStack(alignment: .center) {
                                GeometryReader { geo in
                                    Image("menu-icon-spirituality")
                                        .resizable()
                                        .scaledToFit()
                                        .frame(
                                            width: geo.size.width,
                                            height: geo.size.height)
                                        .shadow(color: Color("ColorSpiritualityDark"), radius: CGFloat(radius), x: CGFloat(xOffset), y: CGFloat(yOffset))
                                }
                                Text("Spirituality")
                                    .titleStyle()
                            }
                            .frame(maxWidth: .infinity, maxHeight: 110)
                            .padding(30)
                            .background(colorSpirituality)
                            .cornerRadius(20)
                        }
                        NavigationLink(destination: ComposeListView()) {
                            VStack(alignment: .center) {
                                GeometryReader { geo in
                                    Image("menu-icon-compose")
                                        .resizable()
                                        .scaledToFit()
                                        .frame(
                                            width: geo.size.width,
                                            height: geo.size.height)
                                        .shadow(color: Color("ColorSpiritualityDark"), radius: CGFloat(radius), x: CGFloat(xOffset), y: CGFloat(yOffset))
                                }
                                Text("Compose")
                                    .titleStyle()
                            }
                            .frame(maxWidth: .infinity, maxHeight: 110)
                            .padding(30)
                            .background(colorCompose)
                            .cornerRadius(20)
                        }
                    }
                }
                .navigationTitle("Main Menu")
            }
        }
        .padding(.horizontal)
    }
    
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

// Custom modifiers

// MenuTitle
struct MenuTitle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.system(.title2, design: .rounded))
            .foregroundColor(.white)
            .fontWeight(.regular)
            .shadow(color: Color(.black), radius: 7, x: 2, y: 2)
    }
}

extension View {
    func titleStyle() -> some View {
        modifier(MenuTitle())
    }
}

// Struct List Views

struct FriendshipListView:View {
    var body: some View {
        Text("Friendship List")
    }
}

struct WealthListView:View {
    var body: some View {
        Text("Wealth List")
    }
}

struct EducationListView:View {
    var body: some View {
        Text("Education List")
    }
}

struct CareerListView:View {
    var body: some View {
        Text("Career List")
    }
}

struct FamilyListView:View {
    var body: some View {
        Text("Family List")
    }
}

struct HealthListView:View {
    var body: some View {
        Text("Health List")
    }
}

struct SpiritualityListView:View {
    var body: some View {
        Text("Spirituality List")
    }
}
struct ComposeListView:View {
    var body: some View {
        Text("Compose List")
    }
}

iPhone 11 Simulator Screenshot

vacawama
  • 150,663
  • 30
  • 266
  • 294

1 Answers1

1

To restructure this you would first need to create some sort of datamodel that holds the different informations related to each topic you want to display.

Disclaimer:

I´m not going to post a complete working example. So no need in asking for one. But I will try to point you in the correct direction.


First create an enum that defines all the different topics you want to display. I implemented only 2 cases and not all properties but the idea behind it should become clear.

enum Topic: CaseIterable, Identifiable{
    // all the topics
    case friendship, wealth
    
    // to conform to Identifiable
    var id: Topic { self }
    
    // create the background gradient
    var background: LinearGradient{
        switch self{
        case .friendship:
            return LinearGradient(colors: [Color("ColorFriendshipLight"), Color("ColorFriendshipDark")], startPoint: .topLeading, endPoint: .bottomTrailing)
        case .wealth:
            return LinearGradient(colors: [Color("ColorWealthLight"), Color("ColorWealthDark")], startPoint: .topLeading, endPoint: .bottomTrailing)
        }
    }
    
    // the image names
    var imagename: String{
        switch self{
        case .friendship:
            return "menu-icon-friendship"
        case .wealth:
            return "menu-icon-wealth"
        }
    }

    // here you create the target view. If the views need to get properties set, you can give your enum cases assosiated values and add them during initialization    
    @ViewBuilder
    var targetView: some View{
        switch self{
        case .friendship:
            FriendshipListView()
        case .wealth:
            WealthListView()
        }
    }
}

and using a LazyVGrid the Views become pretty simple:

struct ContentView: View{

    let columns = [GridItem(.flexible()), .init(.flexible())]
    
    var body: some View{
        NavigationView{
            ScrollView{
                LazyVGrid(columns: columns) {
                    // iterate over all topics
                    ForEach(Topic.allCases){ topic in
                    // create you reusable view and pass the data into it
                        CardView(topic: topic)
                    }
                }
            }
        }
    }
}

// this view will be reused for displayin the topics
struct CardView: View{
    
    let topic: Topic
    // Shadow Icons
    private let radius = 7
    private let xOffset = 6
    private let yOffset = 6
    
    var body: some View{
        NavigationLink(destination: topic.targetView) {
            VStack(alignment: .center) {
                // I don´t think you need the geometry reader here but 
                // I don´t know how your view will look without it
                GeometryReader { geo in
                    Image(topic.imagename)
                        .resizable()
                        .scaledToFit()
                        .frame(
                            width: geo.size.width,
                            height: geo.size.height)
                        .shadow(color: topic.shadowColor, radius: CGFloat(radius), x: CGFloat(xOffset), y: CGFloat(yOffset))
                        
                }
                Text(topic.title)
                    .titleStyle()
            }
            .frame(maxWidth: .infinity, maxHeight: 110)
            .padding(30)
            .background(topic.background)
            .cornerRadius(20)
        }
    }
}
burnsi
  • 6,194
  • 13
  • 17
  • 27
  • Thank you for your direction the code is light and lean. I like Enumeration very slick. I'm still trying to figure out how to code the enum for the icon shadow – Kraig Kistler Sep 27 '22 at 01:10
  • @KraigKistler well if you look at the view implementation it should become clear what type I used. If you get stuck don´t hesitate to ask. – burnsi Sep 27 '22 at 07:15
  • Here is the working solution I came up with. `var shadowColor: Color { switch self{ case .friendship: return Color("ColorFriendshipDark") case .wealth: return Color("ColorWealthLight") } }` – Kraig Kistler Sep 29 '22 at 22:46
  • @KraigKistler seems fine – burnsi Sep 29 '22 at 22:48
  • Would it be best practice to move my Enum's into a MainMenuViewModel.swift to conform to MVVM using ObservableObject to separate the data from the view. In addition, create two new views for the CardView and MenuTitle struct? Or is this not really necessary? – Kraig Kistler Sep 30 '22 at 23:51
  • 1
    @KraigKistler there is no MVVM here. It is all View stuff. There is no "logic" involved here. If there were e.g. network calls or core data stuff, than we could talk about MVVM. But until then this stuff can happily live in the View. To your View question: I don´t think the CardView can be modulized more. I would probably even try to generalize the destination Views to get that ViewBuilder code out of the enum. Then the enum would be just model. – burnsi Oct 01 '22 at 06:41