天天看点

SwiftUI 日历实战

构建一个日历,以反转和点击来控制。

首先,日历中每个日期作为一个独立的控件,应当由外部传入。所以,日历的日期控件使用@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