Search code examples
iosswiftxcodeswiftuigeometryreader

Is there a way to define the width of my SwiftUI Image to be relative to screen size using Geometry Reader?


I am working through a sample app to familiarize myself with Swift and SwiftUI and am wanting to combine the ideas of composite layouts using custom views and GeometryReader to determine the screen-size of the device.

The page I'm trying to build is a scrollable list of friends where each row item is a button that can be selected to expand the list to see more details of each friend. I've built the button and all containing views as a FriendView to use inside of my FriendListView. I have defined 2 GeometryReader views within the FriendListView but do not know how I can define the size of each row from within the FriendView to make it maintain the appropriate size.

The need for this comes from not every image that could be set by a user later on for their friend will have the same dimensions so I want to keep it consistent. I understand I could set a .frame() modifier and hardcode the width but I would like it to be relational to the screen to be consistent across all phone screens.

Example: When a row is not expanded, the image should take up half of the device width. When it is expanded I would want it to take up one quarter of the device screen width.

Screenshot

Application View

Sample Code

struct FriendListView: View {
    var pets = ["Woofer", "Floofer", "Booper"]

    var body: some View {
        GeometryReader { geo1 in
            NavigationView {
                GeometryReader { geo2 in
                    List(self.pets, id: \.self) { pet in
                            FriendView(name: pet)

                    }// End of List
                }
                .navigationBarTitle(Text("Furiends"))

            }// End of NavigationView
        }// End of GeometryReader geo1
    }// End of body
}
struct FriendView: View {
    @State private var isExpanded = false
    var randomPic = Int.random(in: 1...2)

    var name =  ""
    var breed = "Dawg"
    var body: some View {

            Button(action: self.toggleExpand) {
                HStack {
                    VStack {
                    Image("dog\(self.randomPic)")
                        .resizable()
                        .renderingMode(.original)
                        .scaledToFill()
                        .clipShape(Circle())

                       if self.isExpanded == false {
                        Text(self.name)
                            .font(.title)
                            .foregroundColor(.primary)
                       }
                   }

                   if self.isExpanded == true {
                       VStack {
                        Text(self.name).font(.title)
                        Text(self.breed).font(.headline)
                       }
                       .foregroundColor(.primary)
                   }
               }
            }// End of Button
    }
    func toggleExpand() {
        isExpanded.toggle()
    }
}

Solution

  • You can define a GeometryReader in the top level FriendListView and pass the screen width to the FriendView:

    struct FriendListView: View {
        var pets = ["Woofer", "Floofer", "Booper"]
    
        var body: some View {
            GeometryReader { geo in
                NavigationView {
                    List(self.pets, id: \.self) { pet in
                        FriendView(name: pet, screenWidth: geo.size.width)
                    }
                    .navigationBarTitle(Text("Furiends"))
                }
            }
        }
    }
    

    Then use the .frame to set the maximum width for an image.

    struct FriendView: View {
        ...
    
        var screenWidth: CGFloat
    
        var imageWidth: CGFloat {
            isExpanded ? screenWidth / 4 : screenWidth / 2
        }
    
        var body: some View {
            Button(action: self.toggleExpand) {
                HStack {
                    VStack {
                        Image("dog\(self.randomPic)")
                            .resizable()
                            .renderingMode(.original)
                            .scaledToFill()
                            .clipShape(Circle())
                            .frame(width: imageWidth, height: imageWidth)  // <- add frame boundaries
    
                        if self.isExpanded == false {
                            Text(self.name)
                                .font(.title)
                                .foregroundColor(.primary)
                        }
                    }
    
                    if self.isExpanded == true {
                        VStack {
                            Text(self.name).font(.title)
                            Text(self.breed).font(.headline)
                        }
                        .foregroundColor(.primary)
                    }
                }
            }
        }
    
        func toggleExpand() {
            isExpanded.toggle()
        }
    }
    

    In case you'd want your picture to be centered you can use Spacer:

    HStack {
        Spacer()
        FriendView(name: pet, screenWidth: geo.size.width)
        Spacer()
    }