I'm trying to make a Card View (very similar to the one on the App Story Today page) in SwiftUI. Each card has an image, some text below it, and rounded edges.
Each card image is 600 pixels by 400 pixels. On each image, there is a specific place I want to crop to, and it varies from image to image. For example, in the image below (and this is the image I use for this post), it is roughly the center of the plate (i.e. when I crop the image for the card, I want to preserve the plate but not the wood background). But that cropping origin/reference coordinate varies from image to image – for some it could be to the left side, right side, etc. (now that I think about it, how might I optimize for that?).
Here's the code that I use to generate my cards, along with the code I use to crop the images:
// StoryView.swift
import SwiftUI
struct StoryView: View {
@Environment(\.colorScheme) var colorScheme
var story: Story
var body: some View {
RoundedRectangle(cornerRadius: self.cornerRadius)
.fill(self.colorScheme == .light ? Color.white : self.darkModeCardColor)
.frame(height: self.cardHeight)
.shadow(radius: self.colorScheme == .light ? 20 : 0)
.overlay(imageAndText())
.padding([.leading, .trailing], horizontalSidePadding)
}
@ViewBuilder
private func imageAndText() -> some View {
VStack(spacing: 0) {
Image(uiImage: self.croppedPrimaryImage)
.resizable()
// Spacer()
HStack {
VStack(alignment: .leading) {
Text("Lorem Ipsum".uppercased())
.font(.headline)
.foregroundColor(.secondary)
Text(self.story.title)
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(2)
.padding([.vertical], 4)
Text("Lorem ipsum dolor sit".uppercased())
.font(.caption)
.foregroundColor(.secondary)
}
.layoutPriority(1)
Spacer()
}
.padding()
}
.cornerRadius(self.cornerRadius)
}
// MARK: - Image Cropping
// TODO: - Fix this so that there are no force unwrappings
var croppedPrimaryImage: UIImage {
cropImage(image: story.previewImage, toRect: CGRect(x: 85, y: 0, width: cardWidth, height: 400))!
}
// see: https://stackoverflow.com/questions/31254435/how-to-select-a-portion-of-an-image-crop-and-save-it-using-swift
func cropImage(image: UIImage, toRect: CGRect) -> UIImage? {
// Cropping is available through CGGraphics
let cgImage :CGImage! = image.cgImage
let croppedCGImage: CGImage! = cgImage.cropping(to: toRect)
return UIImage(cgImage: croppedCGImage)
}
// MARK: - Drawing Constants
private let cornerRadius: CGFloat = 30
private let cardHeight: CGFloat = 450
private let cardWidth: CGFloat = UIScreen.main.bounds.size.width
private let horizontalSidePadding: CGFloat = 26
private let darkModeCardColor = Color(red: 28/255, green: 28/255, blue: 30/255)
}
struct StoryView_Previews: PreviewProvider {
static var previews: some View {
Group {
StoryView(story: Story(title: "Lorem ipsum", previewImage: UIImage(imageLiteralResourceName: "img.jpg")).colorScheme(.dark)
// StoryView(story: Story(title: "Lorem ipsum dolor sit amet", previewImage: UIImage(imageLiteralResourceName: "img.jpg")).colorScheme(.light)
}
}
}
In this line, cropImage(image: story.previewImage, toRect: CGRect(x: 85, y: 0, width: cardWidth, height: 400))!
I figured out the constants that bring me to the center of the cropped plate image (definitely not the best solution, but I'm at a loss for exactly how to integrate GeometryReader into this – any ideas on that?)
Anyway, if I run it on iPhone 11 Pro Max, the card looks great:
But then, if I switch to iPhone SE (2nd generation), the plate is no longer centered in the card. To be expected, given that I hardcoded the points and CGRect, but how to do I fix this?
Again, I feel like I need to use GeometryReader here at some point, and I should store the center coordinate in pixels for each image and then work off of that.
For instance, the cropping coordinate of this image would be the point at the middle of the plate, so approx (300, 200) pixels, and then I would add and subtract a certain amount (based on available space (width and height) in card, depending on device - we get this with geometry reader) from both the x and y coordinates of the cropping coordinate to get my cropped image for the card.
I hope some of that made sense - let me know if you might have any ideas to help. I'm at a loss.
Just use as less hard-codings as possible and as many system-defined as possible, and then auto-layout will fit into every device.
Here is some modified parts (added .scaledToFill
and .clipShape
, and removed hardcoding) - and image centered naturally. Demo & tested with Xcode 12 / iOS 14.
struct StoryView: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
RoundedRectangle(cornerRadius: self.cornerRadius)
.fill(self.colorScheme == .light ? Color.white : self.darkModeCardColor)
.frame(height: self.cardHeight)
.shadow(radius: self.colorScheme == .light ? 20 : 0)
.overlay(imageAndText())
.clipShape(RoundedRectangle(cornerRadius: self.cornerRadius))
.padding([.leading, .trailing])
}
@ViewBuilder
private func imageAndText() -> some View {
VStack(spacing: 0) {
Image("img")
.resizable()
.scaledToFill()
HStack {
VStack(alignment: .leading) {
Text("Lorem Ipsum".uppercased())
.font(.headline)
.foregroundColor(.secondary)
Text("Lorem ipsum")
.font(.title)
.fontWeight(.black)
.foregroundColor(.primary)
.lineLimit(2)
.padding([.vertical], 4)
Text("Lorem ipsum dolor sit".uppercased())
.font(.caption)
.foregroundColor(.secondary)
}
.layoutPriority(1)
Spacer()
}
.padding()
}
}
// ... other code