I’m having some issues with KFImage. It works fine with the placeholder image, until the remote image is loaded. It seems to be some interaction between .aspectRatio() and .layoutPriority(-1).
I have a kind of card with an image, and a couple text blocks beneath it. I want the image to be clipped to a view that has a specific aspect ratio, and whose width is dictated by the container (in the real app these live in a grid, and size will vary from device to device. I want the image to grow, rather than the spacing to grow).
Here’s what I get with the placeholder (the bubbles) and the loaded image (the aspect ratio test image). The placeholder image is 654 x 768, and the loaded image is 1080 x 1920. The aspect ratio of the placeholder is properly maintained, and it is clipped to the desired aspect ratio.
But as you can see, the loaded image is scaled non-uniformly to fit in the same aspect-ratio as the placeholder. The normal solution to that is to add .aspectRatio(.fill). But if I do that on KFImage, it blows up everything:
Here’s the code. Note the .aspectRatio(contentMode: .fill) on KFImage is commented out. If you uncomment it, you get the second image above.
import SwiftUI
import Kingfisher
struct
SellerShowCell: View
{
let title : String
let secondary : String?
var startTime : Date? = nil
var thumbnailURL : URL? = nil
var
body: some View {
VStack(alignment: .leading, spacing: 0.0) {
ZStack(alignment: .topLeading) {
// Thumbnail background…
ZStack {
KFImage(self.thumbnailURL)
.cancelOnDisappear(true)
.placeholder {
ZStack {
Color(.black)
Image("image_placeholder_background")
.resizable()
.aspectRatio(contentMode: .fill)
.layoutPriority(-1)
}
}
.resizable()
// .aspectRatio(contentMode: .fill) // Uncommenting this ruins the placeholder and allows the loaded image to dictate the parent size
// .layoutPriority(-1) // This does not have any obvious effect on KFImage
}
.aspectRatio(158.0 / 220.0, contentMode: .fit)
.cornerRadius(16.0)
Text("A Note")
.foregroundColor(.white)
.padding(8.0)
.background(Color.purple)
.clipShape(Capsule())
.padding([.top, .leading], 10.0)
}
// Title…
Text("\(self.title)")
.multilineTextAlignment(.leading)
.lineLimit(2)
.font(.custom("Helvetica", size: 16.0).weight(.semibold))
.foregroundColor(.black)
.padding(.top, 12.0)
// Secondary…
if let secondary = self.secondary {
Text("\(secondary)")
.multilineTextAlignment(.leading)
.font(.custom("Helvetica Neue", size: 12.0).weight(.medium))
.foregroundColor(Color("secondary-color"))
.padding(.top, 12.0)
}
}
.border(Color.green)
}
}
struct SellerShowCell_Previews: PreviewProvider {
static var previews: some View {
ZStack {
Color("main-tab-background")
.ignoresSafeArea()
VStack(spacing: 20.0) {
SellerShowCell(title: "Here is a caption of sufficient length to be a couple of lines",
secondary: "Secondary Text",
thumbnailURL: nil)
.padding(.horizontal, 80)
SellerShowCell(title: "Here is a caption of sufficient length to be a couple of lines",
secondary: "Secondary Text",
thumbnailURL: URL(string: "https://i.imgur.com/iH7Xe20.jpg")!)
.padding(.horizontal, 80)
}
}
}
}
Related
Does anyone know if it is possible to set some kind of priority of a custom alignment guide?
I have a VStack where one of the items is an image(QR code) that is centered to "background view" outside the VStack with a custom alignment guide. (See picture)
Image with QR code centered on the screen
My problem is that I want this custom alignment guide to only apply if the all items in the VStack fits in the screen. The Spacer does not seams to have any effect when I center the image with the custom alignment guide. I have tried to set layout priority but with no success.
extension VerticalAlignment {
private enum CenterAztecImage: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
return context[VerticalAlignment.center]
}
}
static let centerAztecImage = VerticalAlignment(CenterAztecImage.self)
}
struct InactiveTicketSwiftUIView<ViewModel>: View where ViewModel: InactiveTicket {
#ObservedObject var viewModel: ViewModel
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .centerAztecImage)) {
viewModel.backgroundColor
.ignoresSafeArea()
.alignmentGuide(.centerAztecImage) { d in
d[VerticalAlignment.centerAztecImage]
}
VStack {
VStack {
viewModel.priceCategoryColor.frame(height: 16.0)
HStack {
Text(viewModel.priceCategory).padding()
Spacer()
viewModel.operatorImage.padding()
}
Divider()
// Here is the image that is centered with a custom alignment guide
viewModel.fakeAztecImage
.resizable()
.frame(width: 224, height: 224, alignment: .leading)
.aspectRatio(contentMode: .fit)
.padding()
.alignmentGuide(.centerAztecImage, computeValue: { d in
return d[VerticalAlignment.center]
})
Divider()
// Multiple Text views for testing
Text(viewModel.type).padding()
Text(viewModel.type).padding()
Text(viewModel.type).padding()
Text(viewModel.type).padding()
Text(viewModel.type).padding()
}
.frame(width: 320.0, alignment: .center)
.background(.white)
.cornerRadius(12)
.shadow(color: .gray.opacity(0.5), radius: 12, x: 0, y: 4)
// This Spacer seams to be allways be "overriden" by the custom alignment guide that centers the image
Spacer(minLength: 20).layoutPriority(1)
}
}
}
}
I use a ZStack to display a fullscreen background image underneath the main UI. The main UI consists of a VStack with multiple views separated by flexible Spacers to scale down or up on different device sizes. Now I experience that the Spacers will not scale down on small devices because the background image on small devices remains bigger than the screen size and keeps the ZStack tall, see screenshot of the preview of iPhone 8. What am I doing wrong here?
Code:
import SwiftUI
struct TestView: View {
var headerView: some View {
VStack {
Text("HEADER").padding([.leading,.trailing], 100)
}
}
var middleView: some View {
HStack {
Text("MIDDLE").padding([.leading,.trailing], 100)
}
}
var bottomView: some View {
VStack {
Text("BOTTOM").padding([.leading,.trailing], 100)
}
}
var body: some View {
ZStack {
// Background Image
Image("BgImg")
.resizable()
.aspectRatio(contentMode: .fill)
.edgesIgnoringSafeArea(.all)
// Main UI
VStack(spacing: 0) {
headerView
.frame(height:222)
.background(Color.red)
Spacer()
middleView
.frame(height:155)
.background(Color.orange)
Spacer()
bottomView
.frame(height:288)
.background(Color.yellow)
}
.padding(.bottom, 32)
.padding(.top, 54)
// END Main UI
}
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
// END ZStack
}
}
Preview Screenshot:
See blue border is bigger than device
BG Image:
In described scenario you need to use .background instead of ZStack, such so main view form needed full-screen layout and image in background will not affect it.
So the layout should be like
VStack(spacing: 0) {
// content here
}
.padding(.bottom, 32)
.padding(.top, 54)
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
.background(
// image here
)
Is it possible to make paged TabView that wraps its content? I don't know the height of the content (it is an Image resized to fit the width of the screen) so I can't use frame modifier.
My code looks like this:
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
Color.red
.frame(width: 20, height: 20)
}
The problem is that the TabView is as big as the screen and PageIndicator is placed in the top right corner of the screen instead of top right corner of the image. Tanks for any suggestions.
EDIT:
I've added code that is reproducible. PageIndicator was replaced by red rectangle.
struct Test: View {
struct Entry: Identifiable {
let image: Image
let description: String
var id: String { description }
}
let data = [
Entry(image: Image(systemName: "scribble"), description: "image 1"),
Entry(image: Image(systemName: "trash"), description: "image 2")
]
var body: some View {
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
Color.red
.frame(width: 20, height: 20)
}
}
}
Your PageIndicator is not placed on the image, because you didn't place it there. You are placing it in a layer on top of a VStack that happens to contain text and an image that can be shorter than the screen. If you want the PageIndicator on the image, then you need to do that specifically. You didn't provide a Minimal, Reproducible Example, but does something like this work:
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.overlay(
PageIndicator()
)
.frame(maxWidth: .infinity)
.overlay(
PageIndicator()
)
Text(entry.description)
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
The .overlay() needs to go before the .frame() so it stays on the image, instead of overlaying the .frame().
Edit:
Based off of the MRE and the comment, here is alternate solution that aligns the PageIndicator to the top of the image, but does not scroll with the image. Please note that this is not a perfect MRE as the image heights are different, but this solution actually accounts for that as well. Lastly, I added a yellow background on the image to show that things are aligned properly.
struct Test: View {
struct Entry: Identifiable {
let image: Image
let description: String
var id: String { description }
}
let data = [
Entry(image: Image(systemName: "scribble"), description: "image 1"),
Entry(image: Image(systemName: "trash"), description: "image 2")
]
#State private var imageTop: CGFloat = 50
var body: some View {
ZStack(alignment: .topTrailing) {
TabView {
ForEach(data) { entry in
VStack {
entry.image
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
.background(Color.yellow)
.background(
GeometryReader { imageProxy in
Color.clear.preference(
key: ImageTopInGlobal.self,
// This returns the top of the image relative to the TabView()
value: imageProxy.frame(in: .named("TabView")).minY)
}
)
Text(entry.description)
}
}
}
// This gives a reference to another container for comparison
.coordinateSpace(name: "TabView")
.tabViewStyle(.page(indexDisplayMode: .never))
VStack {
Spacer()
.frame(height: imageTop)
Color.red
.frame(width: 20, height: 20)
}
}
// You either have to ignore the safe area or account for it with regard to the TabView(). This was simpler.
.edgesIgnoringSafeArea(.top)
.onPreferenceChange(ImageTopInGlobal.self) {
imageTop = $0
}
}
}
private extension Test {
struct ImageTopInGlobal: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = nextValue()
}
}
}
Final edit:
In response to the last comment, my answer is a qualified no. I don't think there is any way to make a TabView hug its contents. (I take the original question using the term wrap to mean hug, as the TabView always "wraps" its contents.) If you try to use a preference key, the TabView collapses. There would have to be a minHeight or height set to prevent this which defeats the purpose of the hugging attempt.
I have a VStack that contains an Image and a Text. I am setting the width (and height) of the Image to be half of the screen's width:
struct PopularNow: View {
let item = popular[0]
var body: some View {
VStack(spacing: 5) {
Image(item.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: getRect().width/2, height: getRect().width/2)
Text(item.description)
.font(.caption)
.fontWeight(.bold)
.lineLimit(0)
}
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(15)
}
func getRect() -> CGRect {
return UIScreen.main.bounds
}
}
The problem is that the Text pushes and causes the VStack to expand instead of respecting the Image's width. How can I tell the Text to not grow horizontally more than the Image width and to grow "vertically" (i.e. add as many lines it needs)? I know that I can add the frame modifier to VStack itself, but it seems like a hack. I want to tell the Text to only take as much space width wise as VStack already has, not more.
This is what it looks like right now, as you can see the VStack is not half the screen's size, it's full screen size because the Text is expanding it.
Try to fix its size, like
VStack(spacing: 5) {
Image(item.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: getRect().width/2, height: getRect().width/2)
Text(item.description)
.font(.caption)
.fontWeight(.bold)
.lineLimit(0)
}
.fixedSize() // << here !!
// .fixedSize(horizontal: true, vertical: false) // alternate
.padding()
Added on the 24th of July:
This line of code fixes the space in the detail view. However... in the list view the title has become a lot smaller too.
.navigationBarTitle(Text("Egg management"), displayMode: .inline)
Added on the 23th of July:
Thanks to the tips I made a lot of progress. Especially the tip to add borders does wonders. You see exactly what happens!
However, there seems to be a difference between the Xcode Preview canvas, the simulator and the physical device. Is this a bug because -after all- it is still beta? Or is there anything I can do?
As you can see in the images... only in the Xcode Preview canvas the view connects to the top of the screen.
I believe it has something to do with the tabbar. Since when I look at the Xcode Preview canvas with the tabbar... that space above is also there. Any idea how to get rid of that?
Original postings:
This is my code for a detailed list view:
import SwiftUI
struct ContentDetail : View {
#State var photo = true
var text = "Een kip ..."
var imageList = "Dag-3"
var day = "3.circle"
var date = "9 augustus 2019"
var imageDetail = "Day-3"
var weight = "35.48"
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text(date)
.font(.title)
.fontWeight(.medium)
ZStack (alignment: .topLeading){
Image(photo ? imageDetail : imageList)
.resizable()
.aspectRatio(contentMode: .fit)
.background(Color.black)
.padding(.trailing, 0)
.tapAction {
self.photo.toggle() }
HStack {
Image(systemName: day)
.resizable()
.padding(.leading, 10)
.padding(.top, 10)
.frame(width: 40, height: 32)
.foregroundColor(.white)
Spacer()
Image(systemName: photo ? "photo" : "pencil.circle")
.resizable()
.padding(.trailing, 10)
.padding(.top, 10)
.frame(width: 32, height: 32)
.foregroundColor(.white)
}
}
Text(text)
.lineLimit(6)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 6)
} .padding(20)
}
}
#if DEBUG
struct ContentDetail_Previews : PreviewProvider {
static var previews: some View {
ContentDetail()
}
}
#endif
Also included is the preview canvas. What I don't get is how I can make sure the text and photo are aligned to the top (instead of the middle). I tried with Spacers, padding etc.
I must be overseeing something small I guess... but. Can somebody point me in the right direction? Thanks.
Added:
After both answers I added a Spacer() after the last text. In Xcode in the preview canvas everything looks okay now. But on my connected iPhone 7 Plus there are some problems: the view is not aligned to the top, and the image is cropped (icon on the right is gone; white banding to the right).
Adding a Spacer() after the last text shifts everything to the top. Tested on iPhone Xr simulator (not preview).
...
Text(text)
.lineLimit(6)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 6)
Spacer()
}
To remove the space at the top:
VStack {
...
}
.padding(20)
.navigationBarTitle("TITLE", displayMode: .inline)
Think in terms of what a Spacer() does. It "moves" the views as far apart as it can - at least, without a specific space.
So you have this:
VStack {
Text
ZStack {
Image
HStack {
Image
Spacer()
Image
}
}
Text
}
All told, going from inner to outer, you have a horizontal stack of two images placed as far apart (the spacer is between them) inside of a "Z axis" stack that places an image on top of them, inside of a vertical stack that has some text above it.
So if you want to move everything in that vertical stack to the top, you simply need to add one last spacer:
VStack {
Text
ZStack {
Image
HStack {
Image
Spacer()
Image
}
}
Text
Spacer() // <-- ADD THIS
}
Last note: Don't be afraid to adding additional "stacks" to your view. In terms of memory footprint, it's really just a single view with no performance hit.
EDIT: I took your original view and changed everything to placeholders...
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text("Text #1")
.font(.title)
.fontWeight(.medium)
ZStack (alignment: .topLeading) {
Text( "Image #1")
HStack {
Text("Image #2")
Spacer()
Text("Image #3")
}
}
Text("Text #2")
.lineLimit(6)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 6)
} .padding(20)
}
As expected, everything is vertically centered. Adding a Spacer() below "Text #2" throws everything to the top. A couple of thoughts:
Starting there, and add in your Image views one by one. Add in the modifiers like that also.
I don't have the specific images you are rendering, so maybe put a noticeable background color on various things (orange is my personal favorite) and see if the top Image is actually on top but the image makes it appear as though it isn't. A border would work pretty well too.