I have the following SwiftUI code where I wish to present a 16:9 camera preview in landscape mode & 9:16 preview in portrait mode. I also want a custom alignment for the subviews as follows:
The camera preview (prototyped as
Color.blue
) in the code below to be aligned to the leading edge of safe area insets (+/- few points if I desire).The
VStack/HStack
overlay frame to align and match with Camera preview.
Need inputs on how to fix the code below to proceed in this direction. I tried to fetch the size of the two views using onGeometryChange
but I seem to be getting incorrect size for the top (Color.clear
) view it seems. Returned size is transposed.
struct CameraUI: View {
@Environment(\.verticalSizeClass) var verticalSizeClass
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State var cameraViewSize:CGSize = CGSize.zero
@State var viewSize:CGSize = CGSize.zero
var body: some View {
Color.clear
.ignoresSafeArea()
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newValue in
viewSize = newValue
}
.background {
Color.blue
.ignoresSafeArea()
.aspectRatio(verticalSizeClass == .regular ? 9.0/16.0 : 16.0/9.0, contentMode: .fit)
.offset(x:verticalSizeClass == pact ? -(viewSize.width - cameraViewSize.width)/2 : 0)
//Need to inset with leading edge of Safe Area
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newValue in
cameraViewSize = newValue
print("View size \(viewSize.width), Camera View size \(cameraViewSize.width)")
}
}
.persistentSystemOverlays(.hidden)
.overlay {
/* Need to have this VStack/HStack aligned with the Color.blue view */
VStack {
Spacer()
HStack {
Button("Button1") {
}
Spacer()
Button("Button2") {
}
}
}
}
}
}
#Preview {
CameraUI()
}
I have the following SwiftUI code where I wish to present a 16:9 camera preview in landscape mode & 9:16 preview in portrait mode. I also want a custom alignment for the subviews as follows:
The camera preview (prototyped as
Color.blue
) in the code below to be aligned to the leading edge of safe area insets (+/- few points if I desire).The
VStack/HStack
overlay frame to align and match with Camera preview.
Need inputs on how to fix the code below to proceed in this direction. I tried to fetch the size of the two views using onGeometryChange
but I seem to be getting incorrect size for the top (Color.clear
) view it seems. Returned size is transposed.
struct CameraUI: View {
@Environment(\.verticalSizeClass) var verticalSizeClass
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State var cameraViewSize:CGSize = CGSize.zero
@State var viewSize:CGSize = CGSize.zero
var body: some View {
Color.clear
.ignoresSafeArea()
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newValue in
viewSize = newValue
}
.background {
Color.blue
.ignoresSafeArea()
.aspectRatio(verticalSizeClass == .regular ? 9.0/16.0 : 16.0/9.0, contentMode: .fit)
.offset(x:verticalSizeClass == pact ? -(viewSize.width - cameraViewSize.width)/2 : 0)
//Need to inset with leading edge of Safe Area
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newValue in
cameraViewSize = newValue
print("View size \(viewSize.width), Camera View size \(cameraViewSize.width)")
}
}
.persistentSystemOverlays(.hidden)
.overlay {
/* Need to have this VStack/HStack aligned with the Color.blue view */
VStack {
Spacer()
HStack {
Button("Button1") {
}
Spacer()
Button("Button2") {
}
}
}
}
}
}
#Preview {
CameraUI()
}
Share
Improve this question
edited Mar 10 at 21:32
HangarRash
15.1k5 gold badges20 silver badges55 bronze badges
asked Mar 6 at 9:34
Deepak SharmaDeepak Sharma
6,68110 gold badges67 silver badges162 bronze badges
6
|
Show 1 more comment
1 Answer
Reset to default 1You don't need any GeometryReaders to achieve this layout. You can do it simply with some overlays and orientation-based stacks.
For the buttons to be aligned with the blue camera preview, they just need to be in the same stack. The only difference is whether it's an HStack
or a VStack
based on orientation.
I kept it simple by checking the verticalSizeClass
, but it could also be done using ViewThatFits
maybe, although it may become unnecessarily complicated.
if isLandscapeOrientation {
HStack(spacing: 0) {
//Camera preview
cameraPreview
//Controls
CameraUIControls()
}
}
else {
VStack(spacing: 0) {
//Camera preview
cameraPreview
//Controls
CameraUIControls()
}
}
The buttons are also similarly arranged in a horizontal or vertical stack based on orientation. If needed, their container could be a ScrollView
to accommodate more buttons. (UPDATE: the code below now reflects this).
Depending on the orientation, you need to ignore the appropriate safe areas, so the camera preview can fill the space nicely:
.ignoresSafeArea(.container, edges: isLandscapeOrientation ? [.leading, .vertical] : [.top])
Additional elements can be added over the blue preview as overlays (see the grid lines and the close button as examples in the code below).
Complete code:
import SwiftUI
struct CameraUIRootView: View {
//State values
@State private var showCameraPreview = true
//Body
var body: some View {
VStack {
Button {
withAnimation {
showCameraPreview.toggle()
}
} label: {
Label("Take picture", systemImage: "camera.fill")
}
.buttonStyle(.borderedProminent)
}
.fullScreenCover(isPresented: $showCameraPreview) {
CameraUIPreview()
}
}
}
struct CameraUIPreview: View {
//Environment values
@Environment(\.verticalSizeClass) var verticalSizeClass
@Environment(\.dismiss) var dismiss
//Helper computed property for detecting landscape orientation
private var isLandscapeOrientation: Bool {
verticalSizeClass == pact
}
//State values
@State private var showGridLines = false
//Body
var body: some View {
Group {
if isLandscapeOrientation {
HStack(spacing: 0) {
//Camera preview
cameraPreview
//Controls
CameraUIControls()
}
}
else {
VStack(spacing: 0) {
//Camera preview
cameraPreview
//Controls
CameraUIControls()
}
}
}
.persistentSystemOverlays(.hidden)
.statusBarHidden()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isLandscapeOrientation ? .leading : .top)
.ignoresSafeArea(.container, edges: isLandscapeOrientation ? [.leading, .vertical] : [.top]) // <- Optional: use [.bottom] if you don't want to push the preview into the top safe area or leave blank [], which may cause issues with respecing the aspect ratio depending on device
}
private var cameraPreview: some View {
Color.blue
.aspectRatio(isLandscapeOrientation ? 16.0/9.0 : 9.0/16.0, contentMode: .fit)
//Close preview button
.overlay(alignment: .topTrailing) {
Button {
withAnimation {
// showCameraPreview.toggle()
dismiss()
}
} label: {
Text("Close")
}
.tint(.white)
.padding(30)
}
// Shutter button
.overlay(alignment: isLandscapeOrientation ? .trailing : .bottom) {
Button {
showGridLines.toggle()
} label: {
Image(systemName: "camera")
.imageScale(.large)
.padding()
}
.tint(.white)
.frame(width: 80, height: 80)
.background(.white.gradient.opacity(0.4), in: Circle())
.padding()
}
//Gridlines overlay
.overlay {
CameraUIGridLines()
}
}
}
struct CameraUIControls: View {
//Environment values
@Environment(\.verticalSizeClass) var verticalSizeClass
//Helper computed property for detecting landscape orientation
private var isLandscapeOrientation: Bool {
verticalSizeClass == pact
}
//Body
var body: some View {
Group {
if isLandscapeOrientation {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
controls
}
}
}
else {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
controls
}
}
}
}
.contentMargins(16)
// .padding()
}
@ViewBuilder
private var controls: some View {
Group {
//Flash button
Button {
//...
} label: {
Image(systemName: "bolt.fill")
.padding()
}
// Spacer()
//Gridlines button
Button {
//...
} label: {
Image(systemName: "grid")
.padding()
}
// Spacer()
//Macro mode button
Button {
//...
} label: {
Image(systemName: "camera.macro")
.padding()
}
// Spacer()
//Metering mode button
Button {
//...
} label: {
Image(systemName: "camera.metering.center.weighted")
.padding()
}
// Spacer()
//Flip camera button
Button {
//...
} label: {
Image(systemName: "camera.rotate")
.padding()
}
}
.background(.gray.gradient.opacity(0.2), in: Circle())
.tint(.primary)
.containerRelativeFrame(isLandscapeOrientation ? .vertical : .horizontal, count: 5, spacing: 0)
}
}
struct CameraUIGridLines: View {
//Body
var body: some View {
ZStack {
HStack {
gridLines
}
VStack {
gridLines
}
}
}
private var gridLines: some View {
Group {
Spacer()
divider
Spacer()
divider
Spacer()
}
}
private var divider: some View {
Divider()
//Divider color
.overlay {
Color.white
}
}
}
//Preview
#Preview("Root view") {
CameraUIRootView()
}
#Preview("Camera preview") {
CameraUIPreview()
}
#Preview("Controls") {
CameraUIControls()
}
UPDATE:
If you use a Landscape Right orientation (where the FaceID sensor area is on the left) and you want to respect the leading/left safe area, change .leading
to .trailing
in .ignoresSafeArea()
of CameraUIPreview
:
// ... in CameraUIPreview
.ignoresSafeArea(.container, edges: isLandscapeOrientation ? [.vertical, .trailing] : [.top])
This allows the buttons to the right of the preview to push into the available trailing safe area if needed, without having to change contentMode
to .fill
.
Changed also the divider in CameraUIGridLines
to use an .overlay{}
instead of .background()
, with the curly brackets initializer that respects the (already set) safe areas:
private var divider: some View {
Divider()
//Divider color
.overlay {
Color.white
}
}
UPDATE 2:
Moved the .ignoresSafeAreas()
in CameraUIPreview
to be the last modifier after the .frame
modifier, for better compatibility with older devices and smaller screen sizes.
VStack
as an overlay ofColor.clear
instead toColor.blue
? It would make a lot more sense if it were an overlay ofColor.blue
. – Sweeper Commented Mar 6 at 9:48Color.clear
may make sense. – Deepak Sharma Commented Mar 6 at 13:52CameraPreview
andCameraControls
views that does not depend on each other. How they are structured inCameraUI
is irrelevant to how coupled they are. – Sweeper Commented Mar 6 at 13:57VStack
andHStack
? In that case I would put each camera control as a separatebackground
on theColor.clear
with customalignment:
s. – Sweeper Commented Mar 6 at 14:02