Extra size geometryReader - swift

I'm trying to make this layout:
I want that my rectangle have exactly the same height as my grey text at the right. I used Geometry reader like that:
var body: some View {
HStack(alignment: .top, spacing:0){
Text("14:00")
.modifier(DateSurveyListModifier(fontSize: 14, isSelected: occurrence.isCompleted))
.foregroundColor(occurrence.isCompleted ? .HomeMainGrey : .HomeSecondGrey )
.padding(.top,10)
.padding(.trailing,12)
VStack(spacing:0){
if occurrence.isCompleted
{
pinSurveyHighlighted().frame(width:25,height:25)
}
else
{
pinSurveyHighlighted()
}
// Where I'm trying to make the rectangle
GeometryReader{ geo in
Rectangle().foregroundColor(.HomeMainBlue).frame(width:3,height: geo.size.height)
}.aspectRatio(contentMode: .fit)
}
.padding(.top,5)
VStack(alignment: .leading ,spacing:0){
Text("Lorem ipsum dolor sit dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor")
.multilineTextAlignment(.leading)
.modifier(TitleSurveyListModifier(fontSize : 15 ,isSelected: occurrence.isCompleted))
.foregroundColor(occurrence.isCompleted ? .HomeGreySurveyList : .HomeSecondGrey )
.frame(alignment: .leading)
Text("lit. Aliquam quis consectetu consectet consectetu consectetu consectetu consectetuconsectetu consectetu consectetu consectetu consectetu consectetu consectetu consectetu v consectetuuconsectetu ")
.modifier(DateSurveyListModifier(fontSize : 14 , isSelected: occurrence.isCompleted))
.foregroundColor(occurrence.isCompleted ? .HomeMainGrey : .HomeSecondGrey )
.multilineTextAlignment(.leading)
.frame(alignment: .leading)
.padding(.top,5)
}
.padding(.leading,15)
Spacer()
}
.padding(.top,15)
.padding(.horizontal,37)
}
I've probably missed some rules about Geometry reader but in my case it's taking so much space, I my layout look like this now:
I'm looking for some help to understand how geometryReader really works, in order to achieve my view.

You don't need to use geometryReader here.
VStack{ ...
Spacer().layoutPriority(1) }
//This will push all views in the `HStack` to the top half of the screen and saving some place for `HStack` while preventing the long vertical rectangle from appearing.
and
.fixedSize(horizontal: false, vertical: true)
//This will guarantee the gray text part will have the correct height.
can achieve what you want.
var body: some View {
VStack{
HStack(alignment: .top, spacing:0){
Text("14:00")
//.modifier(DateSurveyListModifier(fontSize: 14, isSelected: occurrence.isCompleted))
// .foregroundColor(occurrence.isCompleted ? .HomeMainGrey : .HomeSecondGrey )
.padding(.top,10)
.padding(.trailing,12)
VStack(spacing:0){
if false
{
Text("hext").frame(width:25,height:25)
}
else
{
Text("hext")
}
// Where I'm trying to make the rectangle
Rectangle() .foregroundColor(.red).frame(width: 3)
}.padding(.top,5 )
VStack(alignment: .leading ,spacing:0){
Text("Lorem ipsum dolor sit dolor dolor dolor dolor dolor dolor dolor dolor dolor dolor")
.multilineTextAlignment(.leading)
.font(Font.system(size: 15))
// .modifier(TitleSurveyListModifier(fontSize : 15 ,isSelected: occurrence.isCompleted))
// .foregroundColor(occurrence.isCompleted ? .HomeGreySurveyList : .HomeSecondGrey )
.frame(alignment: .leading)
Text("lit. Aliquam quis consectetu consectet consectetu consectetu consectetu consectetuconsectetu consectetu consectetu consectetu consectetu consectetu consectetu consectetu v consectetuuconsectetu ")
.font(Font.system(size: 14))
// .modifier(DateSurveyListModifier(fontSize : 14 , isSelected: occurrence.isCompleted))
// .foregroundColor(occurrence.isCompleted ? .HomeMainGrey : .HomeSecondGrey )
.multilineTextAlignment(.leading)
.frame(alignment: .leading)
.padding(.top,5)
}.padding(.leading,15).fixedSize(horizontal: false, vertical: true)
Spacer()
}
Spacer().layoutPriority(1)
}
.padding(.top,15)
.padding(.horizontal,37)
}

Related

SwiftUI: How to change the TextField height dynamicly to a threshold and then allow scrolling?

The LineLimit does not work:
TextField("Text", text: $model.commitDescr)
.multilineTextAlignment(.leading)
.lineLimit(5)
LineLimit change nothing - I'm able to display 10-20-30 lines.
Similar way with scrollView allways have maxHeight(400) insteaad of dymamic height:
ScrollView() {
TextField("Text", text: $model.commitDescr)
.multilineTextAlignment(.leading)
.lineLimit(5)
}
.frame(minHeight: 20, maxHeight: 400)
The following also does not work properly:
TextField("Text", text: $model.commitDescr)
.multilineTextAlignment(.leading)
.lineLimit(5)
.frame(minHeight: 20, maxHeight: 400)
Here is a way for this issue:
struct ContentView: View {
#State private var commitDescr: String = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
var body: some View {
TextEditorView(string: $commitDescr)
}
}
struct TextEditorView: View {
#Binding var string: String
#State private var textEditorHeight : CGFloat = CGFloat()
var body: some View {
ZStack(alignment: .leading) {
Text(string)
.lineLimit(5)
.foregroundColor(.clear)
.padding(.top, 5.0)
.padding(.bottom, 7.0)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
TextEditor(text: $string)
.frame(height: textEditorHeight)
}
.onPreferenceChange(ViewHeightKey.self) { textEditorHeight = $0 }
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
#available(iOS 14.0, macOS 11.0, *)
use TextEditor instead of TextField
TextEditor(text: $text)
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.leading)
in use example :
ScrollView {
VStack{
TextEditor(text: $text)
.frame(minHeight: 40, alignment: .leading)
.frame(maxHeight: MAX_HEIGHT)
.cornerRadius(10, antialiased: true)
.foregroundColor(.black)
.font(.body)
.padding()
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.leading)
}
.background(Color.red)
}
Fixed height, allows scrolling
Use TextEditor, rather than TextField. You then set the maxHeight to whatever you would like.
struct ContentView: View {
#State private var text = ""
var body: some View {
TextEditor(text: $text)
.frame(maxHeight: 300)
}
}
Result:
Fixed number of lines, no scrolling
If you want to limit the number of lines you can display with TextEditor, you can use SwiftUI-Introspect to do that.
Example:
struct ContentView: View {
#State private var text = ""
var body: some View {
TextEditor(text: $text)
.introspectTextView { textView in
textView.textContainer.maximumNumberOfLines = 5
textView.textContainer.lineBreakMode = .byTruncatingTail
}
}
}
Result:
Usually, a TextField is only for one line of text.
For multiple lines of input, a TextEditor may be a better solution in native SwiftUI. You could also set a .frame(maxHeight: XX, alignment: .leading) for this.
To be clear, the example beneath will make sure the editor is always 200 points high, even when no text is entered:
TextEditor(text: $variable)
.frame(maxHeight: 200, alignment: .center)
On request of the asker, an extra example where the editor is small (50 points high) and only expands to the maxHeight when text is entered:
TextEditor(text: $variable)
.frame(minHeight: 50, maxHeight: 200, alignment: .center)
Proof of concept: https://imgur.com/a/kmu1gIe

SwiftUI - Matched Geometry Effect Size doesn't match

Consider the following sample:
struct MatchedGeometryHeight: View {
#State private var mainHeight: CGFloat = 100
#Namespace private var namespace
var body: some View {
ScrollView {
VStack {
VStack {
HStack {
Text("Main Height")
Slider(value: $mainHeight, in: 10...200)
}
}.padding(.vertical, 20)
RoundedRectangle(cornerRadius: 8)
.foregroundColor(.green.opacity(0.8))
.frame(height: mainHeight)
.matchedGeometryEffect(id: "dynamic",
in: namespace)
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras eleifend risus id laoreet imperdiet. Praesent molestie lectus id sem ornare ultricies. Nam faucibus ante erat.")
.foregroundColor(.white)
.padding()
.background(
RoundedRectangle(cornerRadius: 8)
.foregroundColor(.blue)
.shadow(radius: 2, x: 0, y: 2))
.matchedGeometryEffect(id: "dynamic",
in: namespace,
properties: .size,
isSource: false)
}
}
.padding()
}
}
This produces:
I'm expecting the blue rectangle to match the green one's. However, what happens instead is that the blue rectangle tightly hugs its text rather than the geometry effect.
Now, because this is a simple example, I know I could fix that by using the following modifier:
.frame(minWidth: .zero,
maxWidth: .infinity,
minHeight: .zero,
maxHeight: .infinity)
But unfortunately in the real world, I want to be able to be able to pass generic contents to those Rectangles (think cards with varying amounts of data). When that happens, setting the .frame modifier like previously mentioned doesn't help at all.
From what I see, it seem like a view will refuse using a smaller size than what its own contents require.
How can I achieve this?

Why is my SwiftUI view not updating when the model changes

How do I update the content of an existing row in a List?
The print statement shows that the button is updating the Bool, but the View does not update .
the content (button) moves as expected, but the action and formating does not change as expected.
the code for the page I am using:
struct NotificationView: View {
#ObservedObject var notificationVModel: NotificationVModel = NotificationVModel()
var body: some View {
NavigationView{
List(notificationVModel.notificationarray,id:\.NotificationName){notificationVV in
ZStack {
if notificationVV.isShgowen {
Color (.green).opacity(0.1)
.cornerRadius(10)
}
HStack{
Button(action: {
notificationVV.changTogle()
print("\(notificationVV.isShgowen)")
}, label: {
ZStack{
Circle()
.foregroundColor(Color(red: 0 / 255, green: 175 / 255, blue: 80 / 255))
.frame(width: 50, height: 50, alignment: .center)
Image(systemName: "bell")
.frame(width: 40, height: 40, alignment: .center)
.foregroundColor(.white)
} })
VStack{
HStack {
if notificationVV.isShgowen{
Text("true")
.font(.custom("AvenirNext-DemiBold", size: 10))
}
Text(notificationVV.NotificationName)
.font(.custom("AvenirNext-DemiBold", size: 20))
Spacer()
Text(notificationVV.NotifivationDate)
.font(.custom("AvenirNext-Medium", size: 12))
.foregroundColor(.gray)
}.padding(.leading).padding(.trailing)
Text(notificationVV.NotificationDiscrip)
.font(.custom("AvenirNext-Regular", size: 11))
.lineLimit(nil)
.padding(.leading).padding(.trailing)
}
}.padding()
}
}
.navigationTitle("Notification")
.navigationBarTitleDisplayMode(.inline)
}
}
}
The ViewModel
class NotificationVModel: ObservableObject {
#Published var notificationarray : [NotificationV] = [
NotificationV(NotificationName: "Notification 1", NotificationDiscrip: "Lorem ipsum dolor sit amet,consec tetur adipiscing elit Lorem ipsum doloramet,consec tetur adipiscing elit sit ipi piscing… ", NotifivationDate: "04/02/2021", isShgowen: false), NotificationV(NotificationName: "Notification 2", NotificationDiscrip: "Lorem ipsum dolor sit amet,consec tetur adipiscing elit Lorem ipsum doloramet,consec tetur adipiscing elit sit ipi piscing… ", NotifivationDate: "05/03/2021", isShgowen: true),
]
}
The model
class NotificationV : ObservableObject{
let objectWillChange = ObservableObjectPublisher()
#Published var NotificationName : String = ""
#Published var NotificationDiscrip: String = ""
#Published var NotifivationDate:String = ""
#Published var isShgowen:Bool = false
init(NotificationName: String, NotificationDiscrip: String, NotifivationDate: String, isShgowen: Bool) {
self.NotificationName = NotificationName
self.NotificationDiscrip = NotificationDiscrip
self.NotifivationDate = NotifivationDate
self.isShgowen = isShgowen
}
func changTogle(){
if isShgowen == false {
isShgowen = true
}
}
}
Use struct rather than class for the NotificationV.
Create a new view for the buttons and send both your viewmodel and model to the new view.
Find index of model
Fore more info, read this tutorial
https://developer.apple.com/tutorials/swiftui/building-lists-and-navigation
struct NotificationView: View {
#ObservedObject var notificationVModel: NotificationVModel = NotificationVModel()
var body: some View {
NavigationView{
List(notificationVModel.notificationarray,id:\.NotificationName){notificationVV in
ZStack {
if notificationVV.isShowen {
Color (.green).opacity(0.1)
.cornerRadius(10)
}
HStack{
ToggleNotification(notifications: $notificationVModel.notificationarray, notificationV: notificationVV)
VStack{
HStack {
Text(notificationVV.isShowen ? "true": "false")
if notificationVV.isShowen {
Text("true")
.font(.custom("AvenirNext-DemiBold", size: 10))
}
Text(notificationVV.NotificationName)
.font(.custom("AvenirNext-DemiBold", size: 20))
Spacer()
Text(notificationVV.NotifivationDate)
.font(.custom("AvenirNext-Medium", size: 12))
.foregroundColor(.gray)
}.padding(.leading).padding(.trailing)
Text(notificationVV.NotificationDiscrip)
.font(.custom("AvenirNext-Regular", size: 11))
.lineLimit(nil)
.padding(.leading).padding(.trailing)
}
}.padding()
}
}
.navigationTitle("Notification")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ToggleNotification:View {
#Binding var notifications: [NotificationV]
var notificationV: NotificationV
var index:Int? {
notifications.firstIndex { $0.NotificationName == notificationV.NotificationName}
}
var body: some View {
Button(action: {
notifications[index!].isShowen.toggle()
print("\($notifications[index!].isShowen)")
}, label: {
ZStack{
Circle()
.foregroundColor(Color(red: 0 / 255, green: 175 / 255, blue: 80 / 255))
.frame(width: 50, height: 50, alignment: .center)
Image(systemName: "bell")
.frame(width: 40, height: 40, alignment: .center)
.foregroundColor(.white)
}
})
}
}
struct NotificationV {
var NotificationName : String = ""
var NotificationDiscrip: String = ""
var NotifivationDate:String = ""
var isShowen:Bool = false
init(NotificationName: String, NotificationDiscrip: String, NotifivationDate: String, isShowen: Bool) {
self.NotificationName = NotificationName
self.NotificationDiscrip = NotificationDiscrip
self.NotifivationDate = NotifivationDate
self.isShowen = isShowen
}
}
class NotificationVModel: ObservableObject {
#Published var notificationarray : [NotificationV] = [
NotificationV(NotificationName: "Notification 1", NotificationDiscrip: "Lorem ipsum dolor sit amet,consec tetur adipiscing elit Lorem ipsum doloramet,consec tetur adipiscing elit sit ipi piscing… ", NotifivationDate: "04/02/2021", isShowen: false), NotificationV(NotificationName: "Notification 2", NotificationDiscrip: "Lorem ipsum dolor sit amet,consec tetur adipiscing elit Lorem ipsum doloramet,consec tetur adipiscing elit sit ipi piscing… ", NotifivationDate: "05/03/2021", isShowen: true),
]
}
Follow case conventions. Names of types and protocols are UpperCamelCase. Everything else is lowerCamelCase.
https://swift.org/documentation/api-design-guidelines/
Update
As of now, NotificationV must unique NotificationName. Regardless of that, NotificationV should confirms to both Identifiable, Equatable protocols and use the following in ToggleNotification
var index:Int? {
notifications.firstIndex { $0 == notificationV}
}
NotificationV
struct NotificationV: Identifiable, Equatable {
var id: UUID = UUID()
var NotificationName : String = ""
var NotificationDiscrip: String = ""
var NotifivationDate:String = ""
var isShowen:Bool = false
init(NotificationName: String, NotificationDiscrip: String, NotifivationDate: String, isShowen: Bool) {
self.NotificationName = NotificationName
self.NotificationDiscrip = NotificationDiscrip
self.NotifivationDate = NotifivationDate
self.isShowen = isShowen
}
}
Doing so you always find correct NotificationV even if they do not have unique name.

Dynamically Resizing only one dimension of an element

So I am implementing a UI in SwiftUI and having trouble implementing the little "title tab" all the way to the left in the picture below. Basically I have a title that is rotated 90 degrees to display on the side of the tab and I want the user to be able to enter a custom title so I need the title area to be able to dynamically resize. However I also have it embeded in an HStack and only want it taking a small amount of the space, rather than a full third. When I implement layoutPriority it decreases the horizontal space that the title area takes, but it no longer expands vertically if the title text takes up more space than the other elements in the HStack. If I remove the layoutPriority it expands vertically to display the full title text as I want but also takes up a full third of the HStack which I dont want. Is there a way I am missing to implement this?
UIElement
HStack{
EventTitleBackground(name:name).rotationEffect(.degrees(270))
.frame(minHeight: 0, maxHeight: .infinity)
.frame(minWidth: 0, maxWidth: .infinity)
.layoutPriority(2)
Spacer()
VStack(alignment: .leading){
Text(time)
.font(.title)
Spacer()
Text("\(truncatedLatitude) \(truncatedLongitude)")
.font(.title)
Spacer()
Text("Altitude: \(truncatedAltitude)")
.font(.title)
}
.layoutPriority(4)
Spacer()
VStack(alignment: .leading){
HStack{
Text("BOBR: \(bobrLargeText)")
.font(.title)
Text(" \(bobrSmallText)")
.font(.body)
}
Spacer()
Text("Heading | Course: \(heading) | \(heading)")
.font(.title)
Spacer()
Text("Groundspeed: \(groundSpeed)")
.font(.title)
}
.layoutPriority(4)
Spacer()
}
I suggest you to check this simple example. By tap on rectangle you can rotate it and with sliders you can change the "width" of rectangles. To be honest, it works exactly as mentioned in Apple docs.
import SwiftUI
struct ContentView: View {
#State var angle0 = Angle(degrees: 0)
#State var angle1 = Angle(degrees: 0)
#State var width0: CGFloat = 100
#State var width1: CGFloat = 100
var body: some View {
VStack {
HStack {
Color.red.frame(width: width0, height: 50)
.onTapGesture {
self.angle0 += .degrees(90)
}
.rotationEffect(angle0).border(Color.blue)
Color.green.frame(width: width1, height: 50)
.onTapGesture {
self.angle1 += .degrees(90)
}
.rotationEffect(angle1).border(Color.blue)
Spacer()
}
Slider(value: $width0, in: 50 ... 200) {
Text("red")
}
Slider(value: $width1, in: 50 ... 200) {
Text("green")
}
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
the result is probably far away from what did you expect ...
Let change the red color part
Color.red//.frame(width: width0, height: 50)
.onTapGesture {
self.angle0 += .degrees(90)
}
.rotationEffect(angle0).border(Color.blue).frame(width: 50, height: width0)
and see the difference
Solution for you could be something like
import SwiftUI
struct VerticalText: View {
let text: String
#Binding var size: CGSize
var body: some View {
Color.red.overlay(
Text(text).padding().fixedSize()
.background(
GeometryReader { proxy -> Color in
// avoid layout cycling!!!
DispatchQueue.main.async {
self.size = proxy.size
}
return Color.clear
}
).rotationEffect(.degrees(-90))
)
.frame(width: size.height, height: size.width)
.border(Color.green)
}
}
struct ContentView: View {
#State var size: CGSize = .zero
var body: some View {
HStack(alignment: .top) {
VerticalText(text: "Hello, World!", size: $size)
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus semper eros non condimentum mattis. In hac habitasse platea dictumst. Mauris aliquam, enim eu vehicula sodales, odio enim faucibus eros, scelerisque interdum libero mi id elit. Donec auctor ipsum at dolor pellentesque, sed dapibus felis dignissim. Sed ac euismod purus, sed sollicitudin leo. Maecenas ipsum felis, ultrices a urna nec, dapibus viverra libero. Pellentesque quis est nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum luctus a est eget posuere.")
}.frame(height: size.width)
.border(Color.red).padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
See, that the size of VerticalText is stored in the parent, doesn't matter if you use it or not. Otherwise the parent will not layout properly.

How do you create a multi-line text inside a ScrollView in SwiftUI?

Since List doesn't look like its configurable to remove the row dividers at the moment, I'm using a ScrollView with a VStack inside it to create a vertical layout of text elements. Example below:
ScrollView {
VStack {
// ...
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer mattis ullamcorper tortor, nec finibus sapien imperdiet non. Duis tristique eros eget ex consectetur laoreet.")
.lineLimit(0)
}.frame(width: UIScreen.main.bounds.width)
}
The resulting Text rendered is truncated single-line. Outside of a ScrollView it renders as multi-line. How would I achieve this inside a ScrollView other than explicitly setting a height for the Text frame ?
In Xcode 11 GM:
For any Text view in a stack nested in a scrollview, use the .fixedSize(horizontal: false, vertical: true) workaround:
ScrollView {
VStack {
Text(someString)
.fixedSize(horizontal: false, vertical: true)
}
}
This also works if there are multiple multiline texts:
ScrollView {
VStack {
Text(someString)
.fixedSize(horizontal: false, vertical: true)
Text(anotherLongString)
.fixedSize(horizontal: false, vertical: true)
}
}
If the contents of your stack are dynamic, the same solution works:
ScrollView {
VStack {
// Place a single empty / "" at the top of your stack.
// It will consume no vertical space.
Text("")
.fixedSize(horizontal: false, vertical: true)
ForEach(someArray) { someString in
Text(someString)
.fixedSize(horizontal: false, vertical: true)
}
}
}
You can force views to fill their ideal size, for example in a vertical ScrollView:
ScrollView {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer mattis ullamcorper tortor, nec finibus sapien imperdiet non. Duis tristique eros eget ex consectetur laoreet.")
.fixedSize(horizontal: false, vertical: true)
}
Feels a little better to me than modifying the frame.
It seems like there is bug in SwiftUI. For now you have to specify height for your VStack container
ScrollView {
VStack {
// ...
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer mattis ullamcorper tortor, nec finibus sapien imperdiet non. Duis tristique eros eget ex consectetur laoreet.")
.lineLimit(nil)
}.frame(width: UIScreen.main.bounds.width, height: 500)
}
The following works for me with Beta 3 - no spacer, no width constraint, flexible height constraint 👍:
ScrollView {
VStack {
Text(longText)
.lineLimit(nil)
.font(.largeTitle)
.frame(idealHeight: .infinity)
}
}
The correct solution is to just make sure to set the alignment for your stack:
VStack(alignment: .leading)
ScrollView {
VStack(alignment: .leading) {
// ...
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer mattis ullamcorper tortor, nec finibus sapien imperdiet non. Duis tristique eros eget ex consectetur laoreet.")
.lineLimit(0)
}.frame(width: UIScreen.main.bounds.width)
}
This way you don't need the fixedSize as the layout is properly defined.