I want to resize an Image frame to be a square that takes the same width of the iPhone's screen and consequently the same value (screen width) for height.
The following code don't work cause it gives the image the same height of the view.
var body: some View {
Image("someImage")
.resizable()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.clipped()
}
You can create a UIScreen extension for the same. Like:
extension UIScreen{
static let screenWidth = UIScreen.main.bounds.size.width
static let screenHeight = UIScreen.main.bounds.size.height
static let screenSize = UIScreen.main.bounds.size
}
Usage:
UIScreen.screenWidth
Try using Geometry Reader
let placeholder = UIImage(systemName: "photo")! // SF Symbols
struct ContentView: View {
var body: some View {
GeometryReader { geometry in
Image(uiImage: placeholder)
.resizable()
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
// .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.clipped()
}
}
}
You can use UIScreen.main.bounds .width or .height
.frame(
width:UIScreen.main.bounds.width,
height:UIScreen.main.bounds.height
)
I've come up with a solution using Environment Keys, by creating the following:
private struct MainWindowSizeKey: EnvironmentKey {
static let defaultValue: CGSize = .zero
}
extension EnvironmentValues {
var mainWindowSize: CGSize {
get { self[MainWindowSizeKey.self] }
set { self[MainWindowSizeKey.self] = newValue }
}
}
Then by reading the size from where the window is created:
var body: some Scene {
WindowGroup {
GeometryReader { proxy in
ContentView()
.environment(\.mainWindowSize, proxy.size)
}
}
}
Finally I can directly get the window size in any SwiftUI view, and it changes dynamically (on device rotation or window resizing on macOS):
#Environment(\.mainWindowSize) var mainWindowSize
The simplest way would be to make the image resizable and set the aspect ratio to 1.0:
var body: some View {
Image("someImage")
.resizable()
.aspectRatio(1.0, contentMode: .fit)
}
Related
I am trying to make a SwiftUI TextEditor with a Divider that adapts its position to stay under the bottom-most line of text inside of a edit-bio section of the app.
Note: I have a frame on my TextEditor so that it doesn't take up the whole-screen
Right now the Divider is static and stays in one place. Is there a built-in way to make the divider stay under the bottom most line of text?
I would think the Spacer would have given me this behavior?
Thank you!
struct EditBio: View {
#ObservedObject var editProfileVM: EditProfileViewModel
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM.bio)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
Divider().frame(height: 1).background(.white)
Spacer()
}
}
}
It is doing exactly what you told it to do. But a background color on your TextEditor. You will see that it has a height of 200 + a spacing of 10 from the VStack.
I changed your code to make it obvious:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.background(Color.gray)
Divider().frame(height: 1).background(.red)
Spacer()
}
}
}
to produce this:
You can see the TextEditor naturally wants to be taller than 200, but that is limiting it. Therefore, the Spacer() is not going to cause the TextEditor to be any smaller.
The other problem that setting a fixed frame causes will be that your text will end up off screen at some point. I am presuming what you really want is a self sizing TextEditor that is no larger than it's contents.
That can be simply done with the following code:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
SelfSizingTextEditor(text: $editProfileVM)
// Frame removed for the image below.
// .frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.foregroundColor(.white)
// made the .top padding to be .vertical
.padding(.vertical, 70)
.padding([.leading, .trailing], 50)
.background(Color.gray)
Divider().frame(height: 5).background(.red)
Spacer()
}
}
}
struct SelfSizingTextEditor: View {
#Binding var text: String
#State var textEditorSize = CGSize.zero
var body: some View {
ZStack {
Text(text)
.foregroundColor(.clear)
.copySize(to: $textEditorSize)
TextEditor(text: $text)
.frame(height: textEditorSize.height)
}
}
}
extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
func copySize(to binding: Binding<CGSize>) -> some View {
self.readSize { size in
binding.wrappedValue = size
}
}
}
producing this view:
I'm working with an iPad application using SwiftUI. Now I have a MenuView Stacked on a MapView. The MenuView is supposed to imitate the iPad built-in app Map's menu. Now I'm puzzling about how to implement the draggable bar to adjust the height of the MenuView. I have implemented a clumsy one(code attached below), and it's also stuttered and frame-dropping. I'm wondering if there is already a ready-to-use widget in SwiftUI, because both the Map and iPadOS's floating mini-keyboard have similar things (pictures attached below). Or is there any good ways to implement it? I would appreciate it if you could give me some advice :)
My implementation:
// MenuView.swift
struct MenuView: View {
#Binding var menu_height: CGFloat
var bottom_slider: some View {
ZStack {
Rectangle()
.fill(Color.white)
.opacity(0.011)
.frame(width: 400, height: 28, alignment: .center)
.edgesIgnoringSafeArea(.all)
.padding(.top,-5)
RoundedRectangle(cornerRadius: 25)
.fill(Color.gray)
.frame(width: 100, height: 6, alignment: .center)
.padding(.bottom, 4)
.opacity(0.6)
}
}
var body: some View {
VStack{
//...
//...
//...
}
ScrollView{
//...
//...
//...
}
bottom_slider
.frame(height: 20, alignment: .center)
.gesture(DragGesture().onChanged({ value in
let a = self.menu_height + (value.location.y - value.startLocation.y)
if a<150 {
self.menu_height = 150
} else if a>750{
self.menu_height = 750
} else {
self.menu_height = a
}}))
}
}
// ContentView.swift
struct ContentView: View {
#State var munu_height : CGFloat = 400
var body: some View {
ZStack {
MyMapView(...)
.edgesIgnoringSafeArea(.all)
MenuView(menu_height: $menu_height, ...)
.padding(.horizontal, 10.0)
.frame(width: 400, height: height)
.background(BlurView(colorScheme: colorScheme))
.clipShape(RoundedRectangle(cornerRadius: 15))
.shadow(radius: 8)
.offset(x: 10, y:10)
}
}
}
My implementation pic:
Built-in Map application:
The iOS floating keyboard:
I would like to get the frame size of my image, to use for some calculations in a drag gesture recognizer (basically normalize the touch coordinates of the drag).
I have tried to use GeometryReader but it expands to fill the whole height and thus the reported height is not correct.
How can I fix this behavior? Is there any other way of getting the view size of the image?
struct ContentView: View {
var body: some View {
ZStack(alignment: .center) {
GeometryReader { reader in
Image(uiImage: UIImage(named: "test")!)
.resizable()
.aspectRatio(contentMode: .fit)
.shadow(radius: 5)
//.gesture(dragGesture(forSize: reader.size))
}
.background(Color.red)
}
}
}
Use AVMakeRect from AVFoundation. For more
struct ContentView: View {
var body: some View {
GeometryReader { reader in
ZStack(alignment: .center) {
if let uiImage = UIImage(named: "test") {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
.shadow(radius: 5)
.onReceive(Just(reader), perform: { _ in
let localFrame = reader.frame(in: .local)
let imageFrame = AVMakeRect(aspectRatio: uiImage.size, insideRect: localFrame)
print("Full frame : ", localFrame)
print("Image frame : ", imageFrame)
})
}
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
}.background(Color.red)
}
}
I'd like to ask you some help. I'm trying to capture VStack height using geometry and then based on that VStack height value, calculate its child element's height (inside VStack).
Image of my current view of VStack
I used .frame outside of VStack to fill the whole screen. Then I used .border to visually check if it actually fills the screen (it works fine)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)
The problem can be seen inside the VStack, the height is displayed as GCFloat 734. Even though the border size is a lot bigger.
struct BodyView: View {
#State var stackHeight : CGFloat = 0
var body: some View {
GeometryReader { geometry in
VStack {
Text("VStack size: \(self.stackHeight)")
.bold()
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.background(Color.green)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)
.onAppear{
self.stackHeight = geometry.size.height
}
.border(Color.purple, width: 5)
}
}
}
struct BodyView_Previews: PreviewProvider {
static var previews: some View {
BodyView()
}
}
How could I capture the actual size of the VStack when it finishes loading?
When I trigger (onAppear) the VStack height appears correct, however I need it to be captured instantly.
If I understand your questions, you're very close — you just need to use to GeometryProxy type:
import SwiftUI
struct BodyView: View {
var body: some View {
GeometryReader { geometry in
VStack {
Text("VStack size: \(geometry.size.height)") // no need for state var anymore
.bold()
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.background(Color.green)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: Alignment.topLeading)
.border(Color.purple, width: 5)
}
}
}
struct BodyView_Previews: PreviewProvider {
static var previews: some View {
BodyView()
}
}
A GeometryReader will lay itself out again if its size changes.
My goal is to make sure Text in a container to scale according to its parent. It works well when the container only contains one Text view, as following:
import SwiftUI
struct FontScalingExperiment: View {
var body: some View {
Text("Hello World ~!")
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
.scaledToFill()
)
}
}
struct FontScalingExperiment_Previews: PreviewProvider {
static var previews: some View {
Group {
FontScalingExperiment()
.previewLayout(.fixed(width: 100, height: 100))
FontScalingExperiment()
.previewLayout(.fixed(width: 200, height: 200))
FontScalingExperiment()
.previewLayout(.fixed(width: 300, height: 300))
FontScalingExperiment()
.previewLayout(.fixed(width: 400, height: 400))
}
}
}
the result:
However, when we have more complex View, we cant use same approach to automatically scale the text based on its parent size, for example:
import SwiftUI
struct IndicatorExperiment: View {
var body: some View {
VStack {
HStack {
Text("Line 1")
Spacer()
}
Spacer()
VStack {
Text("Line 2")
Text("Line 3")
}
Spacer()
Text("Line 4")
}
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
)
.aspectRatio(1, contentMode: .fit)
}
}
struct IndicatorExperiment_Previews: PreviewProvider {
static var previews: some View {
Group {
IndicatorExperiment()
.previewLayout(.fixed(width: 100, height: 100))
IndicatorExperiment()
.previewLayout(.fixed(width: 200, height: 200))
IndicatorExperiment()
.previewLayout(.fixed(width: 300, height: 300))
IndicatorExperiment()
.previewLayout(.fixed(width: 400, height: 400))
}
}
}
Simply adding these 3 modifiers:
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
wont produce result like the first example; Text enlarged beyond the frame.
I did successfully, produce the result that I want by using GeometryReader then scale the font size based on geometry.size.width. Is this the only approach for achieving the desired result in SwiftUI?
You can try make all the Texts the same height. To do this you will need to set the padding and spacing explicitly, so this will scale rather than the fixed default values.
Also, the Spacer() didn't make much sense here - if the requirement was that all the Text stay the same size, the Spacer would just make all the text small. For Text to scale based on space, and where Spacer tries to use as much space as possible, it's a contradiction. Instead, I decided to just set the VStack's spacing in the initializer.
Working code:
struct IndicatorExperiment: View {
private let size: CGFloat
private let padding: CGFloat
private let primarySpacing: CGFloat
private let secondarySpacing: CGFloat
private let textHeight: CGFloat
init(size: CGFloat) {
self.size = size
padding = size / 10
primarySpacing = size / 15
secondarySpacing = size / 40
let totalHeights = size - padding * 2 - primarySpacing * 2 - secondarySpacing
textHeight = totalHeights / 4
}
var body: some View {
VStack(spacing: primarySpacing) {
HStack {
scaledText("Line 1")
Spacer()
}
.frame(height: textHeight)
VStack(spacing: secondarySpacing) {
scaledText("Line 2")
scaledText("Line 3")
}
.frame(height: textHeight * 2 + secondarySpacing)
scaledText("Line 4")
}
.padding(padding)
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.yellow)
)
.aspectRatio(1, contentMode: .fit)
.frame(width: size, height: size)
}
private func scaledText(_ content: String) -> some View {
Text(content)
.font(.system(size: 500))
.minimumScaleFactor(0.01)
.lineLimit(1)
.frame(height: textHeight)
}
}
Code to test with:
struct ContentView: View {
var body: some View {
ScrollView {
VStack(spacing: 50) {
IndicatorExperiment(size: 100)
IndicatorExperiment(size: 200)
IndicatorExperiment(size: 300)
IndicatorExperiment(size: 400)
}
}
}
}
Result:
Using GeometryReader and a .minimumScaleFactor modifier would probably the only way to scale text in a view. To have more control on sizing, one possible way is to provde the .frame size from the parent view.
Scalable Text View
GeometryReader { geo in
Text("Foo")
.font(
.system(size: min(geo.size.height, geo.size.width) * 0.95))
.minimumScaleFactor(0.05)
.lineLimit(1)
}
Parent View that uses the Scalable Text View
GeometryReader { geo in
ScaleableText()
.frame(width: geo.size.width, height: geo.size.height)
}