How do you add a view to the end of a multi-line Text view? - swift

I have a Text view that is dynamic, and can sometimes be a long name that spans 2 or 3 lines. How can I add a view immediately next the last word of a multi-line Text view?
How can I move Age (23) immediately to the right of "last-name" ?
HStack(alignment: .bottom) {
Text("First Middle hyphenated-long-last-name")
Text("Age (\(age))")
.font(.caption2)
}

Add a plus(+) sign between the Text views. This will concatenate the Text views while keeping each Text view's unique formatting.
var body: some View {
HStack(alignment: .bottom) {
Text("First Middle hyphenated-long-last-name") +
Text(" Age (\(age))")
.font(.caption2)
}
}
Examples with different frame widths:
...and the HStack or other grouping is not needed when concatenating unless additional group formatting is needed.

If you don’t mind the age being the same size as the name, you can just put them in the same Text view:
HStack(alignment: .bottom) {
Text("First Middle hyphenated-long-last-name" + " Age (\(age))")
}

Related

Can you access Picker elements during runtime and change their color

I want to change the color of the current selected Element of the Picker. Is this possible? This is my code.
Picker("Select a Project", selection: $selectedProjectString){
ForEach(projects, id: \.self) {
Text($0).lineLimit(1).frame(height:120)
}
}.foregroundColor(Color.white)
.frame(height: 120)
The element that matches selectedProjectString should be a different color than all the others. Or is it possible to access specific elements like with an index or something?
I came up with this solution:
Picker("Select a Project", selection: $selectedProjectString){
ForEach(projects, id: \.self) {
if (startedProject == $0) {
Text($0).lineLimit(1).frame(height:120).foregroundColor(Color.red)
} else {
Text($0).lineLimit(1).frame(height:120)
}
}
}.foregroundColor(Color.white)
.frame(height: 120)
I check if the selected element equals some value and if, I change the style of the Text object it belongs.
Because i change the value of the variable, the view gets reloaded and it refreshes my picker. Now the selected element has color.

toolbar text item gets cropped

There is a very weird behavior happening with the toolbar, I have the following:
.toolbar {
ToolbarItem(placement: .principal) {
HStack {
Imagegoeshere...
.frame(width: 20, height: 20)
Text("Polkadot")
// Text(coinInfo?.name ?? "-")
}
}
}
Just like that it displays correct the image and on the right the name "Polkadot" (that's for testing), if I replace that Text("Polkadot") with the real value (which contains the exact text with no spaces) it crops it: Text(coinInfo?.name ?? "-")
This is how it looks with the testing text:
and this is how it looks with the real value yet the same exact text:
Any idea what could be causing this?
By accident just now I found the solution, still I have no idea why this happens, this is how it got fixed:
Replace the HStack with LazyHStack

Conflicts with custom alignment guides in SwiftUI

In my project I have to use custom alignment guides with nested views. It is quite complex, but I'll try to simplify my case in the following way:
There are 3 possible views: a circle, an upContainer and a downContainer. The two containers are just arrays of other circles/containers. The downContainer aligns only first array to the current line (where there are, for example, the other circles), regardless of what is inside the second array; the upContainer does the opposite. Here's an image to visualize them:
I want to be able to build complex views dynamically using this three elements. Thus I create a data model (which is a simple enum) with nested associated types:
enum Data: Hashable {
case circle
case upContainer([Data], [Data])
case downContainer([Data], [Data])
}
In order to manually align the three elements I create a custom SwiftUI alignment in this way:
extension VerticalAlignment {
struct MyAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[VerticalAlignment.center]
}
}
static let myAlignment = VerticalAlignment(MyAlignment.self)
}
I want the circles in a specific container to have the same color, so I create an extension to quickly generate a random color:
extension Color {
static var random: Color {
return Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
}
Now I am able to create the views corresponding to the three elements:
struct CirclesView: View {
var circles: [Data]
var color: Color = Color.random
var body: some View {
HStack(alignment: .myAlignment) {
ForEach(circles, id: \.self) { value in
switch value {
case .circle:
Circle()
.frame(width: 20, height: 20)
.foregroundColor(color)
case .upContainer(let firstData, let secondData):
UpContainerView(container: (firstData, secondData))
case .downContainer(let firstData, let secondData):
DownContainerView(container: (firstData, secondData))
}
}
}
}
}
struct UpContainerView: View {
var container: ([Data], [Data])
var color = Color.random
var body: some View {
VStack(alignment: .leading) {
CirclesView(circles: container.0, color: color)
CirclesView(circles: container.1, color: color)
.alignmentGuide(.myAlignment) { $0[VerticalAlignment.center] }
}
}
}
struct DownContainerView: View {
var container: ([Data], [Data])
var color = Color.random
var body: some View {
VStack(alignment: .leading) {
CirclesView(circles: container.0, color: color)
.alignmentGuide(.myAlignment) { $0[VerticalAlignment.center] }
CirclesView(circles: container.1, color: color)
}
}
}
At this point the problem should be solved. For example, if I wanted to render this image:
I should be able to do it by writing:
struct ContentView : View {
var myData: [Data] = [.circle, .circle, .downContainer(
[.circle, .circle, .circle, .downContainer(
[.circle, .circle, .circle, .circle], [.circle]
)], [.circle, .circle, .upContainer(
[.circle, .circle], [.circle, .circle, .circle, .circle]
)]
)]
var body: some View {
CirclesView(circles: myData)
}
}
However this is the result:
As you can see, the first container (the brown one) is not rendered as downContainer, in fact it is in centrally aligned to the first two circles green circles. This is because even if we set explicit alignment guides for only one of the two arrays, SwiftUI takes into account also the alignment guides of the subviews of the other array (even if we want to ignore them) and there is a conflict.
struct DownContainerView: View {
var container: ([Data], [Data])
var color = Color.random
var body: some View {
VStack(alignment: .leading) {
CirclesView(circles: container.0, color: color)
.alignmentGuide(.myAlignment) { $0[VerticalAlignment.center] }
//We set the explicit alignment guide only for the first array, I am looking for a way to ignore completely the alignment guides of the second one and to calculate the alignment based only on the first one.
CirclesView(circles: container.1, color: color)
}
}
}
To solve the conflict we should simply ignore the alignment guides of the other array.
The actual question becomes "How can we ignore those alignment guides, which are implicitly and automatically calculated by SwiftUI?"
Note: I now that a different solution could be to use PreferenceKeys, but I believe this problem should be solved using only alignment guides, since it is a mere alignment problem. I also thought that a solution could be to dynamically create a new custom alignment guide for every subcontainer, but I don't how to do it (if it is possible in Swift).
First up some background info that might help:
EVERY view has EXACTLY ONE alignment which is an x and y value in the coordinate system of the view. Note there is no name (like .top or .leading) mentioned here, those are just ways to tell the view which x an y value to return as the alignment guide for this layout.
If you don't tell the view which alignment to use, it defaults to .center.
The parent view uses the child alignment guides by calculating where that coordinate is in its own coordinate system and places all the children so the alignments are all coincident. Some parents may ignore the child value and place the child where it wants (such as a HStack which ignores the x value of its children and places the children side by side.)
The OP asks if the problem can be solved with layout alone, or are preferences required?
The answers are: it can be done with layout alone depending on how sophisticated you need to be, but you may need preferences depending on how sophisticated you need to be.
Looking at the OPs example we see that a CirclesView is an alias for a HStack and both up and down containers are aliases for a VStack containing two CirclesViews.
Also the test case only has CirclesViews with Circles and either an UpContainerView or a DownContainerView (never both up and down, which gives a false impression of how well the layout is working, it is not even as good as the OP thinks).
So unrolling the test data, using the HStacks and VStacks directly, using Text with fixed colours so we can tell what is what and putting it all into one view so we can play with alignment gives:
struct SOAlignment: View {
var body: some View {
HStack { //CirclesView (solid gray)
Text("C")
.foregroundColor(Color.blue)
.overlay(Rectangle().strokeBorder(style: StrokeStyle(lineWidth: 1.0, dash: [5,5])))
Text("C")
.foregroundColor(Color.blue)
.overlay(Rectangle().strokeBorder(style: StrokeStyle(lineWidth: 1.0, dash: [5,5])))
VStack(alignment: .leading) { //DownContainerView (black dashes)
HStack { //CirclesView (top gray dashes)
Text("C")
.foregroundColor(Color.red)
Text("C")
.foregroundColor(Color.red)
Text("C")
.foregroundColor(Color.red)
VStack(alignment: .leading) { //DownContainerView (yellow)
HStack {
Text("C")
Text("C")
Text("C")
Text("C")
}
HStack {
Text("C")
}
}
.foregroundColor(Color.yellow)
}
.foregroundColor(Color.red)
.overlay(Rectangle()
.inset(by: 1.0)
.strokeBorder(style: StrokeStyle(lineWidth: 0.5, dash: [2.5,2.5]))
.foregroundColor(Color.gray))
HStack { //CirclesView (bottom gray dashes)
Text("C")
.foregroundColor(Color.red)
Text("C")
.foregroundColor(Color.red)
VStack(alignment: .leading) { //UpContainerView (pink)
HStack {
Text("C")
Text("C")
}
HStack {
Text("C")
Text("C")
Text("C")
Text("C")
}
}
.foregroundColor(Color.lightPink)
}
.overlay(Rectangle()
.inset(by: 1.0)
.strokeBorder(style: StrokeStyle(lineWidth: 0.5, dash: [2.5,2.5]))
.foregroundColor(Color.gray))
}
.overlay(Rectangle().strokeBorder(style: StrokeStyle(lineWidth: 1.0, dash: [5,5])))
}
.border(Color.gray.opacity(0.5))
.padding()
}
}
I have added .leading horizontal alignment so the output is as close to the circle example as possible, but these play no role so can be ignored. We are only interested in the vertical alignment.
We get the following output.
I have put borders around the top level views. The top CirclesView is solid gray, it contains a blue Circle, a blue Circle and a DownContainerView all with black dashes, and the DownContainerView contains two CirclesViews with gray dashes.
None of the views have an alignment, by which I mean every view has exactly one alignment which has the default .center value. It is worth pausing for a second here—we are trying to align circles and they all have alignment guides at their centres but the centres of the blue circles do not align with any of the centres of the other coloured circles! The centres of the two black dashed blue circles are of course aligned with the centre of the black dashed DownContainerView/VStack.
The fact that the circles have been substituted for text hints at a possible solution. This is precisely the same problem that Apple solved with .firstTextBaseline and .lastTextBaseline. We want our top three red circles to align with the top row of the yellow down container, and we want the bottom two red circles to align with the bottom row of the pink up container. We can achieve this by putting .firstTextBaseline on the top gray dashed CirclesView and .lastTextBaseline on the bottom gray dashed CirclesView. Now that those are aligned we can also put .firstTextBaseline on the solid gray CirclesView.
e.g.
HStack(alignment: .firstTextBaseline) { //CirclesView (solid gray)
And we get...
Hooray problem solved! Not quite: we put the alignment property on the parent CirclesView/HStack not the child Up/DownContainer/VStack. The point is illustrated by changing the solid gray view alignment to .lastTextBaseline
HStack(alignment: .lastTextBaseline) { //CirclesView (solid gray)
The black dashed DownContainerView has been turned into a black dashed UpContainerView without any changes to itself!
This is what I hinted at previously about having only one type of container in a CirclesView in the test example data. We need to ensure that CirclesViews with both up and down containers work but with this solution the property on the CirclesView (first or last text baseline) turns all the immediate child container views into one type (either up or down).
What we need here is a custom alignment and the OP’s custom alignment is perfect. So let’s use it and replace all the text baselines with myAlignments.
e.g.
HStack(alignment: .myAlignment) { //CirclesView (solid gray)
We get the same as before with all centre alignments because myAlignment defaults to .center. The black dashed down container still doesn’t know how to choose between all the myAlignments of its children without some help from the developer. We need to explicitly tell the DownContainerView/VStack how to calculate the value of myAlignment which is half a circle hight from its top and then give it that alignment guide. Similarly the UpContainerView/VStack needs a .myAlignment guide half a circle height above its .bottom.
VStack(alignment: .leading) { //DownContainerView
.
.
.
}
.alignmentGuide(.myAlignment) { d in
d[.top] + halfCircleHeight
}
and
VStack(alignment: .leading) { //UpContainerView
.
.
.
}
.alignmentGuide(.myAlignment) { d in
d[.bottom] - halfCircleHeight
}
Now critically the CirclesView specifies what alignment it wants with (alignment: .myAlignment) and the Up/DownContainerView returns where that is with .alignmentGuide(.myAlignment)
This is the result:
Now the eagle eyed amongst you will have noticed that the top row blue, red and yellow circles do not line up exactly, and similarly the bottom red and pink circles do not line up exactly. This is because I have used a constant
private let halfCircleHeight = 6.0
for my calculations (half the default system font height.) Text has a little bit of white space above and below the line that I have not accounted for, and variations in font size would also misalign things slightly.
This is where preferences would come in. If you have different sized Circles or .padding or some other effect you may need to get the Up/DownContainerViews to interrogate their child view preferences to get the correct value to use in their alignment guide calculation. If this was wanted the child would use an anchorPreference to record its centre and the Up/DownContainerView would choose between all the child view preferences and use the appropriate one in its alignment guide calculation. (This is left as an exercise for the reader.) By the way, an anchorPreference is simply a preference that carries with it information about the coordinate system it is defined. The child can set the value in its coordinate system (its own centre) and the parent can read the value in its coordinate system (where that is in the parent view).
In summary there are no conflicts with alignment guides because there is only one value and therefore nothing to conflict with. Hopefully this explains why the OPs two green circles .myAlignment which have defaulted to .center aligns with the VStack (in the DownContainerView) .myAlignment which has also defaulted to .center.

SwiftUI Form Picker with text and Image

I am trying to create a form picker that shows the currently selected image resource at the top level and when the user selects it to show the detail, I want it to show all of the image resources available.
Here is the relevant section of code:
Picker("Background image:", selection: $task.background) {
ForEach(0 ..< backgroundImages.count) {
Image("Background\($0)").resizable().frame(width: 100, height: 35, alignment: .center)
Text("Background\($0)")
}
}
The problem with this is that in the detail screen I get:
The image is blank and the image and text appear on 2 different rows.
I have tried wrapping the Image and Text lines in an HStack, but that gives a compile time error on some other line. Any suggestions would be helpful.
The following should compile & work well (tested with replaced system images, Xcode 11.2)
Picker("Background image:", selection: $task.background) {
ForEach(0 ..< backgroundImages.count) { i in
HStack {
Image("Background\(i)").resizable().frame(width: 100, height: 35, alignment: .center)
Text("Background\(i)")
}
}
}

SwiftUI beta 7: Trigger `List` to scroll when item added?

Using Xcode 11 beta 7 and Catalina beta 19A536g (6?)
In my SwiftUI app I have a simple view with a list. Below the list I have a button to add items to the list.
After having added some items, the newly added items are not visible, since they are outside the content size of the list.
var body: some View {
NavigationView {
VStack {
List(self.accounts) { account in
Text("\(account.name)")
}
Button("Add new account") {
self.addNewAccount()
}
}.navigationBarTitle("Select account")
}
}
I guess I would like some binding the content offset of the list, passed to my method addNewAccount and trigger a scrolling of the list.
Can I somehow programmatically trigger the list to scroll down to the new content?
the easiest way to achieve it is to flip the list and its content using .scaleEffect(x: 1, y: -1, anchor: .center) in order to have an upside down structured list but with the correct looking content therefore, insertion of item at index 0 in the list will be performed at the bottom with a nice looking animation
List {
ForEach(self.content) { itemContent in
Item(itemContent).scaleEffect(x: 1, y: -1, anchor: .center)
}
}.scaleEffect(x: 1, y: -1, anchor: .center)