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 just started with SwiftUI and wanted to build a simple home app. In order to have a better overview I structured my site using a collapsible view, which when expanded reveals the home view. However, I've been struggling to make a dedicated "plus" button that adds another one of the predefined collapsable views to my Content View
Here is the code I made (with the help of a tutorial) for the CollapsableViewModel:
var body: some View {
ZStack{
VStack {
HStack{
Button(
action: { self.collapsed.toggle() },
label: {
HStack {
TextField("Empty...", text: $textFieldText)
.font(.title2.bold())
Spacer()
Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
}
.padding(.bottom, 1)
.background(Color.white.opacity(0.01))
}
)
.buttonStyle(PlainButtonStyle())
}
.padding(1)
}
VStack {
self.content()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: collapsed ? 0 : .none)
.clipped()
.transition(.slide)
}
}
}
And here's my Content View:
var body: some View {
ScrollView{
HStack{
CollapsableViewModel(
label: { Text("") .font(.title2.bold()) },
content: {
HStack {
Text("turn off light.")
Text("turn on ac.")
Spacer()}
.frame(maxWidth: .infinity)
}
)
.frame(maxWidth: .infinity)
}
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.statusBar(hidden: false)
.navigationBarHidden(true)
}
Now, the problem is that I really don't know where to start with this. My guess is that I have to work with an button that triggers a "showCollapsView" action. But I don't how I make it so that it appears under the VStack of my main ContentView. Any tipps would be appreciated, as I would love to understand this whole process myself!
Swift UI - Xcode
Problem: I have extra cells when my list is initialized. However, unlike other posts I do not have a navigation view. Is it mandatory to have a NavigationView in order to remove the extra cells from my list.
I have tried to implement a Navigation View but im not sure if its mandatory and how to properly implement.
.navigationBarTitle("List")
.listStyle(GroupedListStyle())
I would like to start a new Text or UIButton directly below the last "Current" cell without extra white spaces.
var body: some View {
VStack(alignment: .leading) {
Image("covidImage")
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(5)
Text("COVID DATA")
.font(.system(.title, design: .rounded))
.bold()
.lineLimit(3)
.padding(.bottom, 3)
.padding(.leading, 10)
Text("Represented by Sate in the United States")
.font(.subheadline)
.foregroundColor(.secondary)
.padding(.leading, 10)
.padding(.bottom, 0)
Text("Current State: CA")
.font(.subheadline)
.foregroundColor(.secondary)
.padding(.leading, 10)
.padding(.bottom, 10)
//List to Initialize TableView Data
List(covid) { covidData in
Image(systemName: "photo")
.padding()
HStack {
Text(covidData.dataTitle).frame(maxWidth: .infinity, alignment: .leading)
Text(covidData.dataValue)
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .trailing)
//.color(.gray)
}
}//END List
}//END Original VStack
}//END Some View
They are not empty cells. The List just fills all available space and draws empty rows placeholders where there is no content. In your scenario is needed to limit height of List to its content.
Here is a solution. Tested with Xcode 11.4 / iOS 13.4
For better visibility let's separate your list into own view named CovidDataList, then here is in your provided body:
//List to Initialize TableView Data
CovidDataList(covid: self.covid)
}//END List
Note1: The height of list updated in run-time so should be tested in running application (not in Preview)
Note2: I tested with replicated cover data model so some adapting might be needed to your code
struct CovidDataList: View {
#Environment(\.defaultMinListRowHeight) var minRowHeight
let covid: [CovidData]
#State private var listHeight = CGFloat.infinity
var body: some View {
List {
ForEach(covid, id: \.self) { covidData in
HStack {
Image(systemName: "photo")
.padding()
HStack {
Text("Title").frame(maxWidth: .infinity, alignment: .leading)
Text("Value")
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.listRowInsets(EdgeInsets()).padding(.horizontal)
}
.anchorPreference(key: BoundsPreferenceKey.self, value: .bounds) { [$0] }
}
.backgroundPreferenceValue(BoundsPreferenceKey.self) { rects in
GeometryReader { gp in
self.updateListFrame(gp: gp, anchors: rects)
}
}.frame(maxHeight: listHeight)
}
private func updateListFrame(gp: GeometryProxy, anchors: [Anchor<CGRect>]) -> some View {
let contentHeight = anchors.reduce(CGFloat.zero) { $0 + gp[$1].size.height }
DispatchQueue.main.async { // << required !!
self.listHeight = max(contentHeight, self.minRowHeight * CGFloat(self.covid.count))
}
return Color.clear
}
}
Note: BoundsPreferenceKey is taken from this my answer
Given this code:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.lineLimit(nil)
.font(.body)
Spacer()
}
.background(Color.red)
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
It results in this interface:
How can I make the VStack fill the width of the screen even if the labels/text components don't need the full width?
A trick I've found is to insert an empty HStack in the structure like so:
VStack(alignment: .leading) {
HStack {
Spacer()
}
Text("Title")
.font(.title)
Text("Content")
.lineLimit(nil)
.font(.body)
Spacer()
}
Which yields the desired design:
Is there a better way?
Try using the .frame modifier with the following options:
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello World")
.font(.title)
Text("Another")
.font(.body)
Spacer()
}
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.background(Color.red)
}
}
This is described as being a flexible frame (see the documentation), which will stretch to fill the whole screen, and when it has extra space it will center its contents inside of it.
With Swift 5.2 and iOS 13.4, according to your needs, you can use one of the following examples to align your VStack with top leading constraints and a full size frame.
Note that the code snippets below all result in the same display, but do not guarantee the effective frame of the VStack nor the number of View elements that might appear while debugging the view hierarchy.
1. Using frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) method
The simplest approach is to set the frame of your VStack with maximum width and height and also pass the required alignment in frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:):
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.font(.body)
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.background(Color.red)
}
}
2. Using Spacers to force alignment
You can embed your VStack inside a full size HStack and use trailing and bottom Spacers to force your VStack top leading alignment:
struct ContentView: View {
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.font(.body)
Spacer() // VStack bottom spacer
}
Spacer() // HStack trailing spacer
}
.frame(
maxWidth: .infinity,
maxHeight: .infinity
)
.background(Color.red)
}
}
3. Using a ZStack and a full size background View
This example shows how to embed your VStack inside a ZStack that has a top leading alignment. Note how the Color view is used to set maximum width and height:
struct ContentView: View {
var body: some View {
ZStack(alignment: .topLeading) {
Color.red
.frame(maxWidth: .infinity, maxHeight: .infinity)
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.font(.body)
}
}
}
}
4. Using GeometryReader
GeometryReader has the following declaration:
A container view that defines its content as a function of its own size and coordinate space. [...] This view returns a flexible preferred size to its parent layout.
The code snippet below shows how to use GeometryReader to align your VStack with top leading constraints and a full size frame:
struct ContentView : View {
var body: some View {
GeometryReader { geometryProxy in
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.font(.body)
}
.frame(
width: geometryProxy.size.width,
height: geometryProxy.size.height,
alignment: .topLeading
)
}
.background(Color.red)
}
}
5. Using overlay(_:alignment:) method
If you want to align your VStack with top leading constraints on top of an existing full size View, you can use overlay(_:alignment:) method:
struct ContentView: View {
var body: some View {
Color.red
.frame(
maxWidth: .infinity,
maxHeight: .infinity
)
.overlay(
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.font(.body)
},
alignment: .topLeading
)
}
}
Display:
An alternative stacking arrangement which works and is perhaps a bit more intuitive is the following:
struct ContentView: View {
var body: some View {
HStack() {
VStack(alignment: .leading) {
Text("Hello World")
.font(.title)
Text("Another")
.font(.body)
Spacer()
}
Spacer()
}.background(Color.red)
}
}
The content can also easily be re-positioned by removing the Spacer()'s if necessary.
There is a better way!
To make the VStack fill the width of it's parent you can use a GeometryReader and set the frame. (.relativeWidth(1.0) should work but apparently doesn't right now)
struct ContentView : View {
var body: some View {
GeometryReader { geometry in
VStack {
Text("test")
}
.frame(width: geometry.size.width,
height: nil,
alignment: .topLeading)
}
}
}
To make the VStack the width of the actual screen you can use UIScreen.main.bounds.width when setting the frame instead of using a GeometryReader, but I imagine you likely wanted the width of the parent view.
Also, this way has the added benefit of not adding spacing in your VStack which might happen (if you have spacing) if you added an HStack with a Spacer() as it's content to the VStack.
UPDATE - THERE IS NOT A BETTER WAY!
After checking out the accepted answer, I realized that the accepted answer doesn't actually work! It appears to work at first glance, but if you update the VStack to have a green background you'll notice the VStack is still the same width.
struct ContentView : View {
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("Hello World")
.font(.title)
Text("Another")
.font(.body)
Spacer()
}
.background(Color.green)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
.background(Color.red)
}
}
}
This is because .frame(...) is actually adding another view to the view hierarchy and that view ends up filling the screen. However, the VStack still does not.
This issue also seems to be the same in my answer as well and can be checked using the same approach as above (putting different background colors before and after the .frame(...). The only way that appears to actually widen the VStack is to use spacers:
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
HStack{
Text("Title")
.font(.title)
Spacer()
}
Text("Content")
.lineLimit(nil)
.font(.body)
Spacer()
}
.background(Color.green)
}
}
The simplest way I manage to solve the issue was is by using a ZStack + .edgesIgnoringSafeArea(.all)
struct TestView : View {
var body: some View {
ZStack() {
Color.yellow.edgesIgnoringSafeArea(.all)
VStack {
Text("Hello World")
}
}
}
}
Way Number 1 -> Using MaxWidth & MaxHeight
import SwiftUI
struct SomeView: View {
var body: some View {
VStack {
Text("Hello, World!")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.red)
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
Way Number 2 -> Using Main Screen Bounds
import SwiftUI
struct SomeView: View {
var body: some View {
VStack {
Text("Hello, World!")
}
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height)
.background(.red)
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
Way Number 3 -> Using Geometry Reader
import SwiftUI
struct SomeView: View {
var body: some View {
GeometryReader { geometryReader in
VStack {
Text("Hello, World!")
}
.frame(maxWidth: geometryReader.size.width, maxHeight: geometryReader.size.height)
.background(.red)
}
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
Way Number 4 -> Using Spacers
import SwiftUI
struct SomeView: View {
var body: some View {
VStack {
Text("Hello, World!")
HStack{
Spacer()
}
Spacer()
}
.background(.red)
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
EDIT: answer updated with simple (better) approach using .frame
Just use frame modifiers!
struct Expand: View {
var body: some View {
VStack(alignment: .leading) {
Text("Title")
.font(.title)
Text("Content")
.lineLimit(nil)
.font(.body)
}
.frame(maxWidth:.infinity,maxHeight:.infinity,alignment:.topLeading)
.background(Color.red)
}
}
note - you don't even need the spacer in the VStack!
note2 - if you don't want the white at top & bottom, then in the background use:
Color.red.edgesIgnoringSafeArea(.all)
use this
.edgesIgnoringSafeArea(.all)
A good solution and without "contraptions" is the forgotten ZStack
ZStack(alignment: .top){
Color.red
VStack{
Text("Hello World").font(.title)
Text("Another").font(.body)
}
}
Result:
You can do it by using GeometryReader
GeometryReader
Code:
struct ContentView : View {
var body: some View {
GeometryReader { geometry in
VStack {
Text("Turtle Rock").frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading).background(Color.red)
}
}
}
}
Your output like:
One more alternative is to place one of the subviews inside of an HStack and place a Spacer() after it:
struct ContentView : View {
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Title")
.font(.title)
.background(Color.yellow)
Spacer()
}
Text("Content")
.lineLimit(nil)
.font(.body)
.background(Color.blue)
Spacer()
}
.background(Color.red)
}
}
resulting in :
This is a useful bit of code:
extension View {
func expandable () -> some View {
ZStack {
Color.clear
self
}
}
}
Compare the results with and without the .expandable() modifier:
Text("hello")
.background(Color.blue)
-
Text("hello")
.expandable()
.background(Color.blue)
This is what worked for me (ScrollView (optional) so more content can be added if needed, plus centered content):
import SwiftUI
struct SomeView: View {
var body: some View {
GeometryReader { geometry in
ScrollView(Axis.Set.horizontal) {
HStack(alignment: .center) {
ForEach(0..<8) { _ in
Text("🥳")
}
}.frame(width: geometry.size.width, height: 50)
}
}
}
}
// MARK: - Preview
#if DEBUG
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
SomeView()
}
}
#endif
Result
I know this will not work for everyone, but I thought it interesting that just adding a Divider solves for this.
struct DividerTest: View {
var body: some View {
VStack(alignment: .leading) {
Text("Foo")
Text("Bar")
Divider()
}.background(Color.red)
}
}
Login Page design using SwiftUI
import SwiftUI
struct ContentView: View {
#State var email: String = "william#gmail.com"
#State var password: String = ""
#State static var labelTitle: String = ""
var body: some View {
VStack(alignment: .center){
//Label
Text("Login").font(.largeTitle).foregroundColor(.yellow).bold()
//TextField
TextField("Email", text: $email)
.textContentType(.emailAddress)
.foregroundColor(.blue)
.frame(minHeight: 40)
.background(RoundedRectangle(cornerRadius: 10).foregroundColor(Color.green))
TextField("Password", text: $password) //Placeholder
.textContentType(.newPassword)
.frame(minHeight: 40)
.foregroundColor(.blue) // Text color
.background(RoundedRectangle(cornerRadius: 10).foregroundColor(Color.green))
//Button
Button(action: {
}) {
HStack {
Image(uiImage: UIImage(named: "Login")!)
.renderingMode(.original)
.font(.title)
.foregroundColor(.blue)
Text("Login")
.font(.title)
.foregroundColor(.white)
}
.font(.headline)
.frame(minWidth: 0, maxWidth: .infinity)
.background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.padding(.horizontal, 20)
.frame(width: 200, height: 50, alignment: .center)
}
Spacer()
}.padding(10)
.frame(minWidth: 0, idealWidth: .infinity, maxWidth: .infinity, minHeight: 0, idealHeight: .infinity, maxHeight: .infinity, alignment: .top)
.background(Color.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
⚠️ Important Note!
🛑 All other solutions are just adding a frame around the content!
✅ but this solution changes the actual frame!
Simple and correct extension
You can use this modifier
.flexible(width: true, height: false)
Demo
💡Note how contents are aligned exactly as you assign in the original stack
The code behind this ( FlexibleViewModifier.swift )
extension View {
func flexible(width: Bool, height: Bool) -> some View {
self.modifier(MatchingParentModifier(width: width, height: height))
}
}
struct MatchingParentModifier: ViewModifier {
#State private var intrinsicSize: CGSize = UIScreen.main.bounds.size
private let intrinsicWidth: Bool
private let intrinsicHeight: Bool
init(width: Bool, height: Bool) {
intrinsicWidth = !width
intrinsicHeight = !height
}
func body(content: Content) -> some View {
GeometryReader { _ in
content.modifier(intrinsicSizeModifier(intrinsicSize: $intrinsicSize))
}
.frame(
maxWidth: intrinsicWidth ? intrinsicSize.width : nil,
maxHeight: intrinsicHeight ? intrinsicSize.height : nil
)
}
}
struct intrinsicSizeModifier: ViewModifier {
#Binding var intrinsicSize: CGSize
func body(content: Content) -> some View {
content.readIntrinsicContentSize(to: $intrinsicSize)
}
}
struct IntrinsicContentSizePreferenceKey: PreferenceKey {
static let defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
extension View {
func readIntrinsicContentSize(to size: Binding<CGSize>) -> some View {
background(
GeometryReader {
Color.clear.preference(
key: IntrinsicContentSizePreferenceKey.self,
value: $0.size
)
}
)
.onPreferenceChange(IntrinsicContentSizePreferenceKey.self) {
size.wrappedValue = $0
}
}
}
Here another way which would save time in your projects:
Much less code and reusable in compare to other answers which they are not reusable!
extension View {
var maxedOut: some View {
return Color.clear
.overlay(self, alignment: .center)
}
func maxedOut(color: Color = Color.clear, alignment: Alignment = Alignment.center) -> some View {
return color
.overlay(self, alignment: alignment)
}
}
use case:
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.maxedOut
.background(Color.blue)
Text("Hello, World!")
.maxedOut(color: Color.red)
}
}
Just add Color.clear to the bottom of the VStack, simple as that :)
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Title")
Color.clear
}
.background(Color.red)
}
}