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)
Related
I have problem because on appear modifier doesn't call every time when I enter the screen and it calls randomly (when on appear is applied to navigation view). When I put it on some view inside navigation view it never calls. What is the problem, I can't solve it, this is very strange behaviour. This is the view:
struct CheckListView: View {
#EnvironmentObject var appState: AppState
#StateObject var checkListViewModel = CheckListViewModel()
//#State var didSelectCreateNewList = false
#State var didSelectShareList = false
var body: some View {
NavigationView {
GeometryReader { reader in
VStack {
Spacer()
.frame(height: 30)
Text("\(appState.hikingTypeModel.name)/\(appState.lengthOfStayType.name)")
.foregroundColor(Color("rectBackground"))
.multilineTextAlignment(.center)
.font(.largeTitle)
Spacer()
.frame(height: 40)
List {
ForEach(checkListViewModel.checkListModel.sections) { section in
CheckListSection(model: section)
.listRowBackground(Color("background"))
}
}
.listStyle(.plain)
.clearListBackground()
.clipped()
Spacer()
.frame(height: 40)
HStack {
FilledRectangleBorderButtonView(titleLabel: "Share", backgroundColor: Color("rectBackground"),foregroundColor: .white, height: 55, didActionOnButtonHappened: $didSelectShareList)
Spacer()
FilledRectangleBorderButtonView(titleLabel: "Create new list", backgroundColor: .clear, foregroundColor: Color("rectBackground"), height: 55, didActionOnButtonHappened: $checkListViewModel.didSelectNewList)
.onChange(of: checkListViewModel.didSelectNewList) { _ in
UserDefaultsHelper().emptyUserDefaults()
appState.moveToRootView = .createNewList
}
}
.padding([.leading, .trailing], 20)
.frame(width: reader.size.width)
Spacer()
}
.frame(width: reader.size.width, height: reader.size.height)
.background(Color("background"))
.navigationBarStyle()
}
}
.onAppear {
self.checkListViewModel.loadJSON(hikingTypeId: appState.hikingTypeModel.id, lengthStayId: appState.lengthOfStayType.id)
}
}
}
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 have a VStack which has some HStack as you can see in my codes, inside my each Hstack there is an Image and Text, after running my codes the Alignmet of all codes is ugly, I want the Image alignment center together and Text alignment leading. How I can solve the problem?
I can make all Image .center Alignment, and also all Text .leading Alignment. But I can not make both happen at same time.
struct CustomAlignment: AlignmentID
{
static func defaultValue(in context: ViewDimensions) -> CGFloat
{
return context[HorizontalAlignment.center]
}
}
struct CustomAlignment2: AlignmentID
{
static func defaultValue(in context: ViewDimensions) -> CGFloat
{
return context[HorizontalAlignment.leading]
}
}
extension HorizontalAlignment
{
static let custom: HorizontalAlignment = HorizontalAlignment(CustomAlignment.self)
static let custom2: HorizontalAlignment = HorizontalAlignment(CustomAlignment2.self)
}
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(alignment: .custom)
{
HStack()
{
Image(systemName: "folder")
.alignmentGuide(.custom) { $0[HorizontalAlignment.center] }
Text("Some texts here.")
.alignmentGuide(.custom2) { $0[HorizontalAlignment.leading] }
Spacer()
}
HStack()
{
Image(systemName: "music.note")
.alignmentGuide(.custom) { $0[HorizontalAlignment.center] }
Text("Some texts here.")
.alignmentGuide(.custom2) { $0[HorizontalAlignment.leading] }
Spacer()
}
HStack()
{
Image(systemName: "person.fill.questionmark")
.alignmentGuide(.custom) { $0[HorizontalAlignment.center] }
Text("Some texts here.")
.alignmentGuide(.custom2) { $0[HorizontalAlignment.leading] }
Spacer()
}
}
.padding()
Spacer()
}
}
Use custom alignment guide if you want precise control.
About your comment on using fixed frame, here is an article which explains how frame works in SwiftUI.
Basically, frame modifier just adds a fixed size frame around the SF in this case, but it won't alter the intrinsic size.
struct ContentView: View {
var body: some View {
VStack(alignment: .sfView) {
SFView(title: "This is some text", image: "folder")
SFView(title: "SwiftUI is cool. Combine is cooler.", image: "snow")
SFView(title: "This is a music note. This has a different length.", image: "music.note")
}
}
}
private struct SFView: View {
let title: String
let image: String
var body: some View {
HStack(spacing: 8) {
Image(systemName: image)
.font(.system(size: 20))
.frame(width: 32, height: 32)
.alignmentGuide(.sfView) { d in d[HorizontalAlignment.center] }
Text(title)
.alignmentGuide(.sfView) { d in d[HorizontalAlignment.leading] }
}
}
}
private extension HorizontalAlignment {
struct SFViewAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.leading]
}
}
static let sfView = HorizontalAlignment(SFViewAlignment.self)
}
You have to give a frame to the image as some SF Symbols are larger than others, also try to create reusable views.
try something like this:
struct ContentView: View {
var body: some View {
VStack {
RowView(title: "Some texts here.", image: "folder")
RowView(title: "Some texts here.", image: "person.fill.questionmark")
RowView(title: "Some texts here.", image: "snow")
RowView(title: "Some texts here.", image: "forward.end.alt.fill")
}
.padding()
Spacer()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct RowView: View {
let title: String
let image: String
var body: some View {
// Option 1 with Label
// Label(
// title: {
// Text(title)
// },
// icon: {
// Image(systemName: image)
// .frame(width: 30, height: 30)
// }
// )
// Option 2 with HStack
HStack {
Image(systemName: image)
.frame(width: 30, height: 30)
Text(title)
Spacer()
}
}
}
You are WAY overcomplicating it. You don't need to use all these custom alignments and constraints for something this simple.
Go back to basics and use a regular VStack / HStack and just set the icon to be an exact frame. The issue was arising because the icons had slightly different widths.
struct TestView: View {
var body: some View {
VStack(alignment: .leading) {
HStack {
Image(systemName: "folder")
.frame(width: 30, height: 30, alignment: .center)
Text("Some text here")
}
HStack {
Image(systemName: "person.fill.questionmark")
.frame(width: 30, height: 30, alignment: .center)
Text("Some text here")
}
HStack {
Image(systemName: "snow")
.frame(width: 30, height: 30, alignment: .center)
Text("Some text here")
}
HStack {
Image(systemName: "music.note")
.frame(width: 30, height: 30, alignment: .center)
Text("Some text here")
}
}
}
}
Forgive me as I am extremely novice with SwiftUI... I am attempting to pull data from the CMS and put it in my app however it is throwing three errors on each attempt for the data to be retrieved and placed...
The errors are highlighting in the sections that read "api.beers.title", "api.beers.type" and "api.beers.description".
Errors
Value of type 'API' has no dynamic member 'beers' using key path from root type 'API'
Referencing subscript 'subscript(dynamicMember:)' requires wrapper 'ObservedObject.Wrapper'
Initializer 'init(_:)' requires that 'Binding' conform to 'StringProtocol'
API Call Code
func getArray(id: String, completion: #escaping([Entry]) -> ()) {
let query = Query.where(contentTypeId: id)
client.fetchArray(of: Entry.self, matching: query) { result in
switch result {
case .success(let array):
DispatchQueue.main.async {
completion(array.items)
}
case .failure(let error):
print(error)
}
}
}
class API: ObservableObject {
#Published var draft: [Draft] = draftData
init() {
getArray(id: "beers") { (items) in
items.forEach { (item) in
self.draft.append(Draft(
title: item.fields["title"] as! String,
type: item.fields["type"] as! String,
description: item.fields["type"] as! String
))
}
}
}
}
struct LandingPageView: View {
var body: some View {
VStack {
VStack {
Text("Problem Solved")
Text("Brewing Company")
}
.font(.system(size: 24, weight: .bold))
.multilineTextAlignment(.center)
.foregroundColor(Color("TextColor"))
VStack {
Text("NEWS & EVENTS")
.font(.title)
.fontWeight(.bold)
.padding(.top, 40)
.foregroundColor(Color("TextColor"))
NewsTile()
Text("On Draft" .uppercased())
.font(.title)
.fontWeight(.bold)
.padding(.top)
.foregroundColor(Color("TextColor"))
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
ForEach(draftData) { item in
GeometryReader { geometry in
DraftList(beer: item)
.rotation3DEffect(Angle(degrees: Double(geometry.frame(in: .global).minX - 30) / -20), axis: (x: 0, y: 10.0, z: 0))
}
.frame(width: 275, height: 200)
}
}
.padding(.leading, 30)
.padding(.trailing, 30)
}
}
.frame(width: 400, height: 850)
.background(Color("PageBackground"))
.edgesIgnoringSafeArea(.all)
}
}
}
struct DraftList: View {
var width: CGFloat = 275
var height: CGFloat = 200
#ObservedObject var api = API()
var beer: Draft
var body: some View {
VStack {
Spacer()
Text(api.beers.title)
.font(.system(size: 24, weight: .bold))
.padding(.horizontal, 20)
.frame(width: 275, alignment: .leading)
.foregroundColor(Color("TextColor"))
Text(api.beers.type .uppercased())
.font(.system(size: 14, weight: .bold))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
Text(api.beers.description)
.font(.system(size: 12))
.padding(.horizontal, 20)
.padding(.top, 10)
Spacer()
HStack {
// Add OnTapGesture to bring to full view + cart options.
Text("Click To Add To Cart")
.font(.footnote)
Image(systemName: "cart")
}
.padding(.bottom)
}
.padding(.horizontal, 20)
.frame(width: width, height: height)
.background(Color("TileOrangeColor"))
.cornerRadius(15)
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
}
}
Your draft is array, you can access by index like Text(api.draft[0].title)
#Published var draft: [Draft] = draftData instead #Published var draft: [Draft] = []
Updated:
class API: ObservableObject {
#Published var draft: [Draft] = []
init() {
getArray(id: "beers") { (items) in
items.forEach { (item) in
self.draft.append(Draft(
title: item.fields["title"] as! String,
type: item.fields["type"] as! String,
description: item.fields["type"] as! String
))
}
}
}
}
struct DraftList: View {
var width: CGFloat = 275
var height: CGFloat = 200
#ObservedObject var api = API()
var body: some View {
VStack {
ForEach(api.draft) {item in
Spacer()
Text(item.title)
.font(.system(size: 24, weight: .bold))
.padding(.horizontal, 20)
.frame(width: 275, alignment: .leading)
.foregroundColor(Color("TextColor"))
...
}
}
.padding(.horizontal, 20)
.frame(width: width, height: height)
.background(Color("TileOrangeColor"))
.cornerRadius(15)
.shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
}
}
How can I insert the preview of a widget into the app?
Can anyone give me some advice?
I did a naive version of a Widget configuration preview using SwiftUI. I used a RoundedRectangle with a small 3D rotation and applied a shadow:
I contained the code inside a custom View called WidgetView:
struct WidgetView<Content: View>: View {
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 24, style: .continuous)
.fill(Color(.systemBackground))
content
}
.frame(width: 162, height: 162)
.rotation3DEffect(.degrees(10), axis: (x: 1, y: 1, z: 0))
.shadow(color: Color(UIColor.systemGray), radius: 20, x: -5, y: 10)
}
}
The usage is quite simple. Just add the content view you want inside the Widget view:
WidgetView {
HealthActiveEnergyView(entry: ActiveEnergyEntry.mock())
}
.preferredColorScheme(.light)
.previewLayout(layout)
struct HealthActiveEnergyView: View {
var entry: ActiveEnergyEntry
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(NSLocalizedString("Active", comment: ""))
.foregroundColor(entry.configuration.accentColor)
Spacer()
Image(systemName: "flame.fill")
.font(.system(.title))
.foregroundColor(entry.configuration.accentColor)
}
Text(dateFormatter.string(from: entry.date))
.foregroundColor(Color(.systemGray))
.font(.caption)
HStack(alignment: .lastTextBaseline) {
Text("\(entry.totalOfKilocalories)")
.font(.system(.largeTitle, design: .rounded))
.bold()
.foregroundColor(Color(.label))
Text("kcal")
.foregroundColor(Color(.systemGray))
Spacer()
}
Spacer()
Text(NSLocalizedString("AVG 7 DAYS", comment: ""))
.foregroundColor(Color(.systemGray))
.font(.caption)
HStack(alignment: .lastTextBaseline) {
Text("\(entry.avgOfKilocalories)")
.font(.system(.callout, design: .rounded))
.bold()
.foregroundColor(Color(.label))
Text("kcal")
.foregroundColor(Color(.systemGray))
.font(.caption)
Spacer()
}
}
.padding()
}
}