I’m working on a project that let the user pick a data from a calendar view and the app will show the data for that day. I have made a collapsable calendar view that expand by default and the collapse as the user scroll up (The code below). But I want it to collapse at first by the default and then if the user want they can scroll down to expand the calendar. Any help will be appreciated. Thank you!!
import SwiftUI
struct Home: View {
@State private var selectedMonth: Date = .currentMonth
@State private var selectedDate: Date = .now
var safeArea: EdgeInsets
var body: some View {
let maxHeight = calendarHeight - (calendarTitleViewHeight + weekLabelHeight + safeArea.top + 50 + topPadding + bottomPadding - 50)
ScrollView(.vertical) {
VStack(spacing: 0) {
CalendarView()
VStack(spacing: 15) {
ForEach(1...15, id: \.self) { _ in
CardView()
}
}
.padding(15)
}
}
.scrollIndicators(.hidden)
.scrollTargetBehavior(CustomScrollBehaviour(maxHeight: maxHeight))
}
@ViewBuilder
func CardView() -> some View {
RoundedRectangle (cornerRadius: 15)
.fill(.blue.gradient)
.frame(height: 70)
.overlay(alignment: .leading) {
HStack(spacing: 12) {
Circle()
.frame(width: 40, height: 40)
VStack(alignment: .leading, spacing: 6, content: {
RoundedRectangle(cornerRadius: 5)
.frame(width: 100, height: 5)
RoundedRectangle(cornerRadius: 5)
.frame(width: 70, height: 5)
})
}
.foregroundStyle(.white.opacity(0.25))
.padding(15)
}
}
@ViewBuilder
func CalendarView() -> some View {
GeometryReader {
let size = $0.size
let minY = $0.frame(in: .scrollView(axis: .vertical)).minY
let maxHeight = size.height - (calendarTitleViewHeight + weekLabelHeight + safeArea.top + 50 + topPadding + bottomPadding - 50)
let progress = max(min((-minY / maxHeight), 1), 0)
VStack(alignment: .leading, spacing: 0, content: {
Text(currentMonth)
.font(.system(size: 35 - (10 * progress)))
.offset(y: -50 * progress)
.frame(maxHeight: .infinity, alignment: .bottom)
.overlay(alignment: .topLeading, content: {
GeometryReader {
let size = $0.size
Text(year)
.font(.system(size: 25 - (10 * progress)))
.offset(x: (size.width + 5) * progress, y: progress * 3)
}
})
.frame(maxWidth: .infinity, alignment: .leading)
.overlay(alignment: .topTrailing, content: {
HStack(spacing: 15) {
Button("", systemImage: "chevron.left"){
monthUpdate(false)
}
.contentShape(.rect)
Button("", systemImage: "chevron.right"){
monthUpdate(true)
}
.contentShape(.rect)
}
.font(.title3)
.foregroundStyle(.primary)
.offset(x: 150 * progress)
})
.frame(height: calendarTitleViewHeight)
VStack(spacing: 0) {
HStack(spacing: 0) {
ForEach(Calendar.current.weekdaySymbols, id: \.self) { symbol in
Text(symbol.prefix(3))
.font(.caption)
.frame(maxWidth: .infinity)
.foregroundStyle(.secondary)
}
}
.frame(height: weekLabelHeight, alignment: .bottom)
LazyVGrid(columns: Array(repeating: GridItem(spacing: 0), count: 7), spacing: 0, content: {
ForEach(selectedMonthDates) { day in
Text(day.shortSymbol)
.foregroundStyle(day.ignored ? .secondary : .primary)
.frame(maxWidth: .infinity)
.frame(height: 50)
.overlay(alignment: .bottom, content: {
Circle()
.fill(.white)
.frame(width: 5, height: 5)
.opacity(Calendar.current.isDate(day.date, inSameDayAs: selectedDate) ? 1 : 0)
.offset(y: progress * -2)
})
.contentShape(.rect)
.onTapGesture {
selectedDate = day.date
}
}
})
.frame(height: calendarGridHeight - ((calendarGridHeight - 50) * progress), alignment: .top)
.offset(y: (monthProgress * -50) * progress )
.contentShape(.rect)
.clipped()
}
.offset(y: progress * -50)
})
.foregroundStyle(.white)
.padding(.horizontal, horizontalPadding)
.padding(.top, topPadding)
.padding(.top, safeArea.top)
.padding(.bottom, bottomPadding)
.frame(maxHeight: .infinity)
.frame(height: size.height - (maxHeight * progress), alignment: .top)
.background(.red.gradient)
//Stick to the top
.clipped()
.contentShape(.rect)
.offset(y: -minY)
}
.frame(height: calendarHeight)
.zIndex(1000)
}
func format(_ format: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = format
return formatter.string(from: selectedMonth)
}
func monthUpdate(_ increment: Bool = true) {
let calendar = Calendar.current
guard let month = calendar.date(byAdding: .month, value: increment ? 1 : -1, to: selectedMonth) else { return }
guard let date = calendar.date(byAdding: .month, value: increment ? 1 : -1, to: selectedDate) else { return }
selectedMonth = month
selectedDate = date
}
var selectedMonthDates: [Day] {
return extractDates(selectedMonth)
}
var currentMonth: String {
return format("MMMM")
}
var monthProgress: CGFloat {
let calendar = Calendar.current
if let index = selectedMonthDates.firstIndex(where: { calendar.isDate($0.date, inSameDayAs: selectedDate)}) {
return CGFloat(index/7).rounded()
}
return 1.0
}
var year: String {
return format("YYYY")
}
var calendarHeight: CGFloat {
return calendarTitleViewHeight + weekLabelHeight + calendarGridHeight + safeArea.top + topPadding + bottomPadding
}
var calendarTitleViewHeight: CGFloat {
return 75.0
}
var weekLabelHeight: CGFloat {
return 30.0
}
var calendarGridHeight: CGFloat {
return CGFloat(selectedMonthDates.count / 7) * 50
}
var horizontalPadding: CGFloat {
return 15.0
}
var topPadding: CGFloat {
return 15.0
}
var bottomPadding: CGFloat {
return 5.0
}
}
#Preview {
ContentView()
}
struct CustomScrollBehaviour: ScrollTargetBehavior {
var maxHeight: CGFloat
func updateTarget(_ target: inout ScrollTarget, context: TargetContext) {
if target.rect.minY < maxHeight {
target.rect = .zero
}
}
}
extension Date {
static var currentMonth: Date {
let calendar = Calendar.current
guard let currentMonth = calendar.date(from: Calendar.current.dateComponents([.month, .year], from: .now)) else {
return .now
}
return currentMonth
}
}
extension View {
func extractDates(_ month: Date) -> [Day] {
var days: [Day] = []
let calendar = Calendar.current
let formatter = DateFormatter()
formatter.dateFormat = "dd"
guard let range = calendar.range(of: .day, in: .month, for: month)?.compactMap({ value -> Date? in
return calendar.date(byAdding: .day, value: value - 1, to: month)
}) else {
return days
}
let firstWeekDay = calendar.component(.weekday, from: range.first!)
for index in Array(0..<firstWeekDay-1).reversed() {
guard let date = calendar.date(byAdding: .day, value: -index - 1, to: range.first!) else { return days}
let shortSymbol = formatter.string(from: date)
days.append(.init(shortSymbol: shortSymbol, date: date, ignored: true))
}
range.forEach { date in
let shortSymbol = formatter.string(from: date)
days.append(.init(shortSymbol: shortSymbol, date: date))
}
let lastWeekDay = 7 - calendar.component(.weekday, from: range.last!)
if lastWeekDay > 0 {
for index in 0..<lastWeekDay {
guard let date = calendar.date(byAdding: .day, value: index + 1, to: range.last!) else { return days}
let shortSymbol = formatter.string(from: date)
days.append(.init(shortSymbol: shortSymbol, date: date, ignored: true))
}
}
return days
}
}
struct Day: Identifiable {
var id: UUID = .init()
var shortSymbol: String
var date: Date
var ignored: Bool = false
}
I have tried to user the .defaultScrollAnchor(.init(x: 0, y: maxHeight/totalHeight)) but I couldn’t figure out the totalHeight of the scrollView. Could you guys please tell me how to use this attribute and make it to work?