I want to have three labels each with a subtitle and distributed equally.
The 1st title/subtitle should be leading aligned (see green border below)
The center title/subtitle should centered (see blue border below)
The last title/subtitle should be trailing aligned. (see red border below)
They all should take as much width as they can as long as they are equal width
This is a Sketch of what I need:
and this is the SwiftUI view that I can up with
The issue is that I cannot align the titles as needed.
Here is my current non working code
import SwiftUI
struct MyView: View {
public var body: some View {
HStack(spacing: 0) {
cellView(top: "Leading",
bottom: "subtitle",
alignment: .leading)
.border(Color.green)
cellView(top: "Centered",
bottom: "subtitle",
alignment: .center)
.border(Color.blue)
cellView(top: "Trailing",
bottom: "subtitle",
alignment: .trailing)
.border(Color.red)
}
.padding()
}
private func cellView(top: String, bottom: String, alignment: HorizontalAlignment) -> some View {
VStack(alignment: alignment, spacing: 0) {
Text(top)
.frame(maxWidth: .infinity)
.font(.subheadline)
.lineLimit(1)
.border(Color.black)
Text(bottom)
.font(.subheadline)
.lineLimit(1)
.border(Color.black)
}
.border(Color.black)
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}
Instead of trying to align your stack's subviews, you can make them all prefer expansion and then directly align the content of their frame (note that since alignment options match between HorizontalAlignment & Alignment you don't need to change something in your body):
private func cellView(top: String, bottom: String, alignment: Alignment) -> some View {
VStack(spacing: 0) {
Text(top)
.frame(maxWidth: .infinity, alignment: alignment)
.font(.subheadline)
.lineLimit(1)
.border(Color.black)
Text(bottom)
.frame(maxWidth: .infinity, alignment: alignment)
.font(.subheadline)
.lineLimit(1)
.border(Color.black)
}
.border(Color.black)
}
which produces the desired result
PS: You can even simplify your cellView by grouping common modifiers:
private func cellView(top: String, bottom: String, alignment: Alignment) -> some View {
VStack(spacing: 0) {
Group {
Text(top)
.font(.subheadline)
Text(bottom)
.font(.subheadline)
}
.frame(maxWidth: .infinity, alignment: alignment)
.lineLimit(1)
.border(Color.black)
}
.border(Color.black)
}
The above can be achieved using the LazyVGrid.
import SwiftUI
struct AlignmentView: View {
var body: some View {
MyView()
}
}
struct aqiItem {
let aqi: Int
let color: Color
}
struct MyView: View {
let columns = [
GridItem(.adaptive(minimum: 80))
]
var body: some View {
LazyVGrid(
columns: [
GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())
],
alignment: .center,
spacing: 10,
pinnedViews: [],
content: {
cellView(top: "Leading",
bottom: "subtitle",
alignment: .leading)
.border(Color.green)
cellView(top: "Centered",
bottom: "subtitle",
alignment: .center)
.border(Color.blue)
cellView(top: "Trailing",
bottom: "subtitle",
alignment: .trailing)
.border(Color.red)
})
}
}
private func cellView(top: String, bottom: String, alignment: Alignment) -> some View {
VStack(spacing: 0) {
Group {
Text(top)
.font(.subheadline)
Text(bottom)
.font(.subheadline)
}
.frame(maxWidth: .infinity, alignment: alignment)
.lineLimit(1)
.border(Color.black)
}
.border(Color.black)
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
AlignmentView()
.previewDevice("iPhone 8")
}
}
Related
I want to achieve this (For the screenshots I used constant heights, just to visualize what I am looking for):
The two Views containing text should together have the same height as the (blue) Image View. The right and left side should also be of the same width. The important part: The Image View can have different aspect ratios, so I can't set a fixed height for the parent View. I tried to use a GeometryReader, but without success, because when I use geometry.size.height as height, it takes up the whole screen height.
This is my code:
import SwiftUI
struct TestScreen: View {
var body: some View {
VStack {
GeometryReader { geometry in
HStack {
Image("blue-test-image")
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
VStack {
Text("Some Text")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
Text("Other Text")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
}
.frame(maxWidth: .infinity, maxHeight: geometry.size.height /* should instead be the height of the Image View */)
}
}
Spacer()
}
.padding()
}
}
This is a screenshot of what my code leads to:
Any help would be appreciated, thanks!
As #Asperi pointed out, this solves the problem: stackoverflow.com/a/62451599/12299030
This is how I solved it in this case:
import SwiftUI
struct TestScreen: View {
#State private var imageHeight = CGFloat.zero
var body: some View {
VStack {
HStack {
Image("blue-test-image")
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
.background(GeometryReader {
Color.clear.preference(
key: ViewHeightKeyTestScreen.self,
value: $0.frame(in: .local).size.height
)
})
VStack {
Text("Some Text")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
Text("Other Text")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
}
.frame(maxWidth: .infinity, minHeight: self.imageHeight, maxHeight: self.imageHeight)
}
.onPreferenceChange(ViewHeightKeyTestScreen.self) {
self.imageHeight = $0
}
Spacer()
}
.padding()
}
}
struct ViewHeightKeyTestScreen: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
struct TestScreen_Previews: PreviewProvider {
static var previews: some View {
TestScreen()
}
}
I am trying to use the PageTabview option to allow a user to move through a series of pages whose data is coming from a JSON file. I want to be able to limit the number of visible dots to 5 or 6 even if there are many values in the field. What I don't want is to have 25 dots if there are twenty-five values in the field. How would I do that? I want to be able to show indicator like an arrow that tells the user there is more to come...Thank you.
My code is below:
struct TopicsExperienceCards: View {
#Binding var closeExperience: Bool
let etype: EItype
var body: some View {
//start of content of zstack layout
ZStack {
VStack(spacing: 20) {
HStack{
Rectangle()
.fill(Color.white)
.frame(width: 300, height: 1, alignment: .center)
Spacer()
Button(action: {
closeExperience = false })
{
Image(systemName:"xmark")
.foregroundColor(Color(etype.accentcolor))
.padding()
}
} //HSTACK
TabView {
ForEach(etype.experience,id: \.self) { item in
// Display the content of a card //
VStack (alignment:.center, spacing:0){
Text(item)
.padding()
.frame(width:300, height:300, alignment:.center)
Divider()
Spacer()
Text("Room for an image")
Spacer()
Spacer()
}//VSTACK
//End of display of content of the card //
} //: FOREACH
} //: TABVIEW
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.onAppear {
setupAppearance() }
} //VSTACK
} //: ZStack
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 16)
.strokeBorder()
.foregroundColor(Color(etype.accentcolor)))
.cornerRadius(16.0)
.padding()
}
}
struct TopicsExperienceCards_Previews: PreviewProvider {
static let etypes: [EItype] = Bundle.main.decode("eibasestructure.json")
static var previews: some View {
TopicsExperienceCards(closeExperience:.constant(true),etype:etypes[1])
}
}
enter image description here
System dots view is limited to around 10 dots, maybe depending on the device. You can't change this value.
Instead of that you can hide system one, and create your own view with dots. As an example you can follow this article, so at the end you'll have something like this:
#State var currentIndex = 0
var body: some View {
//start of content of zstack layout
ZStack {
printUI(currentIndex)
VStack(spacing: 20) {
HStack{
Rectangle()
.fill(Color.white)
.frame(width: 300, height: 1, alignment: .center)
Spacer()
Button(action: {
closeExperience = false })
{
Image(systemName:"xmark")
.foregroundColor(Color.red)
.padding()
}
} //HSTACK
TabView(selection: $currentIndex.animation()) {
ForEach(etype.experience.indices,id: \.self) { i in
let item = etype.experience[i]
// Display the content of a card //
VStack (alignment:.center, spacing:0){
Text(item)
.padding()
.frame(width:300, height:300, alignment:.center)
Divider()
Spacer()
Text("Room for an image")
Spacer()
Spacer()
}//VSTACK
//End of display of content of the card //
} //: FOREACH
} //: TABVIEW
.tabViewStyle(PageTabViewStyle())
.onAppear {
setupAppearance()
}
Fancy3DotsIndexView(numberOfPages: etype.experience.count, currentIndex: currentIndex)
.padding()
.background(Color.green)
.clipShape(Capsule())
} //VSTACK
} //: ZStack
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 16)
.strokeBorder()
.foregroundColor(Color.red)
)
.cornerRadius(16.0)
.padding()
}
Result:
I am trying to put together a view that consists of a top header view, a bottom content view, and a view that sits on top centered on the line splitting the two views. I figured out I need an alignment guide within a ZStack to position the middle view but I am having problems getting the items in the lower content view centered without a gap.
This code:
extension VerticalAlignment {
struct ButtonMid: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
return context[.bottom]
}
}
static let buttonMid = VerticalAlignment(ButtonMid.self)
}
struct ContentView: View {
var body: some View {
VStack {
ZStack(alignment: Alignment(horizontal: .center, vertical: .buttonMid)) {
HeaderView()
.frame(maxWidth: .infinity, minHeight: 200, idealHeight: 200, maxHeight: 200, alignment: .topLeading)
// BodyView()
// .alignmentGuide(.buttonMid, computeValue: { dimension in
// return dimension[VerticalAlignment.top]
// })
Color.red
.frame(width: 380, height: 50, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.alignmentGuide(.buttonMid, computeValue: { dimension in
return dimension[VerticalAlignment.center]
})
}
BodyView()
}
.edgesIgnoringSafeArea(.top)
}
}
struct HeaderView: View {
var body: some View {
Color.green
}
}
struct BodyView: View {
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
BodyContent()
Spacer()
}
Spacer()
}
.background(Color.blue)
}
}
struct BodyContent: View {
var body: some View {
VStack {
Text("Line 1")
Text("Line 2")
Text("Line 3")
}
}
}
give you this:
which centers the lower content they way I want it however it leaves a gap between the upper and lower views. If I uncomment the BodyView code in the ZStack and comment it out in the VStack like so:
struct ContentView: View {
var body: some View {
VStack {
ZStack(alignment: Alignment(horizontal: .center, vertical: .buttonMid)) {
HeaderView()
.frame(maxWidth: .infinity, minHeight: 200, idealHeight: 200, maxHeight: 200, alignment: .topLeading)
BodyView()
.alignmentGuide(.buttonMid, computeValue: { dimension in
return dimension[VerticalAlignment.top]
})
Color.red
.frame(width: 380, height: 50, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.alignmentGuide(.buttonMid, computeValue: { dimension in
return dimension[VerticalAlignment.center]
})
}
// BodyView()
}
.edgesIgnoringSafeArea(.top)
}
}
gives you:
which leaves the content uncentered. How can I keep it centered? I tried putting it in a GeometryReader and that had the same results.
You don't need a custom VerticalAlignment. Instead you can put the middle view as an overlay and align it to the top border of the bottom view:
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
HeaderView()
.frame(height: 200)
BodyView()
.overlay(
Color.red
.frame(width: 380, height: 50)
.alignmentGuide(.top) { $0[VerticalAlignment.center] },
alignment: .top
)
}
.edgesIgnoringSafeArea(.top)
}
}
I am having difficultly horizontally aligning two elements inside different container views. I want to align the two scores (in red) horizontally. I tried using a custom alignment guide (and custom CoordinateSpace), and although this did align the two scores, it also caused the corresponding stacks two change. What am I missing? Surely there must be an easy way to do this.
struct ContentViewExample: View {
var body: some View {
VStack(alignment: .leading) {
HStack {
Label("Team 1", systemImage: "seal")
.font(.title3)
Spacer()
Text("65")
.background(Color.red)
Text("Final")
.font(.caption)
.padding(.leading)
}
HStack {
Label("Team No 2", systemImage: "seal")
.font(.title3)
Spacer()
Text("70")
.background(Color.red)
}
}.padding(.horizontal)
}
}
My solution would be to use a (Lazy?)VGrid:
struct Result : Identifiable, Hashable {
var id = UUID()
var name: String
var label: String
var score: Int
var final: Bool
}
var finalScore = [Result(name: "Team 1", label: "seal.fill", score: 167, final: true),
Result(name: "Team No 2", label: "seal", score: 65, final: false)]
struct ContentView: View {
private var columns: [GridItem] = [
GridItem(alignment: .leading),
GridItem(alignment: .trailing),
GridItem(alignment: .leading)
]
var body: some View {
LazyVGrid(
columns: columns,
alignment: .center,
spacing: 16,
pinnedViews: [.sectionHeaders, .sectionFooters]
) {
Section(header: Text("Results").font(.title)) {
ForEach(finalScore) { thisScore in
Label(thisScore.name, systemImage: thisScore.label)
.background(Color(UIColor.secondarySystemBackground))
Text(String(thisScore.score))
.background(Color(UIColor.secondarySystemBackground))
Text(thisScore.final == true ? "Final" : "")
.background(Color(UIColor.secondarySystemBackground))
}
}
}
.border(Color(.blue))
}
}
gotta fiddle with the colors though, I just put some backgrounds and a border to see which things are where... And maybe optimize the column's widths if needed.
To read up on Grids I suggest this: https://swiftwithmajid.com/2020/07/08/mastering-grids-in-swiftui/
And btw: custom alignment won't work because the entire HStack would be moved left and right, you'd have to adjust the width of the "columns" there, that would be extra hassle.
I decided to write a medium.com article to answer this question and did just that. Here is the solution looks like and here is the code too. Here is the article and well the solution.
https://marklucking.medium.com/a-real-world-alignment-challenges-in-swiftui-2-0-ff440dceae5a
In short I setup the four labels with the correct alignment and then aligned the four containers with each other.
In this code I included a slider so that you can better understand how it works.
import SwiftUI
struct ContentView: View {
private var newAlignment1H: HorizontalAlignment = .leading
private var newAlignment1V: VerticalAlignment = .top
private var newAlignment2H: HorizontalAlignment = .trailing
private var newAlignment2V: VerticalAlignment = .top
#State private var zeroX: CGFloat = 160
var body: some View {
VStack {
ZStack(alignment: .theAlignment) {
HStack {
Label {
Text("Team 1")
.font(.system(size: 16, weight: .semibold, design: .rounded))
} icon: {
Image(systemName:"seal")
.resizable()
.scaledToFit()
.frame(width: 30)
}.labelStyle(HorizontalLabelStyle())
}
.border(Color.blue)
.alignmentGuide(.theHorizontalAlignment, computeValue: {d in zeroX})
// .alignmentGuide(.theHorizontalAlignment, computeValue: {d in d[self.newAlignment2H]})
// .alignmentGuide(.theVerticalAlignment, computeValue: {d in d[self.newAlignment1V]})
HStack {
Text("65")
.font(.system(size: 16, weight: .semibold, design: .rounded))
.background(Color.red.opacity(0.2))
.frame(width: 128, height: 32, alignment: .trailing)
Text("Final")
.font(.system(size: 16, weight: .semibold, design: .rounded))
.background(Color.red.opacity(0.2))
.frame(width: 48, height: 32, alignment: .leading)
}
.border(Color.green)
}
ZStack(alignment: .theAlignment) {
HStack {
Label {
Text("Team No 2")
.font(.system(size: 16, weight: .semibold, design: .rounded))
} icon: {
Image(systemName:"seal")
.resizable()
.scaledToFit()
.frame(width: 30)
}.labelStyle(HorizontalLabelStyle())
}
// .alignmentGuide(.theHorizontalAlignment, computeValue: {d in d[self.newAlignment2H]})
// .alignmentGuide(.theVerticalAlignment, computeValue: {d in d[self.newAlignment1V]})
.alignmentGuide(.theHorizontalAlignment, computeValue: {d in zeroX})
.border(Color.pink)
HStack {
Text("70")
.font(.system(size: 16, weight: .semibold, design: .rounded))
.background(Color.red.opacity(0.2))
.frame(width: 128, height: 32, alignment: .trailing)
Text("")
.background(Color.red.opacity(0.2))
.frame(width: 48, height: 32, alignment: .leading)
}
.border(Color.orange)
}
VStack {
Slider(value: $zeroX, in: 0...200, step: 10)
Text("\(zeroX)")
}
}
}
}
struct VerticalLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
VStack(alignment: .center, spacing: 8) {
configuration.icon
configuration.title
}
}
}
struct HorizontalLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .center, spacing: 8) {
configuration.icon
configuration.title
}
}
}
extension VerticalAlignment {
private enum TheVerticalAlignment : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[VerticalAlignment.top]
}
}
static let theVerticalAlignment = VerticalAlignment(TheVerticalAlignment.self)
}
extension HorizontalAlignment {
private enum TheHorizontalAlignment : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[HorizontalAlignment.leading]
}
}
static let theHorizontalAlignment = HorizontalAlignment(TheHorizontalAlignment.self)
}
extension Alignment {
static let theAlignment = Alignment(horizontal: .theHorizontalAlignment, vertical: .theVerticalAlignment)
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is a possible approach (a-la column-oriented)
HStack {
// image column
VStack {
Image(systemName: "seal")
Image(systemName: "seal")
}
.font(.title3)
// text column
VStack(alignment: .leading) {
Text("Team 1")
Text("Team No 2")
}
.font(.title3)
Spacer()
// score column
VStack {
Text("65")
Text("70")
}
.background(Color.red)
// note column
VStack {
Text("Final")
Text("")
}
.font(.caption)
.padding(.leading)
}
.frame(maxWidth: .infinity)
.padding(.horizontal)
As you can see the picture I want to place the search bar exactly to the top of the safeArea but the proxy.safeAreaInsets has not the proper value because in the PreviewProvider the parent uses edgesIgnoringSafeArea.
what can I do ? is there any way to access safeAreaInsets?
struct FindView: View {
// MARK: - Properties
#ObservedObject var viewModel: FindViewModel
init(viewModel: FindViewModel){
self.viewModel = viewModel
}
var body: some View {
GeometryReader { proxy in
ScrollView(.vertical, showsIndicators: true, content: {
VStack(alignment: .leading, spacing: 8){
SearchBar()
.frame(height: 48, alignment: .center)
.padding(.all, 16)
Text("Categories")
.multilineTextAlignment(.leading)
.font(.system(size: 21))
.padding(.all, 16)
}.frame(width: proxy.size.width)
})
}
}
}
struct FindView_Previews: PreviewProvider {
static var previews: some View {
ZStack(alignment: .center, content: {
Rectangle().foregroundColor(.red)
FindView(viewModel: FindViewModel())
}).edgesIgnoringSafeArea(.all)
}
}
simple way;
struct test: View {
var body: some View {
VStack{
Text("Your view comes here")
Spacer()
}
.frame(minWidth:0, maxWidth: .infinity, minHeight: 0,maxHeight: .infinity, alignment: .center)
.padding(.top,UIApplication.shared.windows.first?.safeAreaInsets.top ?? 40)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
}
}
You should apply the .edgesIgnoringSafeArea(.all) only to the rectangle, not to the ZStack. This way, only it should fill the screen, while everything else remains in place.
Then, to make it so that the FindView fills the screen (respecting safe area), you need make its frame extend with .frame(maxWidth: .infinity, maxHeight: .infinity)
Code sample:
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.fill(Color.red)
.edgesIgnoringSafeArea(.all)
Text("This should respect the safe area")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
}
}