Home > Software engineering >  Refactor GeometryReader code extracted to subview in SwiftUI
Refactor GeometryReader code extracted to subview in SwiftUI

Time:09-27

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

CodePudding user response:

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)
        }
    }
}
  • Related