I have a view which consists of a background image/animation and a foreground control buttons. I want to .overlay
the buttons on top of the background, to be aligned at the bottom leading, center, and trailing - see screenshot below (doesn't show the actual background).
However, the background has to be an animation. I'm using a wrapper view around AVPlayer
for this. The video that gets played is portrait, but it gets filled to landscape as is thus wider than screen. I need it to fill the vertical space of the screen. That means that I have to use .frame(maxWidth: .infinity, maxHeight: .infinity).scaledToFill()
.
That leaves vertical alignment just fine - the gray top-center badge and the bottom-center button are rendered at the right places - however, it messes with the horizontal alignment. The bottom-right button aligns itself w.r.t. the video instead of the screen, and is thus aligned out of the bounds of the screen (see the second image).
I need the background to fill the screen, and the overlay to be aligned properly. The videos are recorded portrait, but AVPlayer
makes them landscape with black filling on the sides, so unless that can be tweaked, I can't change the videos' aspect-ratios.
The most beneficial thing for me would be to learn how to align components w.r.t. the screen, not the parent in the overlay stack. Is there are way to achieve that? If not, is there a workaround to fix my problem (make the buttons align properly along the horizontal)?
The code below isn't the source of truth, and just generates the demos. It is provided since a gentleman in the comments politely asked for it. The images are the ultimate source of truth. The actual code is much bigger, with a mechanism of randomly (and based on app state) choosing an AVPlayer
to play an mp4
video. I don't think that this should be important (encapsulation and stuff), but if it is, tell me why it affects the code structure in the comments, and I will add more details.
A function used in the demos below is:
private func bigBgImage() -> some View {
self.backgroundChooser.render()
.resizable()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.scaledToFill()
}
...where backgroundChooser.render()
is to be taken as a blackbox. You can just drop in a dummy Image
.
This demo is achieved through a dummy image in a GeometryReader
. This is a workaround to bound the overlays despite the bigBgImage
having maxWidth: .infinity
. Since GeometryReader
messes with the animation alignment, I don't want to use it in the final code. Nevertheless, here's the snippet:
GeometryReader { _ in
self.bigBgImage()
}
.overlay(alignment: .top) { self.title() }
.overlay(alignment: .bottom) { self.resetButton() }
.overlay(alignment: .bottomTrailing) { self.toggleButton() }
self.bigBgImage()
.overlay(alignment: .top) { self.title() }
.overlay(alignment: .bottom) { self.resetButton() }
.overlay(alignment: .bottomTrailing) { self.toggleButton() }
The mechanism behind backgroundChooser
is too complex to contain in an SO question in a meaningful way. One can just drop in any image
I found that the problem was your .frame
and .scaleToFill()
after the Image.
Here I have a different solution. (Code is below the image)
Use screen max width and height instead of .infinity with overlay
struct DemoView: View {
var body: some View {
ZStack {
bigBigImage
.overlay(alignment: .top) {
Button("Top") {
}
.padding()
.background(.black)
.cornerRadius(10)
}
.overlay(alignment: .bottom) {
Button("Center") {
}
.padding()
.background(.black)
.cornerRadius(10)
}
.overlay(alignment: .bottomTrailing) {
Button("Leading") {
}
.padding()
.background(.black)
.cornerRadius(10)
.padding(.trailing)
}
}
}
var bigBigImage: some View {
Image("Swift")
.resizable()
.scaledToFill() //here
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height) //here
.edgesIgnoringSafeArea(.all)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With