SwiftUI: HStack in ForEach in VStack makes multi line text overlap - swift

I have a Text() view inside a HStack inside of a ForEach inside of a VStack. The text can be a string of any length, and I have no control of that is put inside of it. The problem is that when you run the program, the views in the VStack overlap resulting in this jumbled mess
What I want to do is have a view that resizes its height based on the height of the multi line text view, so that the views never overlap, and always displays the entirety of the string.
Here is some code that generates the view in question:
struct ScrollingChatView: View {
#State var model: WatchModel
#State var messages: [DisplayableMessage] = []
var body: some View {
ScrollView {
if (!messages.isEmpty) {
LazyVStack {
ForEach(messages, id: \.sortTimestamp) { message in
CompactChatView(message: message)
}
}.padding()
} else {
Text("Getting Chat...").padding()
}
}.onReceive(model.chatDriver.publisher) { m in
self.messages = m
}
}
}
struct CompactChatView: View {
#State var message: DisplayableMessage
#State var stringMessage: String? = nil
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .top) {
Text(message.displayAuthor)
.lineLimit(1)
.layoutPriority(1)
Group {
Text(getEmojiText(message))
.font(.headline)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Text(message.displayTimestamp)
.font(.subheadline)
.foregroundColor(Color.gray)
.layoutPriority(1)
}.padding(.all, 6.0)
}
}
func getEmojiText(_ item: DisplayableMessage) -> String {
var fullMessage: String = ""
for m in item.displayMessage {
switch m {
case .text(let s):
fullMessage += s
case .emote(_):
print()
}
}
return fullMessage
}
}
I've tried removing .fixedSize(horizontal: false, vertical: true) from the text view, but it only makes the text cut off after one line, which is not what I want.
If you need more context, the entire project in located at: https://github.com/LiveTL/apple. We're looking at code in the macOS folder.

You may find it useful to instead use a List() with a trailing closure like this...
List(itemList) { item in
Text(item)
}
This should prevent the issue you are running into when trying to display messages. For more information on lists check this out: https://developer.apple.com/documentation/swiftui/list.
You can see an example of this at 25:11 here: https://developer.apple.com/videos/play/wwdc2019/216/

Related

List clipping issue (bug with SwiftUI?)

I'm having this weird issue with the List in macOS Montery Beta 3, where some of the list's items are being clipped, and you have to scroll up and back down to unclip it.
Example:
https://imgur.com/a/P0TUt85
Reproduction
Create a new blank SwiftUI macOS project
Paste the following code:
struct Bla: Identifiable {
var text: String
var subtext: String
var id = UUID()
}
struct ContentView: View {
#State var data = (0..<100).map { Bla(text: "Text: \($0)", subtext: "Subtext: \($0)")}
var body: some View {
NavigationView {
List(data) { item in
VStack(alignment: .leading) {
Text(item.text)
Text(item.subtext)
.foregroundColor(.secondary)
}
}
.toolbar {
Button(action: {
var offset = 0
for i in 0..<data.count {
if i % 2 == 0 {
continue
}
data.remove(at: i - offset)
offset += 1
}
}) {
Image(systemName: "plus")
}
}
}
}
}
Run the app, and scroll the list to the middle.
Press the plus button on the toolbar. This simply removes half of the items.
You should see the clipping issue. Scrolling up and back down fixes it.
Is this something with my code, or with SwiftUI's List?
After adding the .listStyle I dont see the clipping issue anymore, try this:
List(data) { item in
VStack(alignment: .leading) {
Text(item.text)
Text(item.subtext)
.foregroundColor(.secondary)
}
}.listStyle(SidebarListStyle())

SwiftUI TextField takes max width

I have a navigation list with each list item being in this format:
HStack {
TextField("Insert something here.",text: self.$userData.pages[i].title)
.border(Color.blue)
Spacer()
}
This results in the following view:
The touchable area is highlighted by the blue border and it takes the whole width of the row
The problem with this is that despite the list item being a navigation link, the user clicking anywhere along the item will result in them editing the text content. What I would prefer is a TextField that has the same width as a Text:
The blue border wraps the text instead of taking the max width
So if the user clicks outside the TextField, the navigation works, but if they click on the text, it will let them edit the text. (The above view is with Text field).
Apologies if I've asked an unclear or bad question. I'm new to Stack Overflow and SwiftUI.
Edit:
I've tried using the fixedSize modifier, and the TextField correctly wraps my Text, but now the Navigation Link doesn't work (i.e. clicking on it just doesn't navigate). This is my full code:
NavigationLink(destination: PageView(page: self.userData.pages[i])) {
HStack {
Button(action: {}){
TextField(" ", text: self.$userData.pages[i].title)
.fixedSize()
}
.buttonStyle(MyButtonStyle())
.border(Color.blue)
Spacer()
}
}
No need to apologize, your question is clear.
You can do this by using fixedSize()
so your code should be like this
HStack {
TextField("Insert something here.",text: self.$userData.pages[i].title)
.border(Color.blue)
.fixedSize()
Spacer()
}
You can further specify how would you like the stretch to be, either vertical or horizontal or even both by passing parameters like so
.fixedSize(horizontal: true, vertical: false)
UPDATED ANSWER TO MATCH YOUR NEW REQUIREMENTS
import SwiftUI
struct StackOverflow5: View {
#State var text: String = ""
#State var selection: Int? = nil
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: Page2(), tag: 1, selection:self.$selection) {
Color.clear
.onTapGesture {
self.selection = 1
}
}
TextField("Text", text: self.$text)
.fixedSize()
}
}
}
}
struct StackOverflow5_Previews: PreviewProvider {
static var previews: some View {
StackOverflow5()
}
}
struct Page2: View {
var body: some View {
Text("Page2")
}
}
We used a ZStack here to separate between our TextField and our NavigationLink so they can be interacted with separately.
Note the use of Color.clear before our TextField and this is on purpose so that our TextField has interaction priority. Also we used Color.clear because it will stretch as a background and it's clear so it's not visible.
Obviously I hard coded 1 here but this can be from List or a ForEach
Additionally, if you don't want to use selection and tag you can do something like this
...
#State var isActive: Bool = false
...
NavigationLink(destination: Page2(), isActive: self.$isActive) {
Color.clear
.onTapGesture {
self.isActive.toggle()
}
}
....

Cannot use custom view in SwiftUI

For my SwiftUI application, I've created a simple Title view, that has a set font size and text color. Title is declared as follows:
struct Title: View {
var string: String
var body: some View {
Text(string)
.font(.system(size: 32))
.color(Color.black)
}
}
I have the following text objects in my content view's body right now:
var body: some View {
VStack(alignment: .leading) {
Text("Welcome")
.font(.largeTitle)
.color(Color.black)
Text("to SwiftUI")
.font(.largeTitle)
.color(Color.secondary)
}
}
So now, I want to replace these two Texts with my Titles:
var body: some View {
VStack(alignment: .leading) {
Title("Welcome")
Title("to SwiftUI")
}
}
After replacing the views, I'm getting some seemingly unrelated error messages from Xcode, that stop the application from compiling:
Static member 'leading' cannot be used on instance of type 'HorizontalAlignment'
'(LocalizedStringKey) -> Text' is not convertible to '(LocalizedStringKey, String?, Bundle?, StaticString?) -> Text'
'Font' is not convertible to 'Font?'
...and more. Reverting back to Text instead of Title "fixes" the issues.
What's interesting is that I also have a custom PrimaryButton view that I was able to add without any issues:
struct PrimaryButton: View {
var title: String
var body: some View {
Button(action: { print("tapped") }) {
Text(title)
.font(Font.primaryButton)
.offset(y: 1)
.padding(.horizontal, 20)
.padding(.vertical, 14)
}
}
}
...and then using it:
PrimaryButton(title: "Let's go")
Question
Is this simply a beta-issue, or am I missing something?
You need to add string: to your Title() initializer:
var body: some View {
VStack(alignment: .leading) {
Title(string: "Welcome")
Title(string: "to SwiftUI")
}
}
Compiler errors are currently misleading and not located near where the real issue is.
You are missing the string: param in the initializer.
Please find the updated code below:
var body: some View {
VStack(alignment: .leading) {
Title(string: "Welcome")
Title(string: "to SwiftUI")
}
}
FYI:
I have created one sample application
// MARK - CustomView
struct ContentView : View {
var body: some View {
VStack{
CustomView(aString: "First String")
CustomView(aString: "Second String")
}
}
}
// MARK - CustomView
struct CustomView : View {
var aString: String
var body: some View {
Text(aString)
}
}
Today, 01oct2019, Swift prompted me to replace string: with. verbatim: .
Text(verbatim: "Pressure") works today
Text(string: "Pressure") did work yesterday but not today.
hth

Adjusting width / horizontalAlignment with SwiftUI

I have the following layout achieved in SwiftUI:
VStack {
TextField(...)
TextField(...)
Button(...)
}
I tried making my VStack to be 50% of the superview, so I added the following modifier:
VStack { ... }
.relativeWidth(0.5)
Not quite 50% of the superview, but I'm more interested in achieve horizontal centering. Anyone know how to achieve that in SwiftUI?
There's a alignmentGuide(_:computeValue:) modifier that's available, but I'm struggling to give the correct inputs to satisfy the compiler.
I'd use some spacers and HStack ...
struct LoginView: View {
#State var myText: String = ""
var body: some View {
HStack {
Spacer()
VStack {
TextField($myText, placeholder: Text("Email"))
.textFieldStyle(.roundedBorder)
.textContentType(.username)
TextField($myText, placeholder: Text("Password"))
.textFieldStyle(.roundedBorder)
.textContentType(.password)
Button(action: {
// Do your login thing here
}) {
Text("Login")
}
}
Spacer()
}
}
}
It seems to be center aligned already. Letting the HStack know how much width to occupy would finish the job perfectly. Above code by #SMP after modifications and without spacers would look like this:
struct LoginView : View {
#State var myText: String = ""
var body: some View {
HStack {
VStack {
TextField($myText, placeholder: Text("Email"))
.textFieldStyle(.roundedBorder)
.textContentType(.username)
TextField($myText, placeholder: Text("Password"))
.textFieldStyle(.roundedBorder)
.textContentType(.password)
Button(action: {
// Do your login thing here
}) {
Text("Login")
}
}
.relativeWidth(0.7)
}
.relativeWidth(1)
}
}

Views compressed by other views in SwiftUI VStack and List

In my SwiftUI application, I'm trying to implement a UI similar to this:
I've added the two rows for category 1 and category 2. The result looks like this:
NavigationView {
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
Spacer()
}
.navigationBarTitle(Text("Featured"))
}
Now, when added the view for the third category – an VStack with images – the following happens:
This happened, after I replaced Spacer(), with said VStack:
VStack(alignment: .leading) {
Text("Rivers")
.font(.headline)
ForEach(self.categories["Rivers"]!.identified(by: \.self)) { landmark in
landmark.image(forSize: 200)
}
}
My CategoryRow is implemented as follows:
VStack(alignment: .leading) {
Text(title)
.font(.headline)
ScrollView {
HStack {
ForEach(landmarks) { landmark in
CategoryItem(landmark: landmark, isRounded: self.isRounded)
}
}
}
}
Question
It seems that the views are compressed. I was not able to find any compression resistance or content hugging priority modifiers to fix this.
I also tried to use .fixedSize() and .frame(width:height:) on CategoryRow.
How can I prevent the compression of these views?
Update
I've tried embedding the whole outer stack view in a scroll view:
NavigationView {
ScrollView { // also tried List
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
ForEach(...) { landmark in
landmark.image(forSize: 200)
}
}
.navigationBarTitle(Text("Featured"))
}
}
...and the result is worse:
You might prevent the views in VStack from being compressed by using
.fixedSize(horizontal: false, vertical: true)
For example:
I have the following VStack:
VStack(alignment: .leading){
ForEach(group.items) {
FeedCell(item: $0)
}
}
Which render compressed Text()
When I add .fixedSize(horizontal: false, vertical: true)
it doesn't compress anymore
VStack(alignment: .leading){
ForEach(group.items) {
FeedCell(item: $0)
.fixedSize(horizontal: false, vertical: true)
}
}
You could try to add a layoutPriority()operator to your first VStack. This is what the documentation says about the method:
In a group of sibling views, raising a view’s layout priority encourages that view to shrink later when the group is shrunk and stretch sooner when the group is stretched.
So it's a bit like the content compression resistance priority in Autolayout. But the default value here is 0, so you just have to set it to 1 to get the desired effect, like this:
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
Spacer()
}.layoutPriority(1)
VStack(alignment: .leading) {
...
}
Hope it works!
It looks like is not enough space for all your views in VStack, and it compresses some of them. You can embed it into the ScrollView
NavigationView {
ScrollView {
VStack(alignment: .leading) {
CategoryRow(...)
CategoryRow(...)
/// you images and so on
}
}
}
struct ContentView1: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
CategoryListView {
CategoryView()
}
CategoryListView {
SquareCategoryView()
}
CategoryListView {
RectangleCategoryView()
}
}
.padding()
}
.navigationTitle("Featured")
}
}
}
struct CategoryListView<Content>: View where Content: View {
private let viewSize: CGFloat = 150
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack {
HStack {
Text("Category name")
Spacer()
}
ScrollView(.horizontal, showsIndicators: false){
HStack {
ForEach(0..<10) { _ in
content()
}
}
}
}
}
}
struct ContentView1_Previews: PreviewProvider {
static var previews: some View {
ContentView1()
}
}
struct CategoryView: View {
private let viewSize: CGFloat = 150
var body: some View {
Circle()
.fill()
.foregroundColor(.blue)
.frame(width: viewSize, height: viewSize)
}
}
struct RectangleCategoryView: View {
private let viewSize: CGFloat = 350
var body: some View {
Rectangle()
.fill()
.foregroundColor(.blue)
.frame(width: viewSize, height: viewSize * 9 / 16)
}
}
struct SquareCategoryView: View {
private let viewSize: CGFloat = 150
var body: some View {
Rectangle()
.fill()
.foregroundColor(.blue)
.frame(width: viewSize, height: viewSize)
}
}
I think your topmost view (in the NavigationView) needs to be a List, so that it is scrollable:
NavigationView {
List {
...
Or use a ScrollView.
A stack automatically fits within a screen. If you want your content to exceed this, you would have used a ScrollView or a TableView etc i UIKit
EDIT:
Actually, a little Googling brought this result, which seems to be exactly what you are making:
https://developer.apple.com/tutorials/swiftui/composing-complex-interfaces