How to scale the contentview to multiple screen sizes? SwiftUI - swiftui

Newbie here! I am building a quiz app using Swiftui, I built the view controller by previewing it in an iPhone 11 simulator.
And I thought the controlview would fit other iPhone sizes, like iPhone 8. Because Swiftui has a built-in auto layout.
But when I run the iPhone 8 simulator some of the content in the control view is not visible because they are below the screen.
Is there a way to fix it?
I tried to play with multiple Spacer() and different paddings but I can't seem to make it look good on both screen at the same time.
This is my code:
import SwiftUI
struct questionOne: View {
#State var totalClicked: Int = 0
#State var showDetails = false
#State var isSelected = false
var body: some View {
VStack {
TRpic().frame(width: 350.0, height: 233.0).cornerRadius(10).padding(.top, 80)
Spacer()
Text(verbatim: "What's the capital of Turkey?")
.font(.title)
.padding(.bottom, 60)
.frame(height: 100.0)
Button(action: {}) {
Text("Istanbul")
}.buttonStyle(MyButtonStyle())
Spacer()
Button(action: {self.isSelected.toggle()}) {
Text("Ankara")
}.buttonStyle(SelectedButtonStyle(isSelected: $isSelected))
Spacer()
Button(action: {}) {
Text("Athens")
} .buttonStyle(MyButtonStyle())
Spacer()
NavigationLink(destination: questionTwo()) {
VStack {
Text("Next Question")
Adview().frame(width: 150, height: 60)
}
}
}.edgesIgnoringSafeArea(.top)
}
}
struct MyButtonStyle: ButtonStyle {
func makeBody(configuration:
Self.Configuration) -> some View {
configuration.label
.padding(20)
.foregroundColor(.white)
.background(configuration.isPressed ? Color.red : Color.gray)
.cornerRadius(10.0)
}
}
struct SelectedButtonStyle: ButtonStyle {
#Binding var isSelected: Bool
public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding(20)
.foregroundColor(.white)
.background(isSelected ? Color.green : Color.gray)
.cornerRadius(10.0)
}
}
enter image description here
Screenshot

Being in the given context I guess you do not want a scroll view, so regarding spacing I suggest using a VStack with spacing parameter VStack(alignment: .center, spacing: n){ ... } and remove the Spacers, if between 2 views you need another distance than n, just use padding to add some extra space.
This should adjust everything to fit the height of any screen, including the image, so do not need a fixed frame for it.
But, you might have a very wide image that could go beyond safe area, so, you could set a maximum width for the image as being the screen width
struct questionOne: View {
var body: some View {
GeometryReader { geometryProxy in
VStack(alignment: .center, spacing: 20) {
TRpic().frame(maxWidth: geometryProxy.size.width, alignment: .center)
.padding([.leading, .trailing], 10)
.......
}
}
}

Related

Why do the views extend wider than the screen?

Edit: Substitute your "system name:" of choice. "pencil.circle" works fine. "edit" is not a valid SF Symbol.
(I've simplified my code so you can cut and paste. That's why you see .frame, resizable, etc. where much simpler code might your first instinct.)
I have created a view which is a vertical list of row items (table view).
Each row item has a horizontal view with two images inside it.
The images take up too much space and do not fit correctly on the screen:
import SwiftUI
#main
struct StackOverflowDemoApp: App {
var body: some Scene {
WindowGroup {
TandemView()
}
}
}
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
Spacer()
}
}
struct TandemView: View {
var body: some View {
HStack {
Spacer()
Image(systemName: "pencil")
.resizable()
.background(Color.orange)
.frame(height: 80)
.aspectRatio(1, contentMode: .fill)
PaddedImageView()
.frame(width: 200, height: 80)
}
.padding()
.fixedSize()
}
}
struct TandemView_Previews: PreviewProvider {
static var previews: some View {
TandemView()
}
}
The above is the closest I can get to the desired layout (it just needs to fit horizontally). I experimented with GeometryReader but that did not produce desired results.
Here are some things I tried:
The code as provided
NoConstraintsOnPencilOrHStack
NoConstraintsOnTandemView
NoConstraintsOnImageInPaddedViewButWithFrameConstraint
I am trying to get a row view which consists of two Images (my actual source consists of UIImage objects) that fits within the width of the screen.
Edit:
After Accepting cedricbahirwe's spot-on response, I was able to simplify the code further. New results:
I added at the top level
TandemView()
.padding(.horizontal)
I removed:
// Spacer()
at the end of PaddedImageView
updated TandemView -- changed both frames and removed 3 lines:
struct TandemView: View {
var body: some View {
HStack {
Spacer()
Image(systemName: "pencil")
.resizable()
.background(Color.orange)
.frame(width: 80, height: 80)
// .aspectRatio(1, contentMode: .fill)
PaddedImageView()
.frame(height: 80)
}
// .padding()
// .fixedSize()
}
}
This is happening because of the layout of PaddedImageView View, you can actually remove the Spacer since it is not needed there.
So change
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
Spacer()
}
}
to
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
}
}
Note:
SwiftUI Engine infers the layout of your view from the implementation of the body property. It's recommended to have one Parent View inside the body property.

Size a SwiftUI view to be safeAreaInsets.top + 'x'

I am trying to create a full bleed SwiftUI 'header' view in my scene. This header will sit within a List or a scrollable VStack.
In order to do this, I'd like to have my text in the header positioned below the safe area, but the full view should extend from the top of the screen (and thus, overlap the safe area). Here is visual representation:
V:[(safe-area-spacing)-(padding)-(text)]
here is my attempt:
struct HeaderView: View {
#State var spacing: CGFloat = 100
var body: some View {
HStack {
VStack(alignment: .leading) {
Rectangle()
.frame(height: spacing)
.opacity(0.5)
Text("this!").font(.largeTitle)
Text("this!").font(.headline)
Text("that!").font(.subheadline)
}
Spacer()
}
.frame(maxWidth: .infinity)
.background(Color.red)
.background(
GeometryReader { proxy in
Color.clear
.preference(
key: SafeAreaSpacingKey.self,
value: proxy.safeAreaInsets.top
)
}
)
.onPreferenceChange(SafeAreaSpacingKey.self) { value in
self.spacing = value
}
}
}
This however, does not seem to correctly size 'Rectangle'. How can I size a view according to the safe area?
Is this what you're looking for? I try to avoid using GeometryReader unless you really need it... I created a MainView, which has a background and a foreground layer. The background layer will ignore the safe areas (full bleed) but the foreground will stay within the safe area by default.
struct HeaderView: View {
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("this!").font(.largeTitle)
Text("this!").font(.headline)
Text("that!").font(.subheadline)
}
Spacer(minLength: 0)
}
}
}
struct MainView: View {
var body: some View {
ZStack {
// Background
ZStack {
}
.frame(maxWidth:. infinity, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
// Foreground
VStack {
HeaderView()
Spacer()
}
}
}
}
add an state to store desired height
#State desiredHeight : CGFloat = 0
then on views body :
.onAppear(perform: {
if let window = UIApplication.shared.windows.first{
let phoneSafeAreaTopnInset = window.safeAreaInsets.top
desiredHeight = phoneSafeAreaTopnInset + x
}
})
set the desiredHeight for your view .
.frame(height : desiredHeight)

SwiftUI: List is messing up animation for its subviews inside an HStack

I'm making a WatchOS app that displays a bunch of real-time arrival times. I want to place a view, a real-time indicator I designed, on the trailing end of each cell of a List that will be continuously animated.
The real-time indicator view just has two image whose opacity I'm continuously animating. This View by itself seems to work fine:
animated view by itself
However, when embedded inside a List then inside an HStack the animation seems to be affecting the position of my animated view not only its opacity.
animated view inside a cell
The distance this view travels seems to only be affected by the height of the HStack.
Animated view code:
struct RTIndicator: View {
#State var isAnimating = true
private var repeatingAnimation: Animation {
Animation
.spring()
.repeatForever()
}
private var delayedRepeatingAnimation: Animation {
Animation
.spring()
.repeatForever()
.delay(0.2)
}
var body: some View {
ZStack {
Image("rt-inner")
.opacity(isAnimating ? 0.2 : 1)
.animation(repeatingAnimation)
Image("rt-outer")
.opacity(isAnimating ? 0.2 : 1)
.animation(delayedRepeatingAnimation)
}
.frame(width: 16, height: 16, alignment: .center)
.colorMultiply(.red)
.padding(.top, -6)
.padding(.trailing, -12)
.onAppear {
self.isAnimating.toggle()
}
}
}
All code:
struct SwiftUIView: View {
var body: some View {
List {
HStack {
Text("Cell")
.frame(height: 100)
Spacer()
RTIndicator()
}.padding(8)
}
}
}
Here is found workaround. Tested with Xcode 12.
var body: some View {
List {
HStack {
Text("Cell")
.frame(height: 100)
Spacer()
}
.overlay(RTIndicator(), alignment: .trailing) // << here !!
.padding(8)
}
}
Although it's pretty hacky I have found a temporary solution to this problem. It's based on the answer from Asperi.
I have create a separate View called ClearView which has an animation but does not render anything visual and used it as a second overall in the same HStack.
struct ClearView: View {
#State var isAnimating = false
var body: some View {
Rectangle()
.foregroundColor(.clear)
.onAppear {
withAnimation(Animation.linear(duration: 0)) {
self.isAnimating = true
}
}
}
}
var body: some View {
List {
HStack {
Text("Cell")
.frame(height: 100)
Spacer()
}
.overlay(RTIndicator(), alignment: .trailing)
.overlay(ClearView(), alignment: .trailing)
.padding(8)
}
}

SwiftUI: animating between Single and HStack views

Goal:
1- Shows a view (Blue) that covers entire screen
2- When tapped on bottom (top right corner), it shows an HStack animating right side HStack (Green) "Slide Offset animation".
import SwiftUI
struct ContentView: View {
#State var showgreen = false
var body: some View {
NavigationView {
HStack {
Rectangle()
.foregroundColor(.blue)
if showgreen {
Rectangle()
.foregroundColor(.green)
.offset(x: showgreen ? 0 : UIScreen.main.bounds.width)
.animation(.easeInOut)
}
}
.navigationBarItems(trailing:
Button(action: { self.showgreen.toggle() }) {
Image(systemName: "ellipsis")
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
The code works, however I cannot get the Green "Slide Offset animation" to work. Really appreciate any help! : )
Instead of using the if conditional, you need the green rectangle to be already present, and just offscreen. When showgreen toggles, you need to shrink the size of the blue rectangle, which will make room for the green rectangle.
struct ContentView: View {
#State var showgreen = false
var body: some View {
NavigationView {
HStack {
Rectangle()
.foregroundColor(.blue)
.frame(width: showgreen ? UIScreen.main.bounds.width / 2 : UIScreen.main.bounds.width)
.animation(.easeInOut)
Rectangle()
.foregroundColor(.green)
.animation(.easeInOut)
}
.navigationBarItems(trailing:
Button(action: { self.showgreen.toggle() }) {
Image(systemName: "ellipsis")
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}

Square Text using aspectRatio in SwiftUI

I'm trying to achieve a following layout using Swift UI…
struct ContentView : View {
var body: some View {
List(1...5) { index in
HStack {
HStack {
Text("Item number \(index)")
Spacer()
}.padding([.leading, .top, .bottom])
.background(Color.blue)
Text("i")
.font(.title)
.italic()
.padding()
.aspectRatio(1, contentMode: .fill)
.background(Color.pink)
}.background(Color.yellow)
}
}
}
I'd like the Text("i") to be square, but setting the .aspectRatio(1, contentMode: .fill) doesn't seem to do anything…
I could set the frame width and height of the text so it's square, but it seems that setting the aspect ratio should achieve what I want in a more dynamic way.
What am I missing?
I think this is what you're looking for:
List(1..<6) { index in
HStack {
HStack {
Text("Item number \(index)")
Spacer()
}
.padding([.leading, .top, .bottom])
.background(Color.blue)
Text("i")
.font(.title)
.italic()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(1, contentMode: .fill)
.background(Color.pink)
.fixedSize(horizontal: true, vertical: false)
.padding(.leading, 6)
}
.padding(6)
.background(Color.yellow)
}
The answer being said, i don't recommend giving SwiftUI too much freedom to decide the sizings. one of the biggest SwiftUI problems right now is the way it decides how to fit the views into each other. if something goes not-so-good on SwiftUI's side, it can result in too many calls to the UIKit's sizeToFit method which can slowdown the app, or even crash it.
but, if you tried this solution in a few different situations and it worked, you can assume that in your case, giving SwiftUI the choice of deciding the sizings is not problematic.
The issue is due to used different fonts for left/right sides, so paddings generate different resulting area.
Here is possible solution. The idea is to give right side rect based on default view size of left side text (this gives ability to track dynamic fonts sizes as well, automatically).
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State private var height = CGFloat.zero
var body: some View {
List(1...5, id: \.self) { index in
HStack(spacing: 8) {
HStack {
Text("Item number \(index)")
Spacer()
}
.padding([.leading, .top, .bottom])
.background(GeometryReader {
Color.blue.preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
})
Text("i")
.italic()
.font(.title)
.frame(width: height, height: height)
.background(Color.pink)
}
.padding(8)
.background(Color.yellow)
.onPreferenceChange(ViewHeightKey.self) {
self.height = $0
}
}
}
}
struct ViewHeightKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
I managed to recreate the view in your first screenshot in SwiftUI. I wasn't sure on how much padding you wanted so I defined a private immutable variable for this value
The blue view is the one that will have the text content and could change in size so by using a GeometryReader you can get the size of the blue view and then use the height value from the size to set the width and height of the pink view. This means that whatever the height of the blue view is, the pink view will follow keeping an equal aspect ratio
The SizeGetter view below is used to get any views size using a GeometryReader and then binds that value back to a #State variable in the ContentView. Because the #State and #Binding property wrappers are being used, whenever the blueViewSize is updated SwiftUI will automatically refresh the view.
The SizeGetter view can be used for any view and is implemented using the .background() modifier as shown below
struct SizeGetter: View {
#Binding var size: CGSize;
var body: some View {
// Get the size of the view using a GeometryReader
GeometryReader { geometry in
Group { () -> AnyView in
// Get the size from the geometry
let size = geometry.frame(in: .global).size;
// If the size has changed, update the size on the main thread
// Checking if the size has changed stops an infinite layout loop
if (size != self.size) {
DispatchQueue.main.async {
self.size = size;
}
}
// Return an empty view
return AnyView(EmptyView());
}
}
}
}
struct ContentView: View {
private let padding: Length = 10;
#State private var blueViewSize: CGSize = .zero;
var body: some View {
List(1...5) { index in
// The yellow view
HStack(spacing: self.padding) {
// The blue view
HStack(spacing: 0) {
VStack(spacing: 0) {
Text("Item number \(index)")
.padding(self.padding);
}
Spacer();
}
.background(SizeGetter(size: self.$blueViewSize))
.background(Color.blue);
// The pink view
VStack(spacing: 0) {
Text("i")
.font(.title)
.italic();
}
.frame(
width: self.blueViewSize.height,
height: self.blueViewSize.height
)
.background(Color.pink);
}
.padding(self.padding)
.background(Color.yellow);
}
}
}
In my opinion it is better to set the background colour of a VStack or HStack instead of the Text view directly because you can then add more text and other views to the stack and not have to set the background colour for each one
I was searching very similar topic "Square Text in SwiftUI", came across your question and I think I've found quite simple approach to achieve your desired layout, using GeometryProxy to set width and heigh of the square view from offered geometry.size.
Checkout the code below, an example of TableCellView which can be used within List View context:
import SwiftUI
struct TableCellView: View {
var index: Int
var body: some View {
HStack {
HStack {
Text("Item number \(index)")
.padding([.top, .leading, .bottom])
Spacer()
}
.background(Color(.systemBlue))
.layoutPriority(1)
GeometryReader { geometry in
self.squareView(geometry: geometry)
}
.padding(.trailing)
}
.background(Color(.systemYellow))
.padding(.trailing)
}
func squareView(geometry: GeometryProxy) -> some View {
Text("i")
.frame(width: geometry.size.height, height: geometry.size.height)
.background(Color(.systemPink))
}
}