SwiftUI and CoreData: Horizontal bar chart with different coloured values - swift

I am learning SwiftUI with CoreData and I have that one demo app where I am stuck.
Outcome:
I would like to present different coloured bar charts based on numeral values (calories). I managed to present numeral values with one colour, but not different colours based on value.
Here is image about desired outcome and my Swift code.
Thank you for any help! -Toni
Desired UI outcome
Code screenshot
import SwiftUI
struct ColourChart: View {
#Environment(\.managedObjectContext) var managedObjContext
#State private var name = ""
#State private var calories: Double = 0
#State private var color: Color = .gray
#FetchRequest(sortDescriptors: [SortDescriptor(\.calories)]) var food: FetchedResults<CaloriesEntity>
var body: some View {
List {
ForEach(food) { food in
VStack(alignment: .leading) {
HStack(alignment: .top) {
Text(food.name ?? "Unknown name")
.onAppear {
calories = food.calories
name = food.name!
}
Spacer()
Text(calcTimeSince(date:food.date!))
.foregroundColor(.gray)
.font(.caption)
.italic()
}///-HStack
ZStack {
Rectangle()
.foregroundColor(ColouredBars())
.frame(width: food.calories/3, height: 15, alignment: .trailing)
.cornerRadius(5)
Text("\(Int(food.calories))")
.foregroundColor(.white)
.font(.caption)
}///-ZStack
}///-VStack
}///ForEach Ends
}///-List
}///-View
func ColouredBars() -> Color {
let calories: Double = 299
if calories > 600 {
color = .red
} else if calories > 300 {
color = .yellow
} else {
color = .green
}
return color
}
}///-Struct
struct ColourChart_Previews: PreviewProvider {
static var previews: some View {
ColourChart()
}
}

You would want to add a calories parameter and change the return type to color. The code would look something like this:
func calorieColor(calorie: Double) -> Color {
if calorie >= 600 {
return .red
} else if calorie >= 300 {
return .yellow
} else {
return .green
}
return .clear
}
the .clear is there just to allow the function to return some type of color if the if statement isn't applicable but that shouldn't be the case.
You could also do func calorieColor(_ calorie: Double) -> Color { The _ allows you to make the code a little more concise by allowing you to just type calorieColor(150) vs calorieColor(Calorie: 150).

Related

SwiftUI - Dynamic LazyHGrid row height

I'm creating vertical layout which has scrollable horizontal LazyHGrid in it. The problem is that views in LazyHGrid can have different heights (primarly because of dynamic text lines) but the grid always calculates height of itself based on first element in grid:
What I want is changing size of that light red rectangle based on visible items, so when there are smaller items visible it should look like this:
and when there are bigger items it should look like this:
This is code which results in state on the first image:
struct TestView: PreviewProvider {
static var previews: some View {
ScrollView {
VStack {
Color.blue
.frame(height: 100)
ScrollView(.horizontal) {
LazyHGrid(
rows: [GridItem()],
alignment: .top,
spacing: 16
) {
Color.red
.frame(width: 64, height: 24)
ForEach(Array(0...10), id: \.self) { value in
Color.red
.frame(width: 64, height: CGFloat.random(in: 32...92))
}
}.padding()
}.background(Color.red.opacity(0.3))
Color.green
.frame(height: 100)
}
}
}
}
Something similar what I want can be achieved by this:
extension View {
func readSize(edgesIgnoringSafeArea: Edge.Set = [], onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
SwiftUI.Color.clear
.preference(key: ReadSizePreferenceKey.self, value: geometryProxy.size)
}.edgesIgnoringSafeArea(edgesIgnoringSafeArea)
)
.onPreferenceChange(ReadSizePreferenceKey.self) { size in
DispatchQueue.main.async { onChange(size) }
}
}
}
struct ReadSizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
struct Size: Equatable {
var height: CGFloat
var isValid: Bool
}
struct TestView: View {
#State private var sizes = [Int: Size]()
#State private var height: CGFloat = 32
static let values: [(Int, CGFloat)] =
(0...3).map { ($0, CGFloat(32)) }
+ (4...10).map { ($0, CGFloat(92)) }
var body: some View {
ScrollView {
VStack {
Color.blue
.frame(height: 100)
ScrollView(.horizontal) {
LazyHGrid(
rows: [GridItem(.fixed(height))],
alignment: .top,
spacing: 16
) {
ForEach(Array(Self.values), id: \.0) { value in
Color.red
.frame(width: 300, height: value.1)
.readSize { sizes[value.0]?.height = $0.height }
.onAppear {
if sizes[value.0] == nil {
sizes[value.0] = Size(height: .zero, isValid: true)
} else {
sizes[value.0]?.isValid = true
}
}
.onDisappear { sizes[value.0]?.isValid = false }
}
}.padding()
}.background(Color.red.opacity(0.3))
Color.green
.frame(height: 100)
}
}.onChange(of: sizes) { sizes in
height = sizes.filter { $0.1.isValid }.map { $0.1.height }.max() ?? 32
}
}
}
... but as you see its kind of laggy and a little bit complicated, isn't there better solution? Thank you everyone!
The height of a row in a LazyHGrid is driven by the height of the tallest cell. According to the example you provided, the data source will only show a smaller height if it has only a small size at the beginning.
Unless the first rendering will know that there are different heights, use the larger value as the height.
Is your expected UI behaviour that the height will automatically switch? Or use the highest height from the start.

How do I make conjoined buttons in SwiftUI? (MacOS)

There are buttons present in apps, such as TextEdit where the sides of adjacent buttons are joined together. Does anyone know how I could replicate this visual? I'm working in SwiftUI on MacOS, and answers for MacOS 10 & 11 would be appreciated.
I don’t think that’s possible in SwiftUI, unless you want to build it from scratch. What you’re seeing in TextEdit is an NSSegmentedControl that allows multiple selection: https://developer.apple.com/design/human-interface-guidelines/macos/selectors/segmented-controls/
In SwiftUI, segmented controls are made using Picker, which doesn’t allow multiple selection. Your best bet is to wrap NSSegmentedControl in an NSHostingView.
Replicating these buttons using SwiftUI is not all that difficult.
Here is a very basic approach, adjust the colors, corners etc... to your liking:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
TextFormats()
}
}
struct GrayButtonStyle: ButtonStyle {
let w: CGFloat
let h: CGFloat
init(w: CGFloat, h: CGFloat) {
self.w = w
self.h = h
}
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.foregroundColor(Color.white)
.frame(width: w, height: h)
.padding(5)
.background(Color(UIColor.systemGray4))
.overlay(Rectangle().strokeBorder(Color.gray, lineWidth: 1))
}
}
struct TextFormats: View {
let sx = CGFloat(20)
let color = Color.black
#State var bold = false
#State var italic = false
#State var underline = false
var body: some View {
HStack (spacing: 0) {
Group {
Button(action: { bold.toggle() }) {
Image(systemName: "bold").resizable().frame(width: sx, height: sx)
.foregroundColor(bold ? .blue : color)
}
Button(action: { italic.toggle() }) {
Image(systemName: "italic").resizable().frame(width: sx, height: sx)
.foregroundColor(italic ? .blue : color)
}
Button(action: { underline.toggle() }) {
Image(systemName: "underline").resizable().frame(width: sx, height: sx)
.foregroundColor(underline ? .blue : color)
}
}.buttonStyle(GrayButtonStyle(w: sx+5, h: sx+5))
}.padding(1)
.overlay(RoundedRectangle(cornerRadius: 5).strokeBorder(.white, lineWidth: 2))
.clipped(antialiased: true)
}
}

Align views in Picker

How do I align the Color views in a straight line with the text to the side?
To look like so (text aligned leading):
█  red
█  green
█  blue
Or this (text aligned center):
█    red
█  green
█   blue
Current code:
struct ContentView: View {
#State private var colorName: Colors = .red
var body: some View {
Picker("Select color", selection: $colorName) {
ForEach(Colors.allCases) { color in
HStack {
color.asColor.aspectRatio(contentMode: .fit)
Text(color.rawValue)
}
}
}
}
}
enum Colors: String, CaseIterable, Identifiable {
case red
case green
case blue
var id: String { rawValue }
var asColor: Color {
switch self {
case .red: return .red
case .green: return .green
case .blue: return .blue
}
}
}
Result (not aligned properly):
Without the Picker, I found it is possible to use alignmentGuide(_:computeValue:) to achieve the result. However, this needs to be in a Picker.
Attempt:
VStack(alignment: .custom) {
ForEach(Colors.allCases) { color in
HStack {
color.asColor.aspectRatio(contentMode: .fit)
.alignmentGuide(.custom) { d in
d[.leading]
}
Text(color.rawValue)
.frame(maxWidth: .infinity)
.fixedSize()
}
}
.frame(height: 50)
}
/* ... */
extension HorizontalAlignment {
struct CustomAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
return context[HorizontalAlignment.leading]
}
}
static let custom = HorizontalAlignment(CustomAlignment.self)
}
Result of attempt:
Possible solution is to use dynamic width for labels applied by max calculated one using view preferences.
Here is a demo. Tested with Xcode 13beta / iOS15
Note: the ViewWidthKey is taken from my other answer https://stackoverflow.com/a/63253241/12299030
struct ContentView: View {
#State private var colorName: Colors = .red
#State private var maxWidth = CGFloat.zero
var body: some View {
Picker("Select color", selection: $colorName) {
ForEach(Colors.allCases) { color in
HStack {
color.asColor.aspectRatio(contentMode: .fit)
Text(color.rawValue)
}
.background(GeometryReader {
Color.clear.preference(key: ViewWidthKey.self,
value: $0.frame(in: .local).size.width)
})
.onPreferenceChange(ViewWidthKey.self) {
self.maxWidth = max($0, maxWidth)
}
.frame(minWidth: maxWidth, alignment: .leading)
}
}
}
}
I think this should align your text and fix your issue.
struct ContentView: View {
#State private var colorName: Colors = .red
var body: some View {
Picker("Select color", selection: $colorName) {
ForEach(Colors.allCases) { color in
HStack() {
color.asColor.aspectRatio(contentMode: .fit)
Text(color.rawValue)
.frame(width: 100, height: 30, alignment: .leading)
}
}
}
}
}
A simple .frame modifier will fix these issues, try to frame your text together or use a Label if you don't want to complicate things when it comes to pickers or list views.
If you do go forward with this solution try to experiment with the width and height based on your requirements, and see if you want .leading, or .trailing in the alignment

SwiftUI onTapGesture interact with caller only

I've just started looking into SwiftUI and since it's so different I'm trying to wrap my head around basic concepts.
In this scenario, how would I go about changing the color only for the circle tapped?
ForEach(1...count, id: \.self) { _ in
Circle()
.foregroundColor(colors.randomElement()).opacity(0.2)
.frame(width: .random(in: 10...100), height: .random(in: 10...100))
.position(x: .random(in: self.stepperValue...400),
y: .random(in: self.stepperValue...400))
.saturation(2.0)
.onTapGesture(count: 1, perform: {
// Change color of circle that was tapped
print("Tapped")
})
.animation(.default) // Animate the change in position
}
You create a View that has the "Rows" individual properties
import SwiftUI
struct SampleColorChangeView: View {
//If the options are fixed no need to keep an eye on them
//You can move this down to the Row if you don't need to have them available here
let colors: [Color] = [.red,.blue,.gray, .yellow,.orange]
#State var count: Int = 10
var body: some View {
VStack{
ForEach(1...count, id: \.self) { _ in
RowView(colors: colors)
}
}
}
}
//Create a row View to observe individual objects
//You will do this with anything that you want to Observe independently
struct RowView: View {
let colors: [Color]
//#State observes changes so the View is updated
#State var color: Color = .blue
//This kind of works like the colors do you want one for each or a shared for all. Does the parent need access? You can move it up or keep it here
#State var stepperValue: CGFloat = 0
//The only change here is the reference to the individual Color
var body: some View {
Circle()
//You set the individual color here
.foregroundColor(color).opacity(0.2)
.frame(width: .random(in: 10...100), height: .random(in: 10...100))
.position(x: .random(in: self.stepperValue...400),
y: .random(in: self.stepperValue...400))
.saturation(2.0)
.onTapGesture(count: 1, perform: {
// Change color of circle that was tapped
color = colors.randomElement()!
print("Tapped")
})
.animation(.default) // Animate the change in position
//If you want to set a random color to start vs just having them all be the same Color you can do something like this
.onAppear(){
color = colors.randomElement()!
}
}
}
struct SampleColorChangeView_Previews: PreviewProvider {
static var previews: some View {
SampleColorChangeView()
}
}
Well there are two main options I see here
Make a custom view like
struct MyCircle: View {
#State var color: Color?
var body: some View {
Circle()
.foregroundColor(color)
.onTapGesture {
self.color = colors.randomElement()
}
}
}
and then integrate that or
Use a model for your color
struct MyView: View {
#State var colors = allColors.indices.compactMap { _ in allColors.randomElement() }
var body: some View {
ForEach(colors.indices) { index in
Circle()
.foregroundColor(colors[index])
.onTapGesture {
colors[index] = allColors.randomElement()
}
}
}
}
A state like this should preferably be in its own class which should be inserted as ObservedObject.

SwiftUI: Understanding .sheet / .fullScreenCover lifecycle when using constant vs #Binding initializers

I'm trying to understand how and when the .sheet and .fullScreenCover initializers are called. Below is a minimal reproducible example, where the first screen has 3 colored rectangles and the SecondView (shown via .fullScreenCover) has 1 rectangle that changes color based on the selected color from the first screen.
When the app first loads, the color is set to .gray.
If I tap on the green rectangle, SecondView presents with a gray rectangle. (ie. the color DIDN'T change correctly).
If I then dismiss the SecondView and tap on the red rectangle, the SecondView presents with a red rectangle. (ie. the color DID change correctly.)
So, I'm wondering why this set up does NOT work on the initial load, but does work on the 2nd/3rd try?
Note: I understand this can be solved by changing the 'let selectedColor' to a #Binding variable, that's not what I'm asking.
Code:
import SwiftUI
struct SegueTest: View {
#State var showSheet: Bool = false
#State var color: Color = .gray
var body: some View {
HStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.red)
.frame(width: 100, height: 100)
.onTapGesture {
color = .red
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.green)
.frame(width: 100, height: 100)
.onTapGesture {
color = .green
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.orange)
.frame(width: 100, height: 100)
.onTapGesture {
color = .orange
showSheet.toggle()
}
}
.fullScreenCover(isPresented: $showSheet, content: {
SecondView(selectedColor: color)
})
}
}
struct SecondView: View {
#Environment(\.presentationMode) var presentationMode
let selectedColor: Color // Should change to #Binding
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
RoundedRectangle(cornerRadius: 25)
.fill(selectedColor)
.frame(width: 300, height: 300)
}
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}
struct SegueTest_Previews: PreviewProvider {
static var previews: some View {
SegueTest()
}
}
See comments and print statements. Especially the red
import SwiftUI
struct SegueTest: View {
#State var showSheet: Bool = false{
didSet{
print("showSheet :: didSet")
}
willSet{
print("showSheet :: willSet")
}
}
#State var color: Color = .gray{
didSet{
print("color :: didSet :: \(color.description)")
}
willSet{
print("color :: willSet :: \(color.description)")
}
}
#State var refresh: Bool = false
init(){
print("SegueTest " + #function)
}
var body: some View {
print(#function)
return HStack {
//Just to see what happens when you recreate the View
//Text(refresh.description)
Text(color.description)
RoundedRectangle(cornerRadius: 25)
.fill(Color.red)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: red")
//Changing the color
color = .red
//Refreshed SegueTest reloads function
//refresh.toggle()
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.green)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: green")
//Changing the color
color = .green
showSheet.toggle()
}
RoundedRectangle(cornerRadius: 25)
.fill(Color.orange)
.frame(width: 100, height: 100)
.onTapGesture {
print("SegueTest :: onTapGesture :: orange")
//Changing the color
color = .orange
showSheet.toggle()
}
}
//This part is likely created when SegueTest is created and since a struct is immutable it keeps the original value
.fullScreenCover(isPresented: $showSheet, content: {
SecondView(selectedColor: color)
})
}
}
struct SecondView: View {
#Environment(\.presentationMode) var presentationMode
//struct is immutable
let selectedColor: Color // Should change to #Binding
init(selectedColor: Color){
print("SecondView " + #function)
self.selectedColor = selectedColor
print("SecondView :: struct :: selectedColor = \(self.selectedColor.description)" )
print("SecondView :: parameter :: selectedColor = \(selectedColor.description)" )
}
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
RoundedRectangle(cornerRadius: 25)
.fill(selectedColor)
.frame(width: 300, height: 300)
}
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
}
struct SegueTest_Previews: PreviewProvider {
static var previews: some View {
SegueTest()
}
}
The problem is that you are not using your #State color inside SegueView, which will not reload your view on State change. Just include your #State somewhere in the code, which will force it to rerender at then update the sheet, with the correct color.
RoundedRectangle(cornerRadius: 25)
.fill(color == .red ? Color.red : Color.red) //<< just use dump color variable here
If you do not include your color state somewhere in the code, it won't re render your SegueView, hence you still pass the old color gray to your SecondView.
Or even pass your colors as Binding your your Second View..
SecondView(selectedColor: $color)
#Binding var selectedColor: Color