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.
Related
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()
}
}
I want to put .edgesIgnoringSafeArea(.all) on a VStack so that the background color covers the whole view.
VStack {
Text("Foo")
}
.edgesIgnoringSafeArea(.all)
.background(Color.red)
This covers the whole view in the background color, which is the behavior I want, but the Text is now also ignoring the safe zones. Is there a better way to have a background cover the entire view without sacrificing the safe zone coverage for the inner content?
You only want the Color to extend into the safe area, so only put ignoresSafeArea on the Color. Also, if you're using iOS 14+, you should use ignoresSafeArea(_:edges:) instead of the deprecated edgesIgnoringSafeArea(_:) as West1 said.
VStack {
Text("Foo")
}
.background(
Color.red.ignoresSafeArea()
)
Full example:
struct ContentView: View {
var body: some View {
VStack {
HStack {
Text("Foo")
Spacer()
}
Spacer()
}
.background(
Color.red.ignoresSafeArea() /// `edgesIgnoringSafeArea` is deprecated
)
}
}
Result:
Given an HStack like the following:
HStack{
Text("View1")
Text("Centre")
Text("View2")
Text("View3")
}
How can I force the 'Centre' view to be in the centre?
Here is possible simple approach. Tested with Xcode 11.4 / iOS 13.4
struct DemoHStackOneInCenter: View {
var body: some View {
HStack{
Spacer().overlay(Text("View1"))
Text("Centre")
Spacer().overlay(
HStack {
Text("View2")
Text("View3")
}
)
}
}
}
The solution with additional alignments for left/right side views was provided in Position view relative to a another centered view
the answer takes a handful of steps
wrap the HStack in a VStack. The VStack gets to control the
horizontal alignment of it's children
Apply a custom alignment guide to the VStack
Create a subview of the VStack which takes the full width. Pin the custom alignment guide to the centre of this view. (This pins the alignment guide to the centre of the VStack)
align the centre of the 'Centre' view to the alignment guide
For the view which has to fill the VStack, I use a Geometry Reader. This automatically expands to take the size of the parent without otherwise disturbing the layout.
import SwiftUI
//Custom Alignment Guide
extension HorizontalAlignment {
enum SubCenter: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.center]
}
}
static let subCentre = HorizontalAlignment(SubCenter.self)
}
struct CentreSubviewOfHStack: View {
var body: some View {
//VStack Alignment set to the custom alignment
VStack(alignment: .subCentre) {
HStack{
Text("View1")
//Centre view aligned
Text("Centre")
.alignmentGuide(.subCentre) { d in d.width/2 }
Text("View2")
Text("View3")
}
//Geometry reader automatically fills the parent
//this is aligned with the custom guide
GeometryReader { geometry in
EmptyView()
}
.alignmentGuide(.subCentre) { d in d.width/2 }
}
}
}
struct CentreSubviewOfHStack_Previews: PreviewProvider {
static var previews: some View {
CentreSubviewOfHStack()
.previewLayout(CGSize.init(x: 250, y: 100))
}
}
Edit: Note - this answer assumes that you can set a fixed height and width of the containing VStack. That stops the GeometryReader from 'pushing' too far out
In a different situation, I replaced the GeometryReader with a rectangle:
//rectangle fills the width, then provides a centre for things to align to
Rectangle()
.frame(height:0)
.frame(idealWidth:.infinity)
.alignmentGuide(.colonCentre) { d in d.width/2 }
Note - this will still expand to maximum width unless constrained!
Asperis answer is already pretty interesting and inspired me for following approach:
Instead of using Spacers with overlays, you could use containers left and right next to the to-be-centered element with their width set to .infinity to stretch them out just like Spacers would.
HStack {
// Fills the left side
VStack {
Rectangle()
.foregroundColor(Color.red)
.frame(width: 120, height: 200)
}.frame(maxWidth: .infinity)
// Centered
Rectangle()
.foregroundColor(Color.red)
.frame(width: 50, height: 150)
// Fills the right side
VStack {
HStack {
Rectangle()
.foregroundColor(Color.red)
.frame(width: 25, height: 100)
Rectangle()
.foregroundColor(Color.red)
.frame(width: 25, height: 100)
}
}.frame(maxWidth: .infinity)
}.border(Color.green, width: 3)
I've put it in a ZStack to overlay a centered Text for demonstration:
Using containers has the advantage, that the height would also translates to the parent to size it up if the left/right section is higher than the centered one (demonstrated in screenshot).
Added on the 24th of July:
This line of code fixes the space in the detail view. However... in the list view the title has become a lot smaller too.
.navigationBarTitle(Text("Egg management"), displayMode: .inline)
Added on the 23th of July:
Thanks to the tips I made a lot of progress. Especially the tip to add borders does wonders. You see exactly what happens!
However, there seems to be a difference between the Xcode Preview canvas, the simulator and the physical device. Is this a bug because -after all- it is still beta? Or is there anything I can do?
As you can see in the images... only in the Xcode Preview canvas the view connects to the top of the screen.
I believe it has something to do with the tabbar. Since when I look at the Xcode Preview canvas with the tabbar... that space above is also there. Any idea how to get rid of that?
Original postings:
This is my code for a detailed list view:
import SwiftUI
struct ContentDetail : View {
#State var photo = true
var text = "Een kip ..."
var imageList = "Dag-3"
var day = "3.circle"
var date = "9 augustus 2019"
var imageDetail = "Day-3"
var weight = "35.48"
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text(date)
.font(.title)
.fontWeight(.medium)
ZStack (alignment: .topLeading){
Image(photo ? imageDetail : imageList)
.resizable()
.aspectRatio(contentMode: .fit)
.background(Color.black)
.padding(.trailing, 0)
.tapAction {
self.photo.toggle() }
HStack {
Image(systemName: day)
.resizable()
.padding(.leading, 10)
.padding(.top, 10)
.frame(width: 40, height: 32)
.foregroundColor(.white)
Spacer()
Image(systemName: photo ? "photo" : "pencil.circle")
.resizable()
.padding(.trailing, 10)
.padding(.top, 10)
.frame(width: 32, height: 32)
.foregroundColor(.white)
}
}
Text(text)
.lineLimit(6)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 6)
} .padding(20)
}
}
#if DEBUG
struct ContentDetail_Previews : PreviewProvider {
static var previews: some View {
ContentDetail()
}
}
#endif
Also included is the preview canvas. What I don't get is how I can make sure the text and photo are aligned to the top (instead of the middle). I tried with Spacers, padding etc.
I must be overseeing something small I guess... but. Can somebody point me in the right direction? Thanks.
Added:
After both answers I added a Spacer() after the last text. In Xcode in the preview canvas everything looks okay now. But on my connected iPhone 7 Plus there are some problems: the view is not aligned to the top, and the image is cropped (icon on the right is gone; white banding to the right).
Adding a Spacer() after the last text shifts everything to the top. Tested on iPhone Xr simulator (not preview).
...
Text(text)
.lineLimit(6)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 6)
Spacer()
}
To remove the space at the top:
VStack {
...
}
.padding(20)
.navigationBarTitle("TITLE", displayMode: .inline)
Think in terms of what a Spacer() does. It "moves" the views as far apart as it can - at least, without a specific space.
So you have this:
VStack {
Text
ZStack {
Image
HStack {
Image
Spacer()
Image
}
}
Text
}
All told, going from inner to outer, you have a horizontal stack of two images placed as far apart (the spacer is between them) inside of a "Z axis" stack that places an image on top of them, inside of a vertical stack that has some text above it.
So if you want to move everything in that vertical stack to the top, you simply need to add one last spacer:
VStack {
Text
ZStack {
Image
HStack {
Image
Spacer()
Image
}
}
Text
Spacer() // <-- ADD THIS
}
Last note: Don't be afraid to adding additional "stacks" to your view. In terms of memory footprint, it's really just a single view with no performance hit.
EDIT: I took your original view and changed everything to placeholders...
var body: some View {
VStack (alignment: .center, spacing: 10) {
Text("Text #1")
.font(.title)
.fontWeight(.medium)
ZStack (alignment: .topLeading) {
Text( "Image #1")
HStack {
Text("Image #2")
Spacer()
Text("Image #3")
}
}
Text("Text #2")
.lineLimit(6)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 6)
} .padding(20)
}
As expected, everything is vertically centered. Adding a Spacer() below "Text #2" throws everything to the top. A couple of thoughts:
Starting there, and add in your Image views one by one. Add in the modifiers like that also.
I don't have the specific images you are rendering, so maybe put a noticeable background color on various things (orange is my personal favorite) and see if the top Image is actually on top but the image makes it appear as though it isn't. A border would work pretty well too.
I'm trying to center a bunch of views in a VStack within a ScrollView in SwiftUI. To simplify things, I'm just trying to get it to work with a single Text view. Here's what I've come up with so far:
var body: some View {
ScrollView(alwaysBounceVertical: true){
HStack(alignment: .center) {
Spacer()
Text("This Is a Test")
Spacer()
} //HStack
.background(Color.green)
} //ScrollView
.background(Color.gray)
}
This results in this:
I want the text to be in the middle like this:
So the HStack should be full-width and the Text should be centered within it. It seems like this should be easy, but I don't get what I'm doing wrong. :)
Using GeometryReader, you can get information about the size of the containing view and use that to size your view.
var body: some View {
GeometryReader { geometry in <--- Added
ScrollView(alwaysBounceVertical: true){
HStack(alignment: .center) {
Spacer()
Text("This Is a Test")
Spacer()
} //HStack
.frame(width: geometry.size.width) <--- Added
.background(Color.green)
} //ScrollView
.background(Color.gray)
}
}
edit: after looking into this more, it seems that part of the problem is that you are using a ScrollView. If you remove that parent, the spacers in the HStack will automatically cause stretching to fill the view. I'm guessing the automatic stretching doesn't happen in ScrollViews because there's no finite limit to how big it can be, how much would it stretch? (because a ScrollView can scroll in any direction)
This seems to be a bug in Xcode 11.0 beta, ScrollView content wouldn't fill the scroll view. If you replace the ScrollView with a List it will work as expected. But if you have to use a scroll view, one workaround is to fix the scroll view's content width.
So your code will look something like this:
ScrollView(alwaysBounceVertical: true) {
HStack(alignment: .center) {
Spacer()
Text("This Is a Test")
Spacer()
} // HStack
.frame(width: UIScreen.main.bounds.width) // set a fixed width
.background(Color.green)
} // ScrollView
.background(Color.gray)
Result:
You should use the modifier frame and set its maxWidth: .infinity.
So it tells its parent: "the wider, the better" :)
var body: some View {
ScrollView(.vertical, showsIndicators: true){
HStack(alignment: .center) {
Spacer()
Text("This Is a Test")
.frame(maxWidth: .infinity) // <- this
Spacer()
} //HStack
.background(Color.green)
} //ScrollView
.background(Color.gray)
}
And this works regardless its parent.
Scrollview or whatever View it's set in.
Paul is doing a great job clarifying it to all of us here:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-give-a-view-a-custom-frame
Answer compatible with Xcode 12.1 (12A7403)
I hope this helps 👍
dsa