Reputation: 2976
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?
Upvotes: 1
Views: 44
Reputation: 1557
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.
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))
}
Upvotes: 0
Reputation: 273530
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
}
}
Upvotes: 1