For each date category, there may be one or more categories, and for each category, there may be one or more financial transactions. These categories are represented in a circle form, similar to a bubble chart. However, I am experiencing issues with the CircleItem view as it crashes, and CategoryView is not functioning as intended. The goal is to have each category represented as a clickable circle, and to ensure that newly created circles do not overlap with existing ones. As this is my first app, I am unsure of how to proceed with resolving these issues.
// for each dateCategory there can be one ore more Category and for each Category there can be one or more FinancialTransaction
struct DateCategory {
let date: Date
var categories: [Category]
}
class Category: Identifiable {
let id = UUID()
var categoryName: String
var transactions: [FinancialTransaction]
var size: CGFloat
var offset = CGSize.zero
init(categoryName: String, transactions: [FinancialTransaction], size: CGFloat, offset: CGSize) {
self.categoryName = categoryName
self.transactions = transactions
self.size = size
self.offset = offset
}
}
struct FinancialTransaction: Identifiable {
let id = UUID()
var amount: Int
var name: String
var date: Date
init(amount: Int, name: String, date: Date) {
self.amount = amount
self.name = name
self.date = date
}
}
class CategoryEnvironment: ObservableObject {
@Published var categories: [Category] = []
}
class TransactionsEnvironment: ObservableObject {
@Published var transactions: [FinancialTransaction] = []
}
struct CategoryView: View {
@Namespace var namespace
@State var show = false
@EnvironmentObject var categories: CategoryEnvironment
@State var selectedCategory: Category?
@State var selectedId = UUID()
var body: so
me View {
ZStack {
NavigationView {
VStack{
//MARK: The catefories
if !show {
ForEach(categories.categories) { category in
CiercleItem(namespace: namespace, spacing: 0, startAngle: 180, clockwise: true)
.onTapGesture {
withAnimation(.spring(response: 1, dampingFraction: 1, blendDuration: 1)){
show.toggle()
selectedId = category.id
}
}
}
}
if show {
ForEach(categories.categories) { category in
if category.id == selectedId {
CiercleView(namespace: namespace, show: $show)
.zIndex(1)
.transition(.asymmetric(insertion: .opacity.animation(.easeInOut(duration: 0.1)), removal: .opacity.animation(.easeInOut(duration: 0.3).delay(0.3))))
}
}
}
}
.navigationBarItems(trailing:
NavigationLink(destination: NewCategoryView()) {
Image(systemName: "plus")
}
)
}
}
}
}
struct CiercleItem: View {
var namespace: Namespace.ID
@EnvironmentObject var categories: CategoryEnvironment
@State var selectedCategory: Category?
// Spacing between bubbles
var spacing: CGFloat
// startAngle in degrees -360 to 360 from left horizontal
var startAngle: Int
// direction
var clockwise: Bool
struct ViewSize {
var xMin: CGFloat = 0
var xMax: CGFloat = 0
var yMin: CGFloat = 0
var yMax: CGFloat = 0
}
@State private var mySize = ViewSize()
var body: some View {
let xSize = (mySize.xMax - mySize.xMin) == 0 ? 1 : (mySize.xMax - mySize.xMin)
let ySize = (mySize.yMax - mySize.yMin) == 0 ? 1 : (mySize.yMax - mySize.yMin)
GeometryReader { geometry in
let xScale = geometry.size.width / xSize
let yScale = geometry.size.height / ySize
let scale = min(xScale, yScale)
ZStack {
ForEach(categories.categories.indices, id: \.self) { index in
ZStack {
Circle()
.fill(Color.black)
.matchedGeometryEffect(id: "background", in: namespace)
.frame(width: CGFloat(self.categories.categories[index].size) * scale, height: CGFloat(self.categories.categories[index].size) * scale)
.overlay(
VStack{
Text("-50")
.foregroundColor(.white)
.font(.largeTitle)
.fontWeight(.light)
.padding(2)
Text(self.categories.categories[index].categoryName)
.foregroundColor(.white)
.font(.body)
.fontWeight(.light)
.onTapGesture {
self.selectedCategory = self.categories.categories[index]
}
}
)
}.offset(x: self.categories.categories[index].offset.width * scale, y: self.categories.categories[index].offset.height * scale)
}
}.offset(x: xOffset() * scale, y: yOffset() * scale)
} .onAppear {
setOffets()
mySize = absoluteSize()
}
}
// taken out of main for compiler complexity issue
func xOffset() -> CGFloat {
let size = categories.categories[0].size
let xOffset = mySize.xMin + size / 2
return -xOffset
}
func yOffset() -> CGFloat {
let size = categories.categories[0].size
let yOffset = mySize.yMin + size / 2
return -yOffset
}
// calculate and set the offsets
func setOffets() {
if categories.categories.isEmpty { return }
// first circle
categories.categories[0].offset = CGSize.zero
if categories.categories.count < 2 { return }
// second circle
let b = (categories.categories[0].size + categories.categories[1].size) / 2 + spacing
// start Angle
var alpha: CGFloat = CGFloat(startAngle) / 180 * CGFloat.pi
categories.categories[1].offset = CGSize(width: cos(alpha) * b,
height: sin(alpha) * b)
// other circles
for i in 2..<categories.categories.count {
// sides of the triangle from circle center points
let c = (categories.categories[0].size + categories.categories[i-1].size) / 2 + spacing
let b = (categories.categories[0].size + categories.categories[i].size) / 2 + spacing
let a = (categories.categories[i-1].size + categories.categories[i].size) / 2 + spacing
alpha += calculateAlpha(a, b, c) * (clockwise ? 1 : -1)
let x = cos(alpha) * b
let y = sin(alpha) * b
categories.categories[i].offset = CGSize(width: x, height: y )
}
}
// Calculate alpha from sides - 1. Cosine theorem
func calculateAlpha(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat) -> CGFloat {
return acos(
( pow(a, 2) - pow(b, 2) - pow(c, 2) )
/
( -2 * b * c ) )
}
// calculate max dimensions of offset view
func absoluteSize() -> ViewSize {
let radius = categories.categories[0].size / 2
let initialSize = ViewSize(xMin: -radius, xMax: radius, yMin: -radius, yMax: radius)
let maxSize = categories.categories.reduce(initialSize, { partialResult, item in
let xMin = min(
partialResult.xMin,
item.offset.width - item.size / 2 - spacing
)
let xMax = max(
partialResult.xMax,
item.offset.width + item.size / 2 + spacing
)
let yMin = min(
partialResult.yMin,
item.offset.height - item.size / 2 - spacing
)
let yMax = max(
partialResult.yMax,
item.offset.height + item.size / 2 + spacing
)
return ViewSize(xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax)
})
return maxSize
}
}
this is a lick of an image perhaps it will give you an idea what I am trying to make hear