Search code examples
swiftimageswiftuipositionscale

How to scale a SwiftUI Image including its overlay?


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

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.

[UPDATE] Accepted answer

Apparently not having a real view hierarchy in SwiftUI vs UIKit comes at a price. Scaling a hierarchy shouldn’t be that complicated imho. Instead I still would expect .scaledToFit (if added at the end) to scale everything before it (see A), B), and C)).

Adapted accepted answer (minus contentSize and alignment: .topLeading):

struct ContentView: View {
    var body: some View {
        Image("image")
            .resizable()
            .scaledToFit()
            .overlay {
                GeometryReader { geometry in
                    let imageSize = CGSize(width: 150, height: 150)
                    let logoSize = CGSize(width: 20, height: 20)
                    let logoPosition = CGPoint(x: 20, y: 130)
                    Image("logo")
                        .resizable()
                        .position(
                            x: logoPosition.x / imageSize.width * geometry.size.width,
                            y: logoPosition.y / imageSize.height * geometry.size.height
                        )
                        .frame(
                            width: logoSize.width / imageSize.width * geometry.size.width,
                            height: logoSize.height / imageSize.height * geometry.size.height
                        )
                }
            }
    }
}

Solution

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