构建一个日历,以反转和点击来控制。
首先,日历中每个日期作为一个独立的控件,应当由外部传入。所以,日历的日期控件使用@ViewBuilder进行声明。
struct CalenderView<DateView>: View where DateView:View
{
@Environment(\.calendar) var calendar
//attribute
@Binding var selectDate:Date
@Binding var isMoved:Bool
@Binding var isDark:Bool
@State var isAdvanced:Bool = false
@State var isHorizontal:Bool = false
@State var angleX: Double = 0
@State var angleY: Double = 0
//function
let content: (Date) -> DateView
init(
selection_:Binding<Date>,
isMoved_: Binding<Bool>,
isDark_:Binding<Bool>,
@ViewBuilder content: @escaping (Date) -> DateView
)
{
self._selectDate = selection_
self._isMoved = isMoved_
self._isDark = isDark_
self.content = content
}
}
分拆日历的部分:日历从单日->单周->单月->日历整体
由上到下:
整体:
struct CalenderView<DateView>: View where DateView:View
{
@Environment(\.calendar) var calendar
//attribute
@Binding var selectDate:Date
@Binding var isMoved:Bool
@Binding var isDark:Bool
@State var isAdvanced:Bool = false
@State var isHorizontal:Bool = false
@State var angleX: Double = 0
@State var angleY: Double = 0
//function
let content: (Date) -> DateView
init(
selection_:Binding<Date>,
isMoved_: Binding<Bool>,
isDark_:Binding<Bool>,
@ViewBuilder content: @escaping (Date) -> DateView
)
{
self._selectDate = selection_
self._isMoved = isMoved_
self._isDark = isDark_
self.content = content
}
//翻转日历的方法
func turnCalender(_ isAdvance : Bool = true, _ isHor : Bool = true)
{
var incValue:Int = 1
if isAdvance == false
{
incValue = -1
}
let todayComponrnts = self.calendar.dateComponents([Calendar.Component.day, Calendar.Component.year, Calendar.Component.month], from: self.selectDate)
var nextMonth:Int = todayComponrnts.month ?? 1
var currentYear:Int = todayComponrnts.year ?? 2021
if (isHor)
{
nextMonth += incValue
if nextMonth == 0
{
nextMonth = 12
currentYear += -1
}
if nextMonth > 12
{
nextMonth = 1
currentYear += 1
}
}
else
{
currentYear += incValue
}
var componrnts = DateComponents()
componrnts.year = currentYear
componrnts.month = nextMonth
componrnts.day = 1
self.selectDate = self.calendar.date(from: componrnts) ?? Date()
}
//计算翻转的方向
func calDirection(_ offsetSize: CGSize)
{
var offset:CGFloat = offsetSize.width
self.isHorizontal = true
if (abs(offsetSize.width) < abs(offsetSize.height))
{
offset = offsetSize.height
self.isHorizontal = false
}
self.isAdvanced = false
self.isMoved = false
if (offset < 0) {
self.isAdvanced = true
}
if(abs(offset) > 15)
{
self.isMoved = true
}
}
//翻转时需要播放动画
func playTrunsAnim(_ mode: Int = 1, _ animTimes: Int = 1, _ isAdvance: Bool = false)
{
var incAngle:Double = 360
if (isAdvance)
{
incAngle = -360
}
if (mode == 1)
{
self.angleY += (incAngle * Double(animTimes))
}
else if (mode == 2)
{
self.angleX -= (incAngle * Double(animTimes))
}
else if(mode == 3)
{
self.angleY = 0
self.angleX = 0
}
}
var body: some View
{
let tad = DragGesture()
.onChanged { value in
calDirection(value.translation)
}.onEnded { _ in
if (self.isMoved)
{
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.02) {
turnCalender(self.isAdvanced, self.isHorizontal)
}
var mode:Int = 2
if (self.isHorizontal)
{
mode = 1
}
playTrunsAnim(mode, 1, self.isAdvanced)
self.isMoved = false
}
}
VStack
{
Spacer(minLength: 50)
MonthView(isDark_: self.$isDark, selection: self.$selectDate, content: self.content)
.frame(width: 400, height: 400, alignment: .center)
.background(Color.init(.sRGB, white: 0, opacity: 0))
.padding()
.gesture(tad)
.rotation3DEffect(.degrees(angleY), axis: (x: 0, y: 1, z: 0))
.rotation3DEffect(.degrees(angleX), axis: (x: 1, y: 0, z: 0))
.animation(.interpolatingSpring(stiffness: 200, damping: 20).speed(1.2), value: [self.angleX, self.angleY])
Spacer(minLength: 20)
Spacer(minLength: 110)
}
.onTapGesture(count: 2) //双击回到当前日历
{
self.selectDate = Date()
playTrunsAnim(3, 1, false)
}
}
}
月历:月历需要显示头部的周信息
//month
struct MonthView<DateView>: View where DateView: View
{
@Environment(\.calendar) var calendar
@Binding var isDark: Bool
let selectDate: Date
let content:(Date) -> DateView
private let weekTitle: [String] = ["日", "一", "二", "三", "四", "五", "六"]
init(
isDark_:Binding<Bool>,
selection: Binding<Date>,
@ViewBuilder content: @escaping (Date) ->DateView
)
{
self._isDark = isDark_
self.selectDate = selection.wrappedValue
self.content = content
}
private var header: some View
{
let formatter = DateFormatter.monthAndYear
return Text(formatter.string(from: selectDate))
.font(.title)
.foregroundColor(config.getShareInstance().getFontColor(.normal, self.isDark ? .dark : .light))
.padding()
}
private var weekHeader: some View
{
return HStack
{
ForEach(weekTitle, id:\.self)
{title in
Text(title)
.frame(width: 38, height: 20, alignment: .center)
.foregroundColor(config.getShareInstance().getFontColor(.normal, self.isDark ? .dark : .light))
.padding(1)
}
}
}
private var weeks: [Date]
{
guard
let monthInterval = calendar.dateInterval(of: .month, for: selectDate)
else{ return[] }
return calendar.generateDates(inside: monthInterval, matching: DateComponents(hour:0, minute: 0, second: 0, weekday: 1))
}
var body: some View
{
VStack
{
header
weekHeader
ForEach(weeks, id:\.self){week in
WeekView(week: week, isDark_: self.$isDark, content: self.content)
}
Spacer()
}
}
}
周历:
//week
struct WeekView<DateView>: View where DateView:View
{
@Environment(\.calendar) var calendar
@Environment(\.colorScheme) var colorScheme
@Binding var isDark:Bool
let week: Date
let content: (Date) ->DateView
init(week: Date,
isDark_:Binding<Bool>,
@ViewBuilder content: @escaping (Date) ->DateView
)
{
self.week = week
self._isDark = isDark_
self.content = content
}
private var days: [Date]
{
guard
let weekInterval = calendar.dateInterval(of: .weekOfYear, for: week)
else {return []}
return calendar.generateDates(inside: weekInterval, matching: DateComponents(hour: 0, minute: 0, second: 0))
}
var body: some View
{
HStack
{
ForEach(days, id: \.self){ date in
HStack
{
if self.calendar.isDate(self.week, equalTo: date, toGranularity: .month)
{
self.content(date)
.frame(width: 40, height: 40, alignment: .center)
.font(.system(size: 20))
.foregroundColor(self.calendar.isDateInToday(date) ? Color.blue :config.getShareInstance().getFontColor(.normal, self.isDark ? .dark : .light))
}
else
{
self.content(date)
.frame(width: 40, height: 40, alignment: .center)
.font(.system(size: 20, weight: self.calendar.isDateInToday(date) ? Font.Weight.heavy : Font.Weight.light, design: .default))
.foregroundColor(Color.gray)
.disabled(true)
}
}
}
}
}
}
自定义的日历(此处展示的是用Button作为主题):
struct DateButton:View
{
@Environment(\.colorScheme) var colorScheme
@Environment(\.calendar) var calendar
@EnvironmentObject var tagDate:TagData
@State var showEditPage:Bool = false
@Binding var isMoved:Bool
@Binding var isDark:Bool
var date:Date
init(date_:Date,
isMoved_:Binding<Bool>,
isDark_:Binding<Bool>)
{
self.date = date_
self._isMoved = isMoved_
self._isDark = isDark_
}
func getTapGesture()->some Gesture
{
var tapGesture: some Gesture
{
TapGesture(count: 1)
.onEnded
{
if (!self.isMoved)
{
self.showEditPage = true
}
}
}
return tapGesture
}
var body: some View
{
Rectangle()
.fill(config.getShareInstance().getFontColor(.normal, self.isDark ? .light : .dark))
.frame(width: 40, height: 40, alignment: .center)
.gesture(self.getTapGesture())
.overlay(
ZStack
{
Text(String(self.calendar.component(.day, from: self.date)))
.frame(width: 40, height: 40, alignment: .center)
Image(systemName: "scribble.variable")
.frame(width: 1, height: 1, alignment: .init(horizontal: .leading, vertical: .top))
.foregroundColor(config.getShareInstance().getTagColor(self.isDark ? .dark : .light))
.scaleEffect(0.5)
.opacity(self.tagDate.getTagByDate(self.date, self.isDark ? .Survive : .Life) ? 0.5 : 0)
.offset(x: 8, y: -4.5)
}
)
.sheet(isPresented: self.$showEditPage, onDismiss:
{
self.showEditPage = false
}, content: {
EditPage(self.date, self.$isDark, self.$showEditPage)
.environmentObject(self.tagDate)
})
}
}
需要注意的是,翻转日历的过程会触碰到按钮,按钮会响应点击事件。所以需要传递一个被声明了状态的变量来区分点击与翻转。
如有问题可联系作者:rogerorion163.com