How can fix the size of view in mac in SwiftUI? - swift

I am experiencing a new change with Xcode Version 14.1 about view size in macOS, in older version if I gave a frame size or use fixedSize modifier, the view would stay in that size, but with 14.1 I see that view could get expended even if I use frame or fixedSize modifier, which is not what I expect. For example I made an example to show the issue, when I update the size to 200.0, the view/window stay in bigger size, which I expect it shrinks itself to smaller size, so how can I solve the issue?
struct ContentView: View {
#State private var sizeOfWindow: CGFloat = 400.0
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
Button("update", action: {
if (sizeOfWindow == 200.0) { sizeOfWindow = 400.0 }
else { sizeOfWindow = 200.0 }
print(sizeOfWindow)
})
}
.frame(width: sizeOfWindow, height: sizeOfWindow)
.fixedSize()
}
}

Credit:
Credit to https://developer.apple.com/forums/thread/708177?answerId=717217022#717217022
Approach:
Use .windowResizability(.contentSize)
Along with windowResizability use one of the following:
if you want set size based on the content size use fixedSize()
If you want to hardcode the frame then directly set .frame(width:height:)
Code
#main
struct DemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.frame(width: 200, height: 200) //or use fixed size and rely on content
}
.windowResizability(.contentSize)
}
}

Related

How can I access the height of a Mac app's main window using SwiftUI?

I've been following a tutorial to learn SwiftUI and build an app for my Mac. The tutorial has been assuming that I'm writing for an iPhone, which hasn't been a problem until just now.
I need to access the height of the screen, or in my case the window. The tutorial is using UIScreen.main.bounds.height to do so, but since UIScreen isn't available for Mac apps, I'm trying to find the Mac equivalent. I'm pretty sure that the window that I'm trying to access is of type NSWindow, but I don't see any ways of accessing width and height of an NSWindow in the documentation. On top of that, I don't know how I could even go about accessing the NSWindow since there I don't see a variable or anything holding that information.
This is the tutorial I'm following, and the part where I need the height is at the end of line 36 of this gist where it says UIScreen.main.bounds.height.
Is there an easy way to do this in a Mac app? I appreciate any help that can be offered.
In SwiftUI, independently if you are targeting iOS or macOS, to get the size of the screen you have to wrap the main view with GeometryReader. This way, you can easily access its size.
struct ContentView: View {
var body: some View {
GeometryReader { geo in
NavigationStack {
HStack {
NavigationLink(destination: One(screenSize: geo.size)) {
Text("One")
}
NavigationLink(destination: Two(screenSize: geo.size)) {
Text("Two")
}
}
}
.frame(width: geo.size.width, height: geo.size.height)
}
}
}
struct One: View {
let screenSize: CGSize
var body: some View {
Text("I'm view One")
.frame(width: screenSize.width, height: screenSize.height)
.background(Color.green)
}
}
struct Two: View {
let screenSize: CGSize
var body: some View {
Text("I'm view Two")
.frame(width: screenSize.width / 2, height: screenSize.height / 2)
.background(Color.red)
}
}
Apart from GeometricReader, you can also use UIScreen;
GeometryReader { geo in
//Your code here
}
.frame(width: geo.size.width, height: geo.size.height)
Or
.frame(width: UIScreen.main.bounds.width, height:
UIScreen.main.bounds.height)

Scaling down a text's font size to fit its length to another text in SwiftUI

Consider the following code:
VStack{
Text("A reference text")
Text("(Adjust the length of this line to match the one above)")
}
I want the second Text() to be scaled down in font size to be exactly the length of the sentence above. I've already tried:
Text("(Adjust the length of this line to match the one above)")
.scaledToFit()
.minimumScaleFactor(0.01)
but it seems to not work. How can I achieve this?
Here is possible simple approach (tested with Xcode 13.4 / iOS 15.5)
Update: found even more simpler, based on the same concept (works with dynamic sizes as well)
Text("A reference text")
.padding(.bottom) // << spacing can be tuned
.overlay(
Text("(Adjust the length of this line to match the one above)")
.scaledToFit()
.minimumScaleFactor(0.01)
, alignment: .bottom) // << important !!
Original:
VStack {
let ref = "A reference text"
let target = "(Adjust the length of this line to match the one above)"
Text(ref)
Text(ref).opacity(0)
.overlay(Text(target)
.scaledToFit()
.minimumScaleFactor(0.01)
)
}
You need to get the size of the reference view
struct ScaledTextView: View {
#State var textSize: CGSize = CGSize.zero
var body: some View {
VStack{
Text("A reference text")
.viewSize(viewSize: $textSize) //Get initial size and any changes
Text("(Adjust the length of this line to match the one above)")
//If you want it to be a single line set the limit
//.lineLimit(1)
//Set the dependent's view frame, you can just set the width if you want, height is optional
.frame(width: textSize.width, height: textSize.height)
.scaledToFit()
.minimumScaleFactor(0.01)
}
}
}
//Make it reusable and keep the view clean
extension View{
func viewSize(viewSize: Binding<CGSize>) -> some View{
return self.background(
//Get the size of the View
GeometryReader{geo in
Text("")
//detects changes such as landscape, etc
.onChange(of: geo.size, perform: {value in
//set the size
viewSize.wrappedValue = value
})
//Initial size
.onAppear(perform: {
//set the size
viewSize.wrappedValue = geo.size
})
}
)
}
}
This is a method which doesn't fix the smaller text's height.
struct ContentView: View {
#State private var width: CGFloat?
var body: some View {
VStack {
Text("A reference text")
.background(
GeometryReader { geo in
Color.clear.onAppear { width = geo.size.width }
}
)
Text("(Adjust the length of this line to match the one above)")
.scaledToFit()
.minimumScaleFactor(0.01)
}
.frame(width: width)
.border(Color.red)
}
}
Result:

Arrange custom views in SwiftUI without space or overlap

I'm trying to build a UI (SwiftUI / iOS) out of a number of custom views.
All those custom views have a defined aspect ratio or ratio for their frame.
Here's a simplified version of such a custom view:
struct TestView: View {
var body: some View {
GeometryReader { geometry in
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(height: geometry.size.width / 3)
}
}
}
My ContentView currently looks like that:
struct TestContentView: View {
var body: some View {
GeometryReader {geomerty in
VStack {
TestView()
TestView()
}
}
}
}
I would like to have the two rectangles to be positioned right below each other (at the top of the screen). So without any space between them. So a bit like an old-fashioned UITableView with only to rows.
But whatever I try, I only get one of two results:
They are equally spread out over the screen (vertically)
They overlap (= the view on the top only gets a vertical size of 20
The only solution I've found so far is to define the frame size of the sub-views also in the TestContentView(). But that seems to be quite un-SwiftUI.
Thanks!
Remove the GeometryReader from your content view, since it isn't doing anything
You said that your TestView has a defined aspect ratio, but, in fact, it doesn't -- it just has a defined width. If you do define an aspect ratio, it starts working as expected:
struct TestView: View {
var body: some View {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.aspectRatio(3, contentMode: .fit)
}
}
struct TestContentView: View {
var body: some View {
VStack(spacing: 0) {
TestView()
TestView()
Spacer()
}
}
}

How can I stop position modifier from changing size of View after modification in SwiftUI?

I am importing my View/Content to a CustomView for some modification and working on, if I use position modifier on my input View/Content and before importing, after importing that View/Content, it will takes the screen Size for itself, regardless if the original Size was too large or too small, in other words position modifier, returns kind of all screen in any case. so that make problem and issue for me, because I need to read the true and real size of imported View/Content, how can I read the true and real size of my imported View/Content in my CustomView? with that in mind that I need to use position on my View/Content before getting imported. and it is not so cool or convenient to import the Size with the View together. thanks
import SwiftUI
struct ContentView: View {
var body: some View {
CustomView {
// Rectangle()
// .fill(Color.blue)
// .frame(width: 200, height: 1000, alignment: .center)
// //.position(x: 200, y: 100) // <<: this part make our View lose its original size!
Text("Hello, world!")
.padding()
.background(Color.yellow)
//.position(x: 200, y: 100) // <<: this part make our View lose its original size!
}
}
}
struct CustomView<Content: View>: View {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) { self.content = content }
var body: some View {
return content()
.background( GeometryReader { geometry in Color.clear.onAppear() { print(geometry.size) } } )
}
}
Use offset() instead of position() because it does not change the original dimensions of CustomView though it changes location of Text("Hello, world!").
Text("Hello, world!")
.padding()
.background(Color.yellow)
.offset(x: 0, y: -350)
This article explains the difference of them in depth.
https://www.hackingwithswift.com/books/ios-swiftui/absolute-positioning-for-swiftui-views

SwiftUI - Using GeometryReader Without Modifying The View Size

I have a header view which extends its background to be under the status bar using edgesIgnoringSafeArea. To align the content/subviews of the header view correctly, I need the safeAreaInsets from GeometryReader. However, when using GeometryReader, my view doesn't have a fitted size anymore.
Code without using GeometryReader
struct MyView : View {
var body: some View {
VStack(alignment: .leading) {
CustomView()
}
.padding(.horizontal)
.padding(.bottom, 64)
.background(Color.blue)
}
}
Preview
Code using GeometryReader
struct MyView : View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
CustomView()
}
.padding(.horizontal)
.padding(.top, geometry.safeAreaInsets.top)
.padding(.bottom, 64)
.background(Color.blue)
.fixedSize()
}
}
}
Preview
Is there a way to use GeometryReader without modifying the underlying view size?
Answer to the question in the title:
It is possible to wrap the GeometryReader in an .overlay() or .background(). Doing so will mitigate the layout changing effect of GeometryReader. The view will be laid out as normal, the GeometryReader will expand to the full size of the view and emit the geometry into its content builder closure.
It's also possible to set the frame of the GeometryReader to stop its eagerness in expanding.
For example, this example renders a blue rectangle, and a "Hello world" text inside at 3/4th the height of the rectangle (instead of the rectangle filling up all available space) by wrapping the GeometryReader in an overlay:
struct MyView : View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(height: 150)
.overlay(GeometryReader { geo in
Text("Hello world").padding(.top, geo.size.height * 3 / 4)
})
Spacer()
}
}
Another example to achieve the same effect by setting the frame on the GeometryReader:
struct MyView : View {
var body: some View {
GeometryReader { geo in
Rectangle().fill(Color.blue)
Text("Hello world").padding(.top, geo.size.height * 3 / 4)
}
.frame(height: 150)
Spacer()
}
}
However, there are caveats / not very obvious behaviors
1
View modifiers apply to anything up to the point that they are applied, and not to anything after. An overlay / background that is added after .edgesIgnoringSafeArea(.all) will respect the safe area (not participate in ignoring the safe area).
This code renders "Hello world" inside the safe area, while the blue rectangle ignores the safe area:
struct MyView : View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(height: 150)
.edgesIgnoringSafeArea(.all)
.overlay(VStack {
Text("Hello world")
Spacer()
})
Spacer()
}
}
2
Applying .edgesIgnoringSafeArea(.all) to the background makes GeometryReader ignore the SafeArea:
struct MyView : View {
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(height: 150)
.overlay(GeometryReader { geo in
VStack {
Text("Hello world")
// No effect, safe area is set to be ignored.
.padding(.top, geo.safeAreaInsets.top)
Spacer()
}
})
.edgesIgnoringSafeArea(.all)
Spacer()
}
}
It is possible to compose many layouts by adding multiple overlays / backgrounds.
3
A measured geometry will be available to the content of the GeometryReader. Not to parent or sibling views; even if the values are extracted into a State or ObservableObject. SwiftUI will emit a runtime warning if that happens:
struct MyView : View {
#State private var safeAreaInsets = EdgeInsets()
var body: some View {
Text("Hello world")
.edgesIgnoringSafeArea(.all)
.background(GeometryReader(content: set(geometry:)))
.padding(.top, safeAreaInsets.top)
Spacer()
}
private func set(geometry: GeometryProxy) -> some View {
self.safeAreaInsets = geometry.safeAreaInsets
return Color.blue
}
}
I tried with the previewLayout and I see what you mean. However, I think the behavior is as expected. The definition of .sizeThatFits is:
Fit the container (A) to the size of the preview (B) when offered the
size of the device (C) on which the preview is running.
I intercalated some letters to define each part and make it more clear:
A = the final size of the preview.
B = The size of what you are modifying with .previewLayout(). In the first case, it's the VStack. But in the second case, it's the GeometryReader.
C = The size of the screen of the device.
Both views act differently, because VStack is not greedy, and only takes what it needs. GeometryReader, on the other side, tries to have it all, because it does not know what its child will want to use. If the child wants to use less, it can do it, but it has to start by being offered everything.
Perhaps if you edit your question to explain exactly what you would like to accomplish, I can refine my answer a little.
If you would like GeometryReader to report the size of the VStack. you may do so by putting it inside a .background modifier. But again, I am not sure what's the goal, so maybe that's a no go.
I have written an article about the different uses of GeometryReader. Here's the link, in case it helps: https://swiftui-lab.com/geometryreader-to-the-rescue/
UPDATE
Ok, with your additional explanation, here you have a working solution. Note that the Preview will not work, because safeInsets are reported as zero. On the simulator, however, it works fine:
As you will see, I use view preferences. They are not explained anywhere, but I am currently writing an article about them that I will post soon.
It may all look too verbose, but if you find yourself using it too often, you can encapsulate it inside a custom modifier.
import SwiftUI
struct InsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
struct InsetGetter: View {
var body: some View {
GeometryReader { geometry in
return Rectangle().preference(key: InsetPreferenceKey.self, value: geometry.safeAreaInsets.top)
}
}
}
struct ContentView : View {
var body: some View {
MyView()
}
}
struct MyView : View {
#State private var topInset: CGFloat = 0
var body: some View {
VStack {
CustomView(inset: topInset)
.padding(.horizontal)
.padding(.bottom, 64)
.padding(.top, topInset)
.background(Color.blue)
.background(InsetGetter())
.edgesIgnoringSafeArea(.all)
.onPreferenceChange(InsetPreferenceKey.self) { self.topInset = $0 }
Spacer()
}
}
}
struct CustomView: View {
let inset: CGFloat
var body: some View {
VStack {
HStack {
Text("C \(inset)").color(.white).fontWeight(.bold).font(.title)
Spacer()
}
HStack {
Text("A").color(.white)
Text("B").color(.white)
Spacer()
}
}
}
}
I managed to solve this by wrapping the page main view inside a GeometryReader and pass down the safeAreaInsets to MyView. Since it is the main page view where we want the entire screen thus it is ok to be as greedy as possible.