admin管理员组

文章数量:1287507

In my app, I need to display images (textures). Some are tiled, and some are stretched.

At the time, I have this code working:

Image(frame.image)
.resizable(resizingMode: (frame.repeatTexture == true) ? .tile : .stretch)
.frame(width: frame.width, height: frame.height)

However,the render of the .tile is not good. The texture is cropped in X axis. In the image sent, you can see that the bezel is not present at the right, it's truncated.

Original texture where there is a bezel at left and right:

I would like to have the "repeat" only in Y for example. Seems there is no option for that by default.

Is there a tip, an option, or maybe another idea like using background() to force the texture to tile only in one axis?

In my app, I need to display images (textures). Some are tiled, and some are stretched.

At the time, I have this code working:

Image(frame.image)
.resizable(resizingMode: (frame.repeatTexture == true) ? .tile : .stretch)
.frame(width: frame.width, height: frame.height)

However,the render of the .tile is not good. The texture is cropped in X axis. In the image sent, you can see that the bezel is not present at the right, it's truncated.

Original texture where there is a bezel at left and right:

I would like to have the "repeat" only in Y for example. Seems there is no option for that by default.

Is there a tip, an option, or maybe another idea like using background() to force the texture to tile only in one axis?

Share Improve this question edited Feb 25 at 10:05 Sweeper 274k23 gold badges238 silver badges388 bronze badges asked Feb 24 at 10:32 alex.bouralex.bour 2,9769 gold badges43 silver badges70 bronze badges 5
  • It's unclear what the desired result is. Can you show the texture image on its own, and the desired result? – Sweeper Commented Feb 24 at 10:41
  • Hello Sweeper. I edited the post using a new render image and the original texture. Thanks. – alex.bour Commented Feb 24 at 10:50
  • I see. So frame.width is less than the image width, and you want to tile the image vertically, but stretch the image horizontally to fill the whole frame. Is that right? – Sweeper Commented Feb 24 at 10:53
  • Absolutely. I add that frame.width can changed (configurable up to twice its original size). – alex.bour Commented Feb 24 at 11:02
  • You can actually tile the image on one axis only by applying .fixedSize. For example, to tile vertically, use .fixedSize(horizontal: true, vertical: false). However, this doesn't give you the scale effect that you were wanting too. You could scale the result manually using scaleEffect(x:y:anchor:), but you would need to work out the ratio yourself based on the image size and view width. – Benzy Neez Commented Feb 24 at 14:48
Add a comment  | 

2 Answers 2

Reset to default 1

You can draw a stretched version of the image using a GraphicsContext, and then tile that using resizable,

let imageHeight = ... // replace this with the height of your image
let frame = CGSize(width: 300, height: 700) // as an example
Image(size: .init(width: frame.width, height: imageHeight)) { gc in
    let image = gc.resolve(Image(.tile))
    let scale = frame.width / image.size.width
    gc.scaleBy(x: scale, y: 1)
    gc.draw(image, at: .zero, anchor: .topLeading)
    gc.scaleBy(x: 1 / scale, y: 1)
}
.resizable(resizingMode: .tile)
.frame(width: frame.width, height: frame.height)

Or, just draw the entire tiled image with the GraphicsContext.

Image(size: frame) { gc in
    var startY: CGFloat = 0
    let image = gc.resolve(Image(.tile))
    while startY < frame.height {
        let scale = frame.width / image.size.width
        gc.scaleBy(x: scale, y: 1)
        gc.draw(image, at: .init(x: 0, y: startY), anchor: .topLeading)
        gc.scaleBy(x: 1 / scale, y: 1)
        startY += image.size.height
    }
}

I know this was accepted, but I wanted to show a different method that may feel more accessible to some.

If .resizable() could be applied after setting a frame size for the image or scaling it to fit, this wouldn't be an issue. But, as far I know, .resizable() must be the first modifier or else you get a member error.

So the question then is, how can you set the width or aspect ratio of the image first, in such way that you end up with an Image view so you can use .resizable(resizingMode:) on it?

You can do so by resizing the input image first to fit the width of the frame, rendering as a (new) image and then using the resized image as the input for the Image view you will use .resizable(resizingMode:) on.

Now, there are a number of ways do it, as @Sweeper showed, but in the code below I used ImageRenderer, introduced with iOS 16, which can create images from SwiftUI views. That means you could build out your view however you want and then use that as the input to ImageRenderer, which will then basically become your repeated tile. See example at the end.

Here's the full code:

import SwiftUI

struct TileRepeatView: View {
    
    //Parameters
    let image: ImageResource
    let frameSize: CGSize
    
    //Body
    var body: some View {
        
        //ScrollView
        ScrollView(showsIndicators: false) {
            Rectangle()
                .stroke(.black)
                .background(alignment: .topLeading) {
                    
                    //Safely unwrap optional Image? returned by resizeToWidth()
                    if let resizedImage = resizeToWidth(image: image, targetSize: frameSize.width) {
                        
                        resizedImage // <- this is the returned Image view
                            .resizable(resizingMode: .tile) // <- now you can use .tile without clipping
                    } else {
                        //Show a placeholder or content unavailable message
                        ContentUnavailableView {
                            Label("Rendering failed", systemImage: "questionmark.circle.fill")
                        }
                    }
                }
                .frame(width: frameSize.width, height: frameSize.height)
        }
        .frame(maxWidth: .infinity, alignment: .center)
    }
    
    //Function to resize an image to a specified width and return an Image view
    private func resizeToWidth(image: ImageResource, targetSize: CGFloat) -> Image? {
        
        //Create a view required for ImageRenderer
        let imageView = Image(image)
            .resizable()
            .frame(width: targetSize)

        let renderer = ImageRenderer(content: imageView)
    
        //Return nil if renderer doesn't return an uiImage
        guard let uiImage = renderer.uiImage else {
            print("Failed to render image")
            return nil
        }
    
        //return Image view
        return Image(uiImage: uiImage)
    }
}

//Preview
#Preview {
    @Previewable @State var frameSize: CGSize = .init(width: 300, height: 700)
    
    //Preview controls
    HStack {
        Text("Frame:")
        Group {
            Button {
                frameSize = CGSize(width: 300, height: 700)
            } label: {
                Text("Size 1")
            }
            
            Button {
                frameSize = CGSize(width: 100, height: 800)
            } label: {
                Text("Size 2")
            }
            
            Button {
                frameSize = CGSize(width: 200, height: 1200)
            } label: {
                Text("Size 3")
            }
        }
        .buttonStyle(.bordered)
        .padding(.vertical)
    }
    
    TileRepeatView(image: .tile, frameSize: frameSize) // <- update the image here as needed
}

Note: I saved the original tile image you provided and roughly cropped it and then imported it to Assets with the name "tile". You can also provide a name in the format below or you can update the type to whatever suits your model:

TileRepeatView(image: .init(name: "tile", bundle: .main), frameSize: frameSize) 

This is the tile image used:

Optional example - Since ImageRenderer can take any views as input, you can customize the view as needed. Here's a rough example, using an overlay added to the imageView, that adds a watermark that will repeat with the tile.

//Create a view required for ImageRenderer
let imageView = Image(image)
    .resizable()
    .frame(width: targetSize)
    .overlay {
        Text("Sample")
            .font(.system(size: 100))
            .rotationEffect(.degrees(-90))
            .fixedSize()
            .foregroundStyle(.white.opacity(0.5))
    }

本文标签: swiftuiHow to tile a texture vertically onlyStack Overflow