SwiftUI re-orderable non full screen list - swiftui

The intended goal is a View that expands in height to show all its elements (no scrolling), that can allow its elements to be drag and dropped to re ordered them. I believe a SwiftUI list will support this drag and drop behaviour however when placed in a VStack with other elements, it limits it's height to be the size of one element. I presume these List views aren't intended to be used within another view?
List {
ForEach(viewModel.sections, id: \.self) { section in
HStack {
Text(section.localisedString)
Spacer()
Image(systemName: "line.horizontal.3")
.foregroundColor(ColourPalette.bodyText)
.smallIcon()
}
.background(ColourPalette.background)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.background(ColourPalette.background)
}
.listStyle(PlainListStyle())
.background(ColourPalette.background)
I would also like to hide the separators however this appears to be an ongoing struggling with SwiftUI but even the SideBarListStyle() doesn't work for me.
Does it make more sense to use a VStack() and if so how easily can the reorder on drag drop and classic list side bar items be implemented?

You have tow options :
1) Using List
For placing the VStack with your List , use Section with header / footer, like that it will scroll with the list
List {
Section(header: Text("Your header here")) {
ForEach(viewModel.sections, id: \.self) { section in
HStack {
Text(section.localisedString)
Spacer()
Image(systemName: "line.horizontal.3")
.foregroundColor(ColourPalette.bodyText)
.smallIcon()
}
.background(ColourPalette.background)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.background(ColourPalette.background)
}
}
And for removidng the separator (IOS 13 support):
Adding UITableView.appearance().separatorColor = .clear anywhere in your code before the List appears should work. While this solution removes the separators, note that all List instances will be bound to this style as there’s no official way currently to only remove separators of specific instances. You may be able to run this code in onAppear and undo it in onDisappear to keep styles different.
2) Use VStack / LazyVStack

Related

Trouble With Constraints for a Navigation Link

Here is my code:
import SwiftUI
struct ContentView: View {
var link: some View {
NavigationLink(destination: OtherView()) {
Text("NLTitle")
}
.foregroundColor(.blue)
}
var body: some View {
NavigationView {
List {
ZStack {
HStack {
Text("1")
Spacer()
}.padding([.leading, .trailing], 20)
HStack {
Spacer()
Text("2")
.multilineTextAlignment(.center)
.frame(
alignment: .center
)
Spacer()
}
.padding([.leading, .trailing], 20)
HStack {
Spacer()
link
}.padding([.leading, .trailing], 20)
}
}
}
}
}
I have a NavigationLink (named 'link') in a list cell. I would like for the Text within 'link' to be to the rightmost side of the view. To try to accomplish this, I inserted 'link' in an HStack and put a Spacer() before it to try and push it to the rightmost part of the view. When I run the app though, the Text ends up in between Text("1") and Text("2") and I can't figure out why. I want Text("1") to be in the leftmost part of the view, Text("2") to be in the center of the view, and 'link' to be in the rightmost part of the view. I have provided visuals (the colors aren't important, I just wanted to make the different Texts clear):
Desired layout:
What I get instead:
I found that if I take everything out of the List view I get my desired layout. Also, if I keep everything in the List view and replace the NavigationLink with a Button I get my desired layout. The goal is to get the desired layout without having to change either of these aspects.
For the sake of clarity, I didn't include the code for OtherView() as I don't think it's necessary for this question.
The "quick" fix is to add fixedSize() to the NavigationLink
var link: some View {
NavigationLink(destination: Text("OtherView()")) {
Text("NLTitle")
}
.foregroundColor(.blue)
.fixedSize()
}
That will allow the link to shrink.

Text with Text.DateStyle in SwiftUI View in Complication alignment

I want to create a complication rendered by a SwiftUI View that contains a label and a timer value.
I want the label to be on the complication background layer, and the timer value to be on the complication foreground layer so that they get tinted separately.
I would like this line of text, comprised of 2 parts, to be centered.
The trouble is, when using Text.DateStyle.timer, the Text behaves differently within a complication vs in a normal view.
In a normal view the Text frame behaves as any other text, only taking the space it needs.
When displayed in a complication, the Text frame expands to fill all the space it can, and the text within is left aligned.
This makes it so I cannot find a way to center the group of 2 Texts.
I tried a somewhat hacky approach with infinite spacers to try to steal the extra space from the Text that has the expanding frame. This works to center the content, but it causes the Text to truncate.
HStack {
Text("T:")
.foregroundColor(.accentColor)
Text(Date(), style: .timer)
.complicationForeground()
}
HStack {
Spacer()
.frame(maxWidth: .infinity)
HStack {
Text("T:")
.foregroundColor(.accentColor)
Text(Date(), style: .timer)
.complicationForeground()
}
Spacer()
.frame(maxWidth: .infinity)
}
A normal preview:
A preview of rendering within complication:
CLKComplicationTemplateGraphicExtraLargeCircularView(
ExtraLargeStack()
)
.previewContext(faceColor: .multicolor)
Edit to show full code
import ClockKit
import SwiftUI
struct ExtraLargeStack: View {
var body: some View {
VStack(alignment: .center) {
HStack {
Text("T:")
.foregroundColor(.accentColor)
Text(Date(), style: .timer)
.complicationForeground()
}
HStack {
Spacer()
.frame(maxWidth: .infinity)
HStack {
Text("T:")
.foregroundColor(.accentColor)
Text(Date(), style: .timer)
.complicationForeground()
}
Spacer()
.frame(maxWidth: .infinity)
}
}
.font(.system(size: 18, weight: .regular))
.lineLimit(1)
}
}
struct ExtraLargeStack_Previews: PreviewProvider {
static var previews: some View {
/// Preview normal view
// ExtraLargeStack()
/// Preview as Complication
CLKComplicationTemplateGraphicExtraLargeCircularView(
ExtraLargeStack()
)
.previewContext(faceColor: .multicolor)
}
}
Edit: Another partial solution
Based on suggestions from #Yrb, an overlay provides a partial solution that may be good enough for my use case.
The following does not fully center the 2 part line, but it is pretty close.
HStack {
// Use placeholder text to create a view with the appropriate size for _most_ timer values that I need to support
Text("L: 00:00 ").hidden()
}
.overlay(
// overlay the real content, which is constrained to the frame created by the hidden placeholder.
HStack(spacing: 5) {
Text("L:")
.foregroundColor(.accentColor)
Text(Date() - 3599, style: .timer)
.complicationForeground()
}
)
So, I figured out what the issue with aligning Text(Date(), style: .timer) is. The timer format from hours on down. The document give this as an example: 2:59 36:59:01. It appears that .timer reserves all of the possible space it needs and then is formatted on that possible space, not the space actually used. There does not appear to be any way to change this behavior, even if your goal is a 5 minute countdown timer.
I think you need to consider slight UI change. I did find that you can change the alignment of the displayed Text with a .timer by using .multilineTextAlignment(), but that is about all you can do. The following code demonstrates this:
struct ExtraLargeStack: View {
var body: some View {
// I removed (alignment: .center) as it is redundant. VStacks default to center
VStack {
// I put the negative spacing to tighten the T: with the timer
VStack(spacing: -6) {
Text("T:")
.foregroundColor(.accentColor)
Text(Date(), style: .timer)
// If you center with .multilineTextAlignment the timer
// will be centered
.multilineTextAlignment(.center)
.complicationForeground()
}
HStack {
HStack {
Text(Date(), style: .timer)
.multilineTextAlignment(.center)
.complicationForeground()
.overlay(
Text("T:")
.foregroundColor(.accentColor)
// This offset would need to be computed
.offset(x: -30, y: 0)
)
}
}
}
.font(.system(size: 18, weight: .regular))
}
}
I left the second timer as an HStack, but I put your Text("T") as an .overlay() with a .offset(). I don't particularly like this as it will be fragile if you attempt to adjust the offset for the additional time units, but if you have a limited range, it may work well enough. Also, if you use .monospaced on the timer text, the computation should be a linear amount.

How to get toolbar separator in SwiftUI?

I would like to get the separator in SwiftUI, but I didn't find the way. This was screenshot from mail.app.
If your view elements are in a HStack (like your mail.app suggest) using Divider() will give you a vertical "separator".
Elsewhere Divider() will give you a horizontal "separator".
You can adjust its size, like this: Divider().frame(width: 123)
You can of course do more things with Dividers, such as set its thickness or height with different color:
HStack {
Divider().frame(width: 5, height: 50).background(Color.blue)
Image(systemName: "line.3.horizontal.decrease.circle")
Divider().frame(width: 10, height: 100).background(Color.pink)
Image(systemName: "envelope")
Divider().frame(width: 15, height: 150).background(Color.green)
}
Here is the right way of doing such thing, do not use Divider, because it has lots of issues. With Divider you cannot control the thickness, also it has issue with updating color, wired Xcode complain in console in some cases, also space issue, it takes more space than it needs. In general it does not worth to use it.
struct ContentView: View {
var body: some View {
HStack {
Group {
Image(systemName: "mail")
Capsule().fill(Color.secondary).frame(width: 2.0)
Image(systemName: "trash")
}
.frame(width: 25, height: 25)
}
}
}
One alternative solution that may be more useful in some cases (e.g if you want a customisable toolbar the accepted solution won't work):
ToolbarItem (placement: .primaryAction) {
HStack {
Divider()
}
}

Swiftui hide disclosure arrow

I have this View
NavigationView {
GeometryReader { geometry in
List {
ForEach(self.viewModel.items) { item in
HStack(spacing: 0, content: {
ZStack {
RowItemView(data: item.FirstItem)
NavigationLink(destination: CustomView(data: item.FirstItem))
{
EmptyView()
}
}
.frame(width: geometry.size.width / 2, alignment: .center)
if (item.SecondItem != nil) {
ZStack {
RowItemView(data: item.SecondItem!)
NavigationLink(destination: CustomView(data: item.SecondItem!))
{
EmptyView()
}
}
})
}.listRowInsets(EdgeInsets())
}
}
I want to hide the disclouse arrow of the NavigationView.
I try to add .buttonStyle(PlainButtonStyle()) or add a negative trailing to the navigationLink, but it doesn't change.
I have already read this question and this one but they do not work in my case, probably because I'm creating a grid and not a plain list.
In this scenario the possible approach is to use zero frame, as following
NavigationLink(destination: CustomView(data: item.FirstItem)) {
EmptyView()
}.frame(width: 0)
Thanks to #Asperi I figured out what is the problem.
With a zero EdgeInsets in listRowInsets the arrow still show.
So I create this trick (Works in Xcode 13.3.1)
List {
ForEach(self.viewModel.items) { item in
//code for creating the row
}.listRowInsets(EdgeInsets.init(top: 8, leading: 0, bottom: 8, trailing: 0))
Put a value for both top and bottom.
I do not know if it is the correct way to get rid of that annoying arrow, but for my app it works :-)

SwiftUI - Resizable List height that dependent on an element count

I have some troubles with dynamically changing List height that dependent on elements count.
I tried this solution but it didn't work.
List {
ForEach(searchService.searchResult, id: \.self) { item in
Text(item)
.font(.custom("Avenir Next Regular", size: 12))
}
}.frame(height: CGFloat(searchService.searchResult.count * 20))
TL;DR
This is not how the designers of SwiftUI want you to use lists. Either you will have to come up with a hacky solution that will probably break in the future (see below), or use something other than a list.
Background
SwiftUI tends to have two types of Views
Those designed to be easily modifiable and composable, providing unlimited customizability for a unique look and feel.
Those designed to provide a standard, consistent feel to some type of interaction, regardless of what app they are used in.
An example of type 1 would be Text. You can change font size, weight, typeface, color, background, padding, etc. It is designed for you to modify it.
An example of type 2 would be List. You are not in direct control of row height, you can't change the padding around views, you can't tell it to show only so many rows, etc. They don't want it to be very customizable, because then each app's lists would behave differently, defeating the purpose of a standard control.
List is designed to fill the entire parent View with as many rows as possible, even if they are empty or only partially on screen (and scroll if there are too many to show at once).
Your issue
The problem you are having comes about because the size of the List does not affect the size of its rows in any way. SwiftUI doesn't care if there are too many or too few rows to fit in your preferred size; it will happily size its rows according to content, even if it means they don't all show or there are empty rows shown.
If you really need rows to resize according to the size of their parent, you should use a VStack. If it needs to scroll, you will need to wrap the VStack in a ScrollView.
Hacky solution
If you still insist on using a list, you will have to do something like the following:
struct ContentView: View {
#State private var textHeight: Double = 20
let listRowPadding: Double = 5 // This is a guess
let listRowMinHeight: Double = 45 // This is a guess
var listRowHeight: Double {
max(listRowMinHeight, textHeight + 2 * listRowPadding)
}
var strings: [String] = ["One", "Two", "Three"]
var body: some View {
VStack {
HStack {
Text(String(format: "%2.0f", textHeight as Double))
Slider(value: $textHeight, in: 20...60)
}
VStack(spacing: 0) {
Color.red
List {
ForEach(strings, id: \.self) { item in
Text(item)
.font(.custom("Avenir Next Regular", size: 12))
.frame(height: CGFloat(self.textHeight))
.background(Color(white: 0.5))
}
}
// Comment out the following line to see how List is expected to work
.frame(height: CGFloat(strings.count) * CGFloat(self.listRowHeight))
Color.red
}.layoutPriority(1)
}
}
}
The slider is there to show how the list row heights change with the height of their child view. You would have to manually pick listRowPadding and listRowMinHeight to get the appearance that best matches your expectation. If Apple ever changes how a List looks (changes padding, minimum row heights, etc.) you will have to remember to come back and adjust these values manually.
Self size List:
If you want a List to show it's content all at once, It means you don't need the recycling feature (the key feature of the list), So all you need is to not using a List! Instead, you can use ForEach directly, then it will size itself based on it's content:
ForEach(searchService.searchResult, id: \.self) { item in
VStack(alignment: .leading) {
Text(item).font(.custom("Avenir Next Regular", size: 12))
Divider()
}.padding(.horizontal, 8)
}
You can change all sizes and spacings according to your needs
Note that You can use LazyVStack from iOS 14 to make it lazy-load and boost its performance.
Starting from iOS 14 you can use LazyVStack instead of List.
List seems to span entire parent view height independent of rows height or count.
LazyVStack {
ForEach(searchService.searchResult, id: \.self) { item in
Text(item)
.font(.custom("Avenir Next Regular", size: 12))
}
}.frame(height:
Other solution is to set .frame(height: ) on List based on rowCount*rowHeight or other GeometryReader -> geometry.size.height
SwiftUi has evolved. Here's a plain and simple answer for SwiftUI 3: https://stackoverflow.com/a/65769005/4514671