I am trying to adapt the font size of a text element whose length varies greatly so that it fills the whole frame where it is located and no word needs to be truncated (in a iOS app). In the screenshot below, I would like the sentence to reach the buttons at the bottom of the screen.
Please find below the structure of my code. I have seen a few threads on SO dealing with this issue, but I have not been able to replicate the solution, because of my poor skills in Swift... If someone were so kind as to give me a detailed explanation of what lines of code should be inserted and where to place them, I'd be very grateful!
I have tried to adjust the font size so to say manually, by incrementing or decrementing it via a function according to the length of the string that Text receives as its argument, but it only yielded a mess, because long words need their own separate line, which mars my computation.
With thanks,
Julien
import SwiftUI
import AVKit
import UIKit
import AVFoundation
import Foundation
import CoreServices
import CoreData
import MediaPlayer
struct ContentView: View {
// Various functions
#State var audioPlayer: AVAudioPlayer!
var body: some View {
VStack {
var currentSentence = "string of variable length, could be a long or a very short sentence"
Text(currentSentence) // the variable currentSentence contains a string whose length varies greatly
.font(.system(size: 40, weight: .light, design: .serif))
HStack {
Spacer()
// Play button
Button(action: {
// Action
}) {
Image(systemName: "play.square.fill").resizable()
.frame(width: 150.0, height: 200.0, alignment: .bottom)
}
.frame(maxHeight: .infinity, alignment: .bottom)
Spacer()
// Pause button
Button(action: {
// Action
}){
Image(systemName: "square.slash").resizable()
.frame(width: 150.0, height: 200.0)
.aspectRatio(contentMode: .fill)
}
.frame(maxHeight: .infinity, alignment: .bottom)
Spacer()
}
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You need to use .minimumScaleFactor on the text and set the text font to the maximum.
struct ContentView: View {
var body: some View {
VStack (spacing: 0) {
var currentSentence = "string of variable length, could be a long or a very short sentence"
Text(currentSentence)
.font(.system(size: 100, weight: .light, design: .serif))
.minimumScaleFactor(0.1)
Spacer()
HStack (alignment: .bottom, spacing: 0) {
Spacer()
// Play button
Button(action: {
// Action
}) {
Image(systemName: "play.square.fill").resizable()
.frame(width: 150.0, height: 200.0, alignment: .bottom)
}
Spacer()
// Pause button
Button(action: {
// Action
}){
Image(systemName: "square.slash").resizable()
.frame(width: 150.0, height: 200.0)
.aspectRatio(contentMode: .fill)
}
Spacer()
}
}
.edgesIgnoringSafeArea([.bottom])
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
}
}
}
In some cases there might be little bit space between text and buttons, It means new line with bigger font can't fit there.
Related
I'm trying to create a chat bubble like this:
Actual Bubble
Actual Bubble 2.0
This is what I have been able to achieve so far.
My attempt
My attempt
This is my code so far:
import SwiftUI
struct TestingView: View {
var body: some View {
ZStack {
/// header
VStack(alignment: .trailing) {
HStack {
HStack() {
Text("abcd")
}
HStack {
Text("~abcd")
}
}.padding([.trailing, .leading], 15)
.fixedSize(horizontal: false, vertical: true)
/// text
HStack {
Text("Hello Everyone, bdhjewbdwebdjewbfguywegfuwyefuyewvfyeuwfvwbcvuwe!")
}.padding([.leading, .trailing], 15)
/// timestamp
HStack(alignment: .center) {
Text("12:00 PM")
}.padding(.trailing,15)
}.background(Color.gray)
.padding(.leading, 15)
.frame(maxWidth: 250, alignment: .leading)
}
}
}
struct TestingView_Previews: PreviewProvider {
static var previews: some View {
TestingView()
}
}
The main goal is that I want the two labels on top to be distant relative to the size of the message content. I am not able to separate the two labels far apart i.e one should be on the leading edge of the bubble and the other one on the trailing edge.
Already tried spacer, it pushes them to the very edge, we need to apart them relative to the content size of the message as shown in attached images.
Here is a simplified code.
Regarding Spacer: To achieve your desired result you put both Text views inside of a HStack, and put a Spacer between them. So the Spacer pushes them apart to the leading and trailing edge.
Also I recommend to only use one padding on the surrounding stack.
VStack(alignment: .leading) {
// header
HStack {
Text("+123456")
.bold()
Spacer() // Spacer here!
Text("~abcd")
}
.foregroundStyle(.secondary)
// text
Text("Hello Everyone, bdhjewbdwebdjewbfguywegfuwyefuyewvfyeuwfvwbcvuwe!")
.padding(.vertical, 5)
// timestamp
Text("12:00 PM")
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding()
.background(Color.gray.opacity(0.5))
.cornerRadius(16)
.frame(maxWidth: 250, alignment: .leading)
}
We can put that header into overlay of main text, so it will be always aligned by size of related view, and then it is safe to add spacer, `cause it do not push label wider than main text.
Tested with Xcode 13.4 / iOS 15.5
var body: some View {
let padding: CGFloat = 15
ZStack {
/// header
VStack(alignment: .trailing) {
/// text
HStack {
//Text("Hello Everyone") // short test
Text("Hello Everyone, bdhjewbdwebdjewbfguywegfuwyefuyewvfyeuwfvwbcvuwe!") // long test
}
.padding(.top, padding * 2)
.overlay(
HStack { // << here !!
HStack() {
Text("abcd")
}
Spacer()
HStack {
Text("~abcd")
}
}
, alignment: .top)
.padding([.trailing, .leading], padding)
/// timestamp
HStack(alignment: .center) {
Text("12:00 PM")
}.padding(.trailing, padding)
}.background(Color.gray)
.padding(.leading, padding)
.frame(maxWidth: 250, alignment: .leading)
}
}
To separate two components with fairly space in the middle, use HStack{} with Spacer().
This is a sample approach for this case. Code is below the image:
VStack {
HStack {
Text("+92 301 8226")
.foregroundColor(.red)
Spacer()
Text("~Usman")
.foregroundColor(.gray)
}
.padding(.bottom, 5)
.padding(.horizontal, 5)
Text("Testing testingtesting testing testing testingtesting testing testing testing testing testing testing testing testing testing.")
.padding(.horizontal, 5)
HStack {
Spacer()
Text("2:57 AM")
.foregroundColor(.gray)
.font(.subheadline)
}
.padding(.trailing, 5)
}
.frame(width: 300, height: 160)
.background(.white)
.cornerRadius(15)
I wanted some extra space on the top of the list so I tried using Spacer within the list and added modifiers to it. However I am not seeing the height getting reduced further. Below is the code for my view.
CustomView:
import SwiftUI
struct CustomView: View {
var body: some View {
VStack {
List {
Spacer()
.frame(minHeight: 1, idealHeight: 1, maxHeight: 2)
.fixedSize().listRowBackground(Color.clear)
UserLoginDetailsRowView().padding(.init(top: 0, leading: 5, bottom: 5, trailing: 5))
ForEach(1..<2) { _ in
VStack(alignment: .leading) {
Text("App version").fixedSize(horizontal: false, vertical: true).font(.headline).foregroundColor(.white)
Text("1.1.0").fixedSize(horizontal: false, vertical: true).font(.subheadline).foregroundColor(.white)
Spacer()
}.padding(.bottom, 15)
}.listRowBackground(Color.clear)
}
}.navigationBarTitle("Main Menu")
}
}
UserLoginDetailsRowView code:
import SwiftUI
struct UserLoginDetailsRowView: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .center) {
Spacer()
Spacer()
Text("User's full name").lineLimit(2).font(.headline)
Text("Username").lineLimit(2).font(.subheadline)
Spacer()
}
ZStack {
Image("user-gray")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30 , height: 30)
.offset(x: geometry.size.width / 2.8, y: -geometry.size.height/4)
}
}.frame(minHeight: 60.0)
}
}
This is how it looks with this code:
Regardless of the changes I make to minHeight, idealHeight and maxHeight in Spacer() within CustomView the result remains the same. However I want half of the space of what it's currently showing. I even tried replacing Spacer() with VStack and setting a frame height modifier to it, but at minimum, I do always see this much of space. I want the space reduced to half.
If I remove the Spacer() from CustomView then the image on my custom row gets chopped off and looks something like this. How do I reduce the space to half of what it is now?
Adding playground source code:
import SwiftUI
import PlaygroundSupport
struct CustomView: View {
var body: some View {
VStack {
Spacer().frame(minHeight: 25, idealHeight: 25, maxHeight: 30).fixedSize().listRowBackground(Color.clear)
List {
// Extra space for the top half of user icon within UserLoginDetailsRowView.
// Spacer().frame(minHeight: 25, idealHeight: 25, maxHeight: 30).fixedSize().listRowBackground(Color.clear)
UserLoginDetailsRowView().padding(.init(top: 0, leading: 5, bottom: 5, trailing: 5))
ForEach(1..<2) { _ in
VStack(alignment: .leading) {
Text("App Version").fixedSize(horizontal: false, vertical: true).font(.headline).foregroundColor(.white)
Text("1.1.0").fixedSize(horizontal: false, vertical: true).font(.subheadline).foregroundColor(.white)
Spacer()
}.padding(.bottom, 15)
}.listRowBackground(Color.clear)
}
}.navigationBarTitle("Back")
}
}
struct UserLoginDetailsRowView: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .center) {
Spacer()
Spacer()
Text("User's full name").lineLimit(2).font(.headline)
Text("Username").lineLimit(2).font(.subheadline)
Spacer()
}
ZStack {
Image(systemName: "person.circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 22 , height: 22)
.offset(x: geometry.size.width / 2.8, y: -geometry.size.height/4)
}
}.frame(minHeight: 60.0)
}
}
PlaygroundPage.current.setLiveView(CustomView())
Solution
The primary gap comes from the list style itself. if you apply .listStyle(PlainListStyle()) to the List it will reduce it to what you are looking for.
List { ... }.listStyle(PlainListStyle())
If you want to further reduce it and control it to the last pixel apply a .onAppear modifier to the list and set the content inset to your desired value.
List { .... }.onAppear(perform: {
UITableView.appearance().contentInset.top = -60
})
In the above code the value 60 is arbitrary in nature and you need to play around to get a value that fits your UI.
Explanation
The List default style adds a larger header which creates the spacing you were having issues with, this behaviour is similar to GroupedListStyle. From the documentation
On iOS, the grouped list style displays a larger header and footer than the plain style, which visually distances the members of different sections.
You can play around with other List Styles from the documentation to fit your needs better.
Full Playground Code - For .onAppear solution
import SwiftUI
import PlaygroundSupport
struct CustomView: View {
var body: some View {
VStack(alignment:.leading, spacing:0) {
List {
// Extra space for the top half of user icon within UserLoginDetailsRowView.
Spacer().frame(minHeight: 1, idealHeight: 1, maxHeight: 2)
.fixedSize().listRowBackground(Color.clear)
UserLoginDetailsRowView().padding(.init(top: 0, leading: 5, bottom: 5, trailing: 5))
ForEach(1..<2) { _ in
VStack(alignment: .leading) {
Text("App Version").fixedSize(horizontal: false, vertical: true).font(.headline).foregroundColor(.white)
Text("1.1.0").fixedSize(horizontal: false, vertical: true).font(.subheadline).foregroundColor(.white)
Spacer()
}.padding(.bottom, 15)
}.listRowBackground(Color.clear)
}.onAppear(perform: {
UITableView.appearance().contentInset.top = -60
})
}.navigationBarTitle("Back")
}
}
struct UserLoginDetailsRowView: View {
var body: some View {
GeometryReader { geometry in
VStack(alignment: .center) {
Spacer()
Spacer()
Text("User's full name").lineLimit(2).font(.headline)
Text("Username").lineLimit(2).font(.subheadline)
Spacer()
}
ZStack {
Image(systemName: "person.circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 22 , height: 22)
.offset(x: geometry.size.width / 2.8, y: -geometry.size.height/4)
}
}.frame(minHeight: 60.0)
}
}
PlaygroundPage.current.setLiveView(CustomView())
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:
How to make a full-width header using List with InsetGroupedListStyle on iOS?
One way is to use negative padding (as an example in my code), but this doesn't seem like the best solution using fixed value as it may change in the future.
Is there a better way?
Example code:
import SwiftUI
struct ContentView: View {
var body: some View {
List {
Section (header:
VStack {
Text("Header")
.foregroundColor(.white)
}
.frame( // This doesn't remove list's paddings
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.background(Color.red)
// .padding(.horizontal, -16) // This works, but fixed value is not the best solution.
.textCase(nil)
.font(.body)
) {
Text("Hello, world!")
.padding()
}
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You could set the width of the header to the width of the screen, and the padding (x2) you need could be reduced from the width. To can achieve this using a GeometryReader. For example,
// Geometry reader to get the width
GeometryReader { reader in
List {
Section (header:
VStack {
Text("Header")
.foregroundColor(.white)
}
// Setting the frame of the Header to the size of the screen
// Reducing 20 from the width, giving a padding of 10 on each side
.frame(width: reader.size.width - 20, height: 20, alignment: .leading)
.background(Color.red)
.textCase(nil)
.font(.body)
) {
Text("Hello, world!")
.padding()
}
}
.listStyle(InsetGroupedListStyle())
}
You can see the result below
you could try this:
...
{
Text("Hello, world!").padding()
}.headerProminence(.increased) // <--- here
I have a SwiftUI View (LandingScreenView.swift) that is using a button style implemented on another file (DefaultButtonStyle.swift). When I go to that Button Style file and change a line of code there, then return to the LandingScreenView.swift the SwiftUI preview has paused. It occurs always. This is a very fresh project with only those 2 files, I have one copy script on my build phases and I already set it to run "For install builds only". How can I make the SwiftUI works without pausing?
Xcode Version: 12.5
(I'm running it on an M1 MacBook, don't know if it makes any difference here)
import SwiftUI
struct LandingScreenView: View {
var body: some View {
VStack {
Spacer().frame(width: 0, height: 44, alignment: .center)
Text("app.title")
.font(.appTitle)
.frame(minWidth: 0,
maxWidth: .infinity,
alignment: .center)
Spacer()
VStack {
Button("landing.login.button", action: {
})
.buttonStyle(DefaultButton())
Button("landing.about.button", action: {
})
.buttonStyle(DefaultButton())
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .topLeading)
Spacer()
}.padding()
}
}
struct LandingScreenView_Previews: PreviewProvider {
static var previews: some View {
LandingScreenView()
}
}
import SwiftUI
struct DefaultButton: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.label
.padding()
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 44,
idealHeight: 44,
alignment: .center)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(40)
}
}
}
EDIT:
My problem here is not that the preview has to recompile it every time. What seems a little bit weird is that I have to press the resume button every time I go out the screen and return.