Consider the following SwiftUI Charts view:
struct ChartView: View {
var data: [DataPoint]
var body: some View {
Chart {
ForEach(data) { point in
LineMark (
x: .value("x", point.x),
y: .value("y", point.y)
)
}
.interpolationMethod(.cardinal)
}
.chartYAxis {
AxisMarks(position: .leading)
}
.chartXAxisLabel(position: .bottom, alignment: .center) {
Text("x title")
}
.chartYAxisLabel(position: .leading, alignment: .center) {
Text("y title")
}
.padding(10)
}
}
This is the result:
Note the Y axis title is backwards. How can I flip the y axis title 180 degrees?
Related
So basically I need to come up with a layout that aligns the middle of a View to the bottom of other View in SwiftUI.
To make it more clear, all I need is something like this:
I guess the equivalent in a UIKit world would be:
redView.bottomAnchor.constraint(equalTo: whiteView.centerYAnchor)
Ive tried setting both in a ZStack and offsetting the white View but it won't work on all screen sizes for obvious reasons.
Any tips?
You can use .aligmentGuide (which is a tricky beast, I recommend this explanation)
Here is your solution, its independent of the child view sizes:
struct ContentView: View {
var body: some View {
ZStack(alignment: .bottom) { // subviews generally aligned at bottom
redView
whiteView
// center of this subview aligned to .bottom
.alignmentGuide(VerticalAlignment.bottom,
computeValue: { d in d[VerticalAlignment.center] })
}
.padding()
}
var redView: some View {
VStack {
Text("Red View")
}
.frame(maxWidth: .infinity)
.frame(height: 200)
.background(Color.red)
.cornerRadius(20)
}
var whiteView: some View {
VStack {
Text("White View")
}
.frame(maxWidth: .infinity)
.frame(width: 250, height: 100)
.background(Color.white)
.cornerRadius(20)
.overlay(RoundedRectangle(cornerRadius: 20).stroke())
}
}
You may try this:
ZStack {
Rectangle()
.frame(width: 300, height: 400)
.overlay(
GeometryReader { proxy in
let offsetY = proxy.frame(in: .named("back")).midY
Rectangle()
.fill(Color.red)
.offset(y: offsetY)
}
.frame(width: 150, height: 140)
, alignment: .center)
}
.coordinateSpace(name: "back")
Bascially the idea is to use coordinateSpace to get the frame of the bottom Rectangle and use geometryreader to get the offset needed by comparing the frame of top rectangle with the bottom one. Since we are using overlay and it is already aligned to the center horizontally, we just need to offset y to get the effect you want.
Based on OPs request in comment, this is a solution making use of a custom Layout.
The HalfOverlayLayout takes two subview and places the second half height over the first. The size of the first subview is flexible. As this is my first Layout I'm not sure if I covered all possible size variants, but it should be a start.
struct ContentView: View {
var body: some View {
HalfOverlayLayout {
redView
whiteView
}
.padding()
}
var redView: some View {
VStack {
ForEach(0..<20) { _ in
Text("Red View")
}
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.cornerRadius(20)
}
var whiteView: some View {
VStack {
Text("White View")
}
.frame(width: 200, height: 150)
.background(Color.white)
.cornerRadius(20)
.overlay(RoundedRectangle(cornerRadius: 20).stroke())
}
}
struct HalfOverlayLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let heightContent = subviews.first?.sizeThatFits(.unspecified).height ?? 0
let heightFooter = subviews.last?.sizeThatFits(.unspecified).height ?? 0
let totalHeight = heightContent + heightFooter / 2
let maxsizes = subviews.map { $0.sizeThatFits(.infinity) }
var totalWidth = maxsizes.max {$0.width < $1.width}?.width ?? 0
if let proposedWidth = proposal.width {
if totalWidth > proposedWidth { totalWidth = proposedWidth }
}
return CGSize(width: totalWidth, height: totalHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let heightFooter = subviews.last?.sizeThatFits(.unspecified).height ?? 0
let maxHeightContent = bounds.height - heightFooter / 2
var pt = CGPoint(x: bounds.midX, y: bounds.minY)
if let first = subviews.first {
var totalWidth = first.sizeThatFits(.infinity).width
if let proposedWidth = proposal.width {
if totalWidth > proposedWidth { totalWidth = proposedWidth }
}
first.place(at: pt, anchor: .top, proposal: .init(width: totalWidth, height: maxHeightContent))
}
pt = CGPoint(x: bounds.midX, y: bounds.maxY)
if let last = subviews.last {
last.place(at: pt, anchor: .bottom, proposal: .unspecified)
}
}
}
I'm trying out Swifts Charts. I am trying to align the annotation to the top of the chart, but I am not able to do it.
What I am trying to achieve is something like this:
But this is done with HStack on top of the chart, and it does not align 100%. So instead I am using the annotations modifier. The result is this:
But as you can see, the values are directly on top of the bar, which is not the result I am looking for.
The code used for the annotations is here:
.annotation {
Text("\(Int(item.price))")
.font(.system(size: 8))
.rotationEffect(.degrees(270))
.padding()
}
I have tried to add in a VStack, put in the Text and a Spacer underneath, but that did not help. I also tried with:
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
But that did not help either.
Hope you guys can help me you here :) Thanks in advance.
You can use .annotation(position: .overlay, alignment: .bottom) which gives you an annotation overlay over the mark that starts at the bottom of the mark.
Here is an example:
struct ContentView: View {
let data: [(Int, Int)] = {
(0...20).map { ($0, Int.random(in: 0...500)) }
}()
#State private var selectedX = 6
var body: some View {
VStack {
Chart {
ForEach(data.indices, id: \.self) { i in
let (x,y) = data[i]
BarMark(
x: .value("x", x),
y: .value("y", y)
)
.annotation(position: .overlay, alignment: .bottom, spacing: 0) {
Text("\(y)")
.font(.system(size: 8))
.frame(width: 570 , alignment: .trailing)
.rotationEffect(.degrees(-90))
}
}
}
.chartXScale(domain: 0...20)
.chartYScale(domain: 0...550)
.chartYAxis(.hidden)
.frame(height: 300)
}
.padding()
}
}
I'm trying to add axis unit labels to a chart built with Swift Charts.
For example:
import SwiftUI
import Charts
struct ContentView: View {
struct ChartData: Identifiable {
var id: Double { xVal }
let xVal: Double
let yVal: Double
}
let data: [ChartData] = [
.init(xVal: -5, yVal: 5),
.init(xVal: 0, yVal: 10),
.init(xVal: 5, yVal: 20),
]
var body: some View {
Chart(data) {
LineMark(
x: .value("Position", $0.xVal),
y: .value("Height", $0.yVal)
)
}
.chartYAxis {
AxisMarks(position: .leading)
}
}
}
Chart output, with the desired labels annotated in red:
How can we create axis labels like those in red above - or otherwise add units/descriptions to our axes?
Edit: I finally found it, see my answer below.
This is the correct modifier:
Chart {
...
}
.chartXAxisLabel("Position (meters)")
You can also provide a custom view like this:
Chart {
...
}
.chartXAxisLabel(position: .bottom, alignment: .center) {
Text("Position (meters)")
}
One way to create those red labels is to use a combination of Text with VStack or HStack as needed. The text can be rotated using rotationEffect(). This is the original code with modifications:
struct ContentView: View {
struct ChartData: Identifiable {
var id: Double { xVal }
let xVal: Double
let yVal: Double
}
let data: [ChartData] = [
.init(xVal: -5, yVal: 5),
.init(xVal: 0, yVal: 10),
.init(xVal: 5, yVal: 20),
]
var body: some View {
HStack(alignment: .center, spacing: 0) {
Text("Height(cm)")
.rotationEffect(Angle(degrees: -90))
.foregroundColor(.red)
VStack {
Chart(data) {
LineMark(
x: .value("Position", $0.xVal),
y: .value("Height", $0.yVal)
)
}
.chartYAxis {
AxisMarks(position: .leading)
}
Text("Position(meters)")
.foregroundColor(.red)
}
}
.frame(width: 350, height: 400, alignment: .center)
}
}
This is the resulting layout:
Use AxisMarks() to change the default axis marks including labels, grid lines and tick marks. Within AxisMarks individual axis labels can be assigned using AxisValueLabel(). Text() is formatted as an individual label for AxisValueLabel()'s content.
When AxisMarks is used, the default grid lines and tick marks disappear, so they have been added back using AxisGridLine() and AxisTick(). The following code should be added immediately following Chart{} and replacing the original .chartYAxis():
.chartXAxis {
AxisMarks(position: .bottom, values: .automatic) { value in
AxisGridLine(centered: true, stroke: StrokeStyle(dash: [1, 2]))
AxisTick(centered: true, stroke: StrokeStyle(dash: [1, 2]))
AxisValueLabel() {
if let intValue = value.as(Int.self) {
Text("\(intValue) m")
.font(.system(size: 10))
}
}
}
}
.chartYAxis {
AxisMarks(position: .leading, values: .automatic) { value in
AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1))
AxisValueLabel() {
if let intValue = value.as(Int.self) {
Text("\(intValue) cm")
.font(.system(size: 10))
}
}
}
}
The chart with custom labels will look like this:
I designed a SwiftUI view which is a scrollview. Now I need to add a vertical swipe gesture to it which shall take it to a different view. I tried to do it using the tabView and adding a rotating effect of -90 degrees to it. But that rotates my original view too and that's not what I want. I couldn't find any relevant help in SwiftUI which deals with swiping up a scrollview to a new view.
Here's my code..
the vertical swipe I achieved using this. But my view get rotated. Setting other angles disappears the view somehow. I am new to SwiftUI, I am stuck on it for a week now.1
GeometryReader { proxy in
TabView {
ScrollView {
VStack(alignment: .center) {
ZStack(alignment: .leading) {
Image("Asset 13").resizable().frame(width: percentWidth(percentage: 100), height: percentHeight(percentage: 50), alignment: .top)
HStack {
Spacer()
Image("Asset 1")//.padding(.bottom, 130)
Spacer()
}.padding(.bottom, 150)
HStack {
VStack(spacing:2) {
Text("followers").foregroundColor(.white).padding(.leading, 20)
HStack {
Image("Asset 3")
Text("10.5k").foregroundColor(.white)
}
}
Spacer()
VStack {
Image("Asset 10").padding(.trailing)
Text("300K Review ").foregroundColor(.white)
}
}.background(Image("Asset 2").resizable().frame(width: percentWidth(percentage: 100), height: percentHeight(percentage: 6), alignment: .leading))
.padding(.top, 410)
HStack {
Spacer()
Image("Asset 14").resizable().frame(width: percentWidth(percentage: 50), height: percentHeight(percentage: 25), alignment: .center)
Spacer()
}.padding(.top, 390)
}
VStack(spacing: 4) {
Text("Karuna Ahuja | Yoga Instructor").font(Font.custom(FontName.bold, size: 22))
Text("12 Years of Experience with Bhartiya Yog Sansthan").tracking(-1).font(Font.custom(FontName.light, size: 16)).opacity(0.4)
}
Divider()
HStack {
ZStack {
Image("Asset 6").resizable().frame(width: percentWidth(percentage: 30), height: percentHeight(percentage: 12), alignment: .center)
VStack {
Image("Asset 5").resizable().frame(width: percentWidth(percentage: 8), height: percentHeight(percentage: 4), alignment: .center)
Text("245").font(Font.custom(FontName.bold, size: 16))
Text("Video").font(Font.custom(FontName.medium, size: 16)).opacity(0.5)
}
}
ZStack {
Image("Asset 6").resizable().frame(width: percentWidth(percentage: 30), height: percentHeight(percentage: 12), alignment: .center)
VStack {
Image("Asset 7").resizable().frame(width: percentWidth(percentage: 8), height: percentHeight(percentage: 4), alignment: .center)
Text("45").font(Font.custom(FontName.bold, size: 16))
Text("Live Class").font(Font.custom(FontName.medium, size: 16)).opacity(0.5)
}
}
ZStack {
Image("Asset 6").resizable().frame(width: percentWidth(percentage: 30), height: percentHeight(percentage: 12), alignment: .center)
VStack {
Image("Asset 9").resizable().frame(width: percentWidth(percentage: 8), height: percentHeight(percentage: 4), alignment: .center)
Text("245").font(Font.custom(FontName.bold, size: 16))
Text("Sessions").font(Font.custom(FontName.medium, size: 16)).opacity(0.5)
}
}
}
Divider()
Text("Shine bright so that your light leads other. I'm a fitness junkie, high-energy yoga instructor. Let's make fitness FUN!").font(Font.custom(FontName.normal, size: 16)).tracking(-1).opacity(0.7).padding([.leading,.trailing], 6)
VideoPlayer(player: AVPlayer(url: videoUrl))
.frame(height: 320)
Spacer()
}.gesture(DragGesture(minimumDistance: 20, coordinateSpace: .global)
.onEnded { value in
let horizontalAmount = value.translation.width as CGFloat
let verticalAmount = value.translation.height as CGFloat
if abs(horizontalAmount) > abs(verticalAmount) {
print(horizontalAmount < 0 ? "left swipe" : "right swipe")
} else {
print(verticalAmount < 0 ? "up swipe" : "down swipe")
}
})
}.edgesIgnoringSafeArea(.all)
.ignoresSafeArea()
Text("this")
Text("this")
Text("this")
// ForEach(colors, id: \.self) { color in
// color // Your cell content
// }
// .rotationEffect(.degrees(-90)) // Rotate content
// .frame(
// width: proxy.size.width,
// height: proxy.size.height
// )
}
.frame(
width: proxy.size.height, // Height & width swap
height: proxy.size.width
)
.rotationEffect(.degrees(90), anchor: .topLeading) // Rotate TabView
.offset(x: proxy.size.width) // Offset back into screens bounds
.tabViewStyle(
PageTabViewStyle(indexDisplayMode: .never)
)
}
The only pure SwiftUI way I see is to do your own ScrollView implementation, which is not too complicated. This example has two views on top of each other. If you drag the first view further up than to the middle of the screen, it swipes away to reveal the second view.
struct ContentView: View {
#State private var offset = CGFloat.zero
#State private var dragOffset = CGFloat.zero
#State private var viewHeight = CGFloat.zero
var body: some View {
GeometryReader { fullgeo in
ZStack(alignment: .top) {
SecondView()
// necessary for second view to resize individually
.frame(height: fullgeo.size.height)
ScrollingView()
.overlay( GeometryReader { geo in Color.clear.onAppear { viewHeight = geo.size.height }})
.offset(y: offset + dragOffset)
.gesture(DragGesture()
.onChanged { value in
dragOffset = value.translation.height
}
.onEnded { value in
withAnimation(.easeOut) {
dragOffset = .zero
offset += value.predictedEndTranslation.height
// if bottom dragged higher than 50% of screen > second view
if offset < -(viewHeight - fullgeo.size.height/2) {
dragOffset = -viewHeight
return
}
// else constrain to top / bottom of ScrollingView
offset = max(min(offset, 0), -(viewHeight - fullgeo.size.height))
}
}
)
}
}
}
}
struct ScrollingView: View {
var body: some View {
VStack {
Text("View Top").font(.headline)
ForEach(0..<10) { _ in
Text("Content")
.frame(width: 200, height: 100)
.background(.white)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.gray)
}
}
struct SecondView: View {
var body: some View {
Text("View Bottom").font(.headline)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.orange)
}
}
Use this code:
import SwiftUI
struct PullToRefreshView: View
{
private static let minRefreshTimeInterval = TimeInterval(0.2)
private static let triggerHeight = CGFloat(100)
private static let indicatorHeight = CGFloat(100)
private static let fullHeight = triggerHeight + indicatorHeight
let backgroundColor: Color
let foregroundColor: Color
let isEnabled: Bool
let onRefresh: () -> Void
#State private var isRefreshIndicatorVisible = false
#State private var refreshStartTime: Date? = nil
init(bg: Color = .neutral0, fg: Color = .neutral90, isEnabled: Bool = true, onRefresh: #escaping () -> Void)
{
self.backgroundColor = bg
self.foregroundColor = fg
self.isEnabled = isEnabled
self.onRefresh = onRefresh
}
var body: some View
{
VStack(spacing: 0)
{
LazyVStack(spacing: 0)
{
Color.clear
.frame(height: Self.triggerHeight)
.onAppear
{
if isEnabled
{
withAnimation
{
isRefreshIndicatorVisible = true
}
refreshStartTime = Date()
}
}
.onDisappear
{
if isEnabled, isRefreshIndicatorVisible, let diff = refreshStartTime?.distance(to: Date()), diff > Self.minRefreshTimeInterval
{
onRefresh()
}
withAnimation
{
isRefreshIndicatorVisible = false
}
refreshStartTime = nil
}
}
.frame(height: Self.triggerHeight)
indicator
.frame(height: Self.indicatorHeight)
}
.background(backgroundColor)
.ignoresSafeArea(edges: .all)
.frame(height: Self.fullHeight)
.padding(.top, -Self.fullHeight)
}
private var indicator: some View
{
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: foregroundColor))
.opacity(isRefreshIndicatorVisible ? 1 : 0)
}
}
sample:
ScrollView{
VStack(spacing:0) {
//top of scrollView
PullToRefreshView{
//todo
}
}
}
I created an excel-like view, using a multi-directional scroll view. Now I want to pin the headers, not only the column headers but the row headers as well. Look at the following gif:
Code I used to create this view:
ScrollView([.vertical, .horizontal]){
VStack(spacing: 0){
ForEach(0..<model.rows.count+1, id: \.self) {rowIndex in
HStack(spacing: 0) {
ForEach(0..<model.columns.count+1) { columnIndex in
if rowIndex == 0 && columnIndex == 0 {
Rectangle()
.fill(Color(UIColor(Color.white).withAlphaComponent(0.0)))
.frame(width: CGFloat(200).pixelsToPoints(), height: CGFloat(100).pixelsToPoints())
.padding([.leading, .trailing])
.border(width: 1, edges: [.bottom, .trailing], color: .blue)
} else if (rowIndex == 0 && columnIndex > 0) {
TitleText(
label: model.columns[columnIndex - 1].label,
columnWidth: CGFloat(columnWidth).pixelsToPoints(),
borderEgdes: [.top, .trailing, .bottom]
)
} else if (rowIndex > 0 && columnIndex == 0) {
TitleText(
label: model.rows[rowIndex - 1].label,
columnWidth: CGFloat(columnWidth).pixelsToPoints(),
borderEgdes: [.trailing, .bottom, .leading]
)
} else if (rowIndex > 0){
//text boxes
let column = model.columns[columnIndex - 1]
switch column.type {
case "Text":
MatrixTextField(keyboardType: .default)
case "Number":
MatrixTextField(keyboardType: .decimalPad)
case "RadioButton":
RadioButton()
case "Checkbox":
MatrixCheckbox()
default:
MatrixTextField(keyboardType: .default)
}
}
}
}
}
}
}
.frame(maxHeight: 500)
Is it possible to pin both column and row headers here?
It's important I use VStack and HStack only instead of LazyVStack and LazyHStack, as I need smoothness while scrolling, when I use Lazy stacks, it jitters a lot for obvious reasons. So cannot really use section headers here.
What other approach could I follow?
It was a little more complex than expected. You have to use .preferenceKey to align all three ScollViews. Here is a working example:
struct ContentView: View {
let columns = 20
let rows = 30
#State private var offset = CGPoint.zero
var body: some View {
HStack(alignment: .top, spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
// empty corner
Color.clear.frame(width: 70, height: 50)
ScrollView([.vertical]) {
rowsHeader
.offset(y: offset.y)
}
.disabled(true)
}
VStack(alignment: .leading, spacing: 0) {
ScrollView([.horizontal]) {
colsHeader
.offset(x: offset.x)
}
.disabled(true)
table
.coordinateSpace(name: "scroll")
}
}
.padding()
}
var colsHeader: some View {
HStack(alignment: .top, spacing: 0) {
ForEach(0..<columns) { col in
Text("COL \(col)")
.foregroundColor(.secondary)
.font(.caption)
.frame(width: 70, height: 50)
.border(Color.blue)
}
}
}
var rowsHeader: some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(0..<rows) { row in
Text("ROW \(row)")
.foregroundColor(.secondary)
.font(.caption)
.frame(width: 70, height: 50)
.border(Color.blue)
}
}
}
var table: some View {
ScrollView([.vertical, .horizontal]) {
VStack(alignment: .leading, spacing: 0) {
ForEach(0..<rows) { row in
HStack(alignment: .top, spacing: 0) {
ForEach(0..<columns) { col in
// Cell
Text("(\(row), \(col))")
.frame(width: 70, height: 50)
.border(Color.blue)
.id("\(row)_\(col)")
}
}
}
}
.background( GeometryReader { geo in
Color.clear
.preference(key: ViewOffsetKey.self, value: geo.frame(in: .named("scroll")).origin)
})
.onPreferenceChange(ViewOffsetKey.self) { value in
print("offset >> \(value)")
offset = value
}
}
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGPoint
static var defaultValue = CGPoint.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value.x += nextValue().x
value.y += nextValue().y
}
}