SwiftUI: Decide which content the keyboard overlaps - swift

I'm trying to find a way to mark which content is overlapped by the keyboard that pops up when a TextField is focused.
Consider the minimal example below:
VStack {
TextField(text: $text) {}
Spacer()
Circle()
.frame(width: 100, height: 100)
}
I'm looking for a custom modifier that I could apply to the Circle. This would dictate whether the keyboard should push the Circle up above the keyboard, as is default, or if the keyboard should overlap the Circle (I'm not sure how to get the keyboard to simply overlap a view, either).
So far I've found primarily Objective-C solutions... Are there any SwiftUI ways to go about this? Thank you for the help!

If I understood correctly your goal then you don't need custom modifier - there is standard modifier, which you can either add or remove, or use with some condition.
VStack {
TextField(text: $text) {}
.textFieldStyle(.roundedBorder)
VStack {
Spacer()
Circle()
.frame(width: 100, height: 100)
}
.ignoresSafeArea(.keyboard, edges: .bottom) // << here !!
//.ignoresSafeArea(yourFlag ? .keyboard : .container, edges: .bottom) // << alternate
}

Related

How to keep an item exactly in the center of an HStack with other items in the HStack

I have two items in an HStack. I want one of them to be perfectly horizontally centered and the other to be on the left side (have an alignment of .leading). Here is my code so far:
HStack {
Text("1")
.frame(alignment: .leading)
Text("Hello")
.frame(alignment: .center)
}
I have figured out that the .overlay method works. However, I still want Text("Hello") to be aware of the position of Text("1") so that if Text("Hello") were a longer string, it would know not to cover up Text("1"), which is exactly what happens when the .overlay function is used. So I'm wondering if there are any other solutions.
You can add one more Text view inside HStack as hidden which have the same content as your left align text view along with Spacer. Something like this.
HStack {
Text("1")
Spacer()
Text("Good Morning, Hello World. This is long message")
.multilineTextAlignment(.center)
Spacer()
Text("1") // Set the same content as left aligned text view
.hidden()
}
.padding(.horizontal)
Preview
Not sure if it's the best way, but you can use a GeometryReader to get the screen width and skip the first half.
To place the text exactly in the center, you should also calculate the width of the text. you can use a simple extension to get the size of your text:
extension String {
public func size(withFont font: UIFont) -> CGSize {
(self as NSString).size(withAttributes: [.font: font])
}
}
Here is the example code:
GeometryReader { geo in
HStack {
Spacer()
.frame(width: (geo.size.width - "center".size(withFont: UIFont.systemFont(ofSize: 18)).width) / 2)
HStack {
Text("center")
Text("another")
}
}
}

SwiftUI Center Content alignment without supporting frames

I'm having trouble trying to center a single element to emulate the navigation modal with a close button.
I would like to center content without using a supporting Rectangle on the sides or spacers.
What i'm trying to achieve is whenever the text grow, if it reaches the left sides where there is the close xmark button it should try to push itself on the right where there is available space until it reaches the right border and after wrap itself if there are no available space on the both sides.
here are some pictures:
expected result 1
expected result 2
current solution short text
current solution long text
i tried using long and short text to test the content behaviour
Currently this is the start of the code and basically i would like to avoid to add the blue rectangle (that would be usually clear)
struct TestAlignmentSwiftUIView: View {
var body: some View {
HStack(spacing: 0) {
Rectangle().fill(Color.blue).frame(width: 44, height: 44)
Text("aaa eee aaa")
.background(Color.red)
.padding(5)
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: 44, height: 44)
.background(Color.yellow)
})
}
.background(Color.green)
}
}
What i've tried so far but doesn't resolve the issue if the code inside the text component grow:
Using a zstack where i place the text and the close button one on
top of each other but the button is pushed to the side using a spacer. It will work for small text or content but is not scalable if the text grows
var body: some View {
ZStack {
HStack {
Spacer()
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: 44, height: 44)
.background(Color.yellow)
})
}
Text("aaa eee aaa random long very long text that should wrap without overlapping. long text")
.background(Color.red)
.frame(maxWidth: .infinity, alignment: .center)
.padding(5)
.opacity(0.7)
}
.background(Color.green)
}
Using alignment guides :
i would create my own center alignment guide, then use this custom alignment on a vstack where i place my content plus a fake filler rectangle that should center the elements on the content side.
the problem is that with swiftui , as far i know, you can only align one descendant element, and doesn't support multiple custom alignments on the stack of elements. so i would have only the text centered or the side button aligned not both aligned one to the center and the other to the trailing edge. and if i put a spacer between them it will just mess the alignment created. If I try with small text they will be both attached.
Heres the code. try to comment the button and you will see that it will center itself or add spacer between them.
extension HorizontalAlignment {
private enum MyAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.center]
}
}
static let myAlignment = HorizontalAlignment(MyAlignment.self)
}
var body: some View {
VStack(alignment: .myAlignment, spacing: 0) {
HStack {
Text("aaa eee aaa random ")
.background(Color.red)
.frame(maxWidth: .infinity, alignment: .center)
.padding(5)
.alignmentGuide(.myhAlignment, computeValue: { dimension in
dimension[HorizontalAlignment.center]
})
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: 44, height: 44)
.background(Color.yellow)
})
}
.background(Color.green)
Rectangle()
.fill(Color.purple)
.frame(width: 10, height: 10, alignment: .center)
.alignmentGuide(.myhAlignment, computeValue: { dimension in
dimension[HorizontalAlignment.center]
})
}
}
Tried with a combination of geometry reader and/or anchor preferences to read with sizes of the text content and side button width and apply the appropriate center offset manually, but it seems too hacky and it never worked as expected without good results
If you're familiar with uikit this problem would be resolved using a
centerX on the container with a minor layout priority and a right constraint from the center to the
close button, and call it a day. But on swiftui it seems soo hard to
handle this simple cases.
So far i haven't found a solution without using a supporting fixed frame on the side that would work with both long and short text. that space is clearly visibile if you try to use long text. and it will leave the user to wonder why is not used.
¯\ (ツ)/¯
EDIT: added possible solution in the answers
From the #Yrb suggestion in the comments, here's what i came up that shrink the blue size so it will center on the available space.
I added a fake text underneath and tracked the size. and if it's over the available space i will take the difference and shrink the blu rectangle.
One thing to keep in mind is that the hidden content if contains some text should have linelimit 1, otherwise it will get a smaller size from wrapping itself.
And i just assume that i know the size of the close button (or at least one side) for center alignment, and even if i don't know it at compile time, i could probably use a preference key to get the size at run time, and have it dynamic.
But for the moment i think it's fine the result that i got.
but honestly i hope to find something more easier in the future.
#State var text: String = "aaa eee aaa"
#State private var fillerWidth: CGFloat = 44
// i assume i know the max size of the close button or at least one side
private let kCloseButtonWidth: CGFloat = 44
private struct FakeSizeTitlteContentKey: PreferenceKey {
static var defaultValue: CGFloat { .zero }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
GeometryReader { parentGeometry in
titleContent
.lineLimit(1) // hidden text must not wrap
.overlay(GeometryReader { proxyFake in
Color.clear.border(Color.black, width: 0.3)
.preference(key: FakeSizeTitlteContentKey.self, value: proxyFake.frame(in: .local).width
.onPreferenceChange(FakeSizeTitlteContentKey.self) { value in
let availableW = parentGeometry.frame(in: .local).width
let fillSpace = availableW - value - kCloseButtonWidth * 2
fillerWidth = min(kCloseButtonWidth, max(0, fillSpace))
}
})
}
.hidden()
VStack {
HStack(spacing: 0) {
Rectangle()
.fill(Color.blue)
.frame(width: fillerWidth, height: 44)
titleContent
.background(Color.green)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, alignment: .center)
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: kCloseButtonWidth, height: kCloseButtonWidth)
.background(Color.yellow)
})
}
.coordinateSpace(name: "fullCont")
.background(Color.green)
TextEditor(text: $text)
.frame(maxHeight: 150, alignment: .center)
.border(Color.black, width: 1)
.padding(15)
Spacer()
}
}
}
#ViewBuilder var titleContent: some View {
HStack(spacing: 0) {
Text(text)
.background(Color.red)
.padding(.horizontal, 5)
}
}

How do I avoid app zoom when keyboard opens?

When I click my SwiftUI text field and the keyboard opens, the app zooms out (shown in video).
I have two questions about this behaviour:
Why does this happen?
How do I avoid this happening?
Here is my code:
struct BestillView: View { // This view is put inside a tab view with .ignoresSafeArea
#State var navn = ""
#State var varsling = true
var body: some View {
NavigationView {
ZStack {
Color("BackgroundColor")
.ignoresSafeArea()
VStack {
Image("Liquid") // This is my image overlayed on the background, i suspect this may be the only element that actually gets zoomed out
.resizable()
.aspectRatio(contentMode: .fit)
.ignoresSafeArea()
Spacer()
}
VStack {
ZStack(alignment: .leading) { // This is where the text field i'm having trouble with is
Color("UnselectedColor")
.frame(height: 50)
.cornerRadius(20.0)
if navn.isEmpty { // I have a separate text element as the placeholder text so i can give it a custom color
Text("Navn")
.foregroundColor(Color("AccentColor"))
.padding()
}
TextField("", text: $navn)
.padding()
}
.frame(width: 300)
Spacer()
.frame(height: 20.0)
// I removed the rest of my code, I don't think it should be necessary in this question - it's only a NavigationLink and a Toggle
}
}
}
}
}
You have .ignoresSafeArea() on your Image, but you actually need it on the VStack that contains the Image. The VStack is shrinking to fit the keyboard’s safe area, which squeezes the image too.
The view is actually not shrinking; the image is shrinking - because as the view moves up, it has less height to fit.
You can update your code as:
Image("Liquid")
.aspectRatio(contentMode: .fill)
and it will keep the size same - as the width will remain same.

How do i set full height on a component in a scroll view?

I am a web developer trying to learn SwiftUI and have some questions regarding about my issue.
So my code architecture is as follows:
ContentView.swift
ScrollView(.vertical){
Foreach(users){
user in UserCard()
}
}
UserCard.swift
VStack(){
Image()
Text("long text")
}
How do I make my UserCard to always the height of my iphone? As of right now, my UserCard sometimes overflow or cuts off too quickly depending on my Text length.
Any help would be appreciated. And any tips for a SwiftUI beginner would be great!
Thank you in advanced.
If you want it to be full screen and ignoring safe area you can set the UserCard's height to screen height like this:
VStack(){
Image()
Text("long text")
}
.frame(height: UIScreen.main.bounds.height)
And if you want it to respect safe area you could use a GeometryReader:
GeometryReader { geometry in
ScrollView {
Color.red
.frame(height: geometry.size.height)
Color.blue
.frame(height: geometry.size.height)
Color.yellow
.frame(height: geometry.size.height)
}
}
one Question -> one Answer
for second part of your Question need new Question with own Topic.
ZStack()
{
VStack()
{
Image(systemName: "star")
Text("long text")
}
}
.ignoresSafeArea()

SwiftUI Nested GeometryReader - Breaking UI

So I am trying to understand why my subview (TopView) is having weird resizing issues.
Here is the sample
import SwiftUI
struct ContentView: View {
#State var isInterfaceHidden: Bool = false
var body: some View {
VStack(spacing: 0, content: {
if !isInterfaceHidden {
TopView()
.background(Color.yellow)
}
Rectangle()
.foregroundColor(Color.red)
/// We make sure it won't cover the top and bottom view.
.zIndex(-1)
if !isInterfaceHidden {
Rectangle()
.foregroundColor(Color.yellow)
.frame(height: 80)
}
})
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
struct TopView: View {var body: some View {
HStack(content: {
VStack(spacing: 0, content: {
Text("Text to show, it is a title.")
.tracking(0.2)
.foregroundColor(.white)
.lineLimit(1)
GeometryReader(content: { geometry in
Text("Text to show, it is a subline.")
.tracking(0.2)
.foregroundColor(.white)
.lineLimit(1)
})
.background(Color.purple)
})
})
.padding([.leading, .trailing], 20)
}
}
I tried to set a .fixedSize() like this:
GeometryReader(content: { geometry in
Text("Text to show, it is a subline.")
.tracking(0.2)
.foregroundColor(.white)
.lineLimit(1)
})
.background(Color.purple)
But it is not fitting the text exactly, so I am not sure if this is the right solution. Do you guys have any idea?
Be aware that GeometryReader has had what appears to be a regression as of 14.0 (26/Sep/20) - or perhaps a wonderfully undocumented change of behaviour - with weighting layouts towards the topleft corner - rather than the center.
This has only appeared with apps I developed and built with XCode 12 - an XCode-11-compiled-app running on iOS 14 did not exhibit the issue. Most tutorials on the net will be assuming this worked the way it did in iOS 13/XCode 11 and your code may function differently
iOS 14 has Changed (or broken?) SwiftUI GeometryReader for a more involved question with the same issues
As far as I know, GeometryReader passes back its parent a size that is given by the parent unless you set frame() to GeometryReader explicitly. Even so, If you want to fit the area of GeometryReader to the Text view (exactly your custom view), you will have to calculate a height of the custom view by using preference or anchorPreference and then set it as a height of GeometryReader in order to let the parent know what size it needs to assign.
I hope the following link will be helpful.
https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
GeometryReader fit to View
If you're looking for the GeometryReader to not affect the size of your view, you should make an inversion. The view that you return inside the GeometryReader should be out, and the GeometryReader itself should be put in a background or in a overlay of that View.
Text("Text to show, it is a subline.")
.tracking(0.2)
.foregroundColor(.white)
.lineLimit(1)
.overlay(
GeometryReader(content: { geometry -> Color in
print(geometry.frame(in: .global))
return Color.clear
})
)
.background(Color.purple)
Either way (background or overlay), would solve your problem. Try changing overlay to background to see.
Just remember to return a Color.clear, this way, the GeometryReader becomes invisible and it doesn't change the View.