Home > Software design >  How to scale a SwiftUI Image including its overlay?
How to scale a SwiftUI Image including its overlay?

Time:02-12

Given this view with an image that has an overlay:

struct TestView: View {
    var body: some View {
        Image("image")
            .overlay {
                Image("logo")
                    .position(x: 20, y: 130) // I want to freely position the overlay image
            }
    }
}

The size of the images shouldn’t matter. I my example they are 150×150 pixels (“image”) and 20×20 pixels (logo).

looking like this

How can I scale this up to its bounds (like superviews frame), including the overlay?

Some must-have conditions:

  • I need to freely position the overlay image, so I can not use the alignment parameter of overlay in conjunction with a padding on the logo.

  • I also need to position the overlay image absolutely, not relatively. So I can’t use a GeometryReader (or alignment).

  • I need the view to be reusable in different scenarios (different devices, different view hierarchies). That means I don’t know the scale factor, so I can’t use scaleEffect.

I tried:

A) .aspectRatio(contentMode: .fit)

struct TestView: View {
    var body: some View {
        Image("image")
            .resizable()
            .overlay {
                Image("logo")
                    .position(x: 20, y: 130)
            }
            .aspectRatio(contentMode: .fit)
    }
}

B) .scaledToFit()

struct TestView: View {
    var body: some View {
        Image("image")
            .resizable()
            .overlay {
                Image("logo")
                    .position(x: 20, y: 130)
            }
            .scaledToFit()
    }
}

C) Making logo resizable

struct TestView: View {
    var body: some View {
        Image("image")
            .resizable()
            .overlay {
                Image("logo")
                    .resizable()
                    .position(x: 20, y: 130)
            }
            .scaledToFit() // or .aspectRatio(contentMode: .fit)
        }
}

A and B looking like this
C looking like this

Both of which gave me a relative position of the logo that was different from the original (see linked screenshot). The relative size differs as well (it is now too small).

Wrapping in a stack and scale this stack instead also didn’t help.

CodePudding user response:

The problem is the absolute positioning of the logo. I see two ways:

  1. scale the whole resulting image with .scaleEffect.
    Be aware this is a render scaling of the underlying image, it will not be redrawn in the new size, so can become blurry.

     Image("dog")
         .overlay {
             Image(systemName: "circle.hexagongrid.circle.fill")
                 .foregroundColor(.pink)
                 .position(x: 20, y: 130)
         }        
         .scaleEffect(2.0)
    

or – and my preference
2. Position the Logo relative to the lower left corner

    ZStack(alignment: .bottomLeading) {
        Image("dog")
            .resizable()
            .scaledToFit()
        Image(systemName: "circle.hexagongrid.circle.fill")
            .resizable()
            .foregroundColor(.pink)
            .frame(width: 70, height: 70)
            .padding()
    }

CodePudding user response:

Then like this with GeometryReader:

struct ContentView: View {
    
    let imageSize = CGSize(width: 150, height: 150) // Size of orig. image
    let logoPos = CGSize(width: 10, height: 120) // Position of Logo in relation to orig. image
    let logoSize = CGSize(width: 20, height: 20) // Size of logo in relation to orig. image
    
    @State private var contentSize = CGSize.zero
    
    var body: some View {
        
        Image("dog")
            .resizable()
            .scaledToFit()
        
            .overlay(
                GeometryReader { geo in
                    Color.clear.onAppear {
                        contentSize = geo.size
                    }
                }
            )
            .overlay(
                Image(systemName: "circle.hexagongrid.circle.fill")
                    .resizable()
                    .foregroundColor(.pink)
                    .offset(x: logoPos.width / imageSize.width * contentSize.width,
                            y: logoPos.height / imageSize.height * contentSize.height)
                    .frame(width: logoSize.width / imageSize.width * contentSize.width,
                           height: logoSize.height / imageSize.height * contentSize.height)
                , alignment: .topLeading)
    }
}
  • Related